bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/graphite/graphite.go (about) 1 // Package graphite defines structures for interacting with a Graphite server. 2 package graphite // import "bosun.org/graphite" 3 4 import ( 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "net/url" 10 "strings" 11 "time" 12 ) 13 14 const requestErrFmt = "graphite RequestError (%s): %s" 15 16 // Request holds query objects. Currently only absolute times are supported. 17 type Request struct { 18 Start *time.Time 19 End *time.Time 20 Targets []string 21 URL *url.URL 22 } 23 24 type Response []Series 25 26 type Series struct { 27 Datapoints []DataPoint 28 Target string 29 } 30 31 type DataPoint []json.Number 32 33 func (r *Request) CacheKey() string { 34 targets, _ := json.Marshal(r.Targets) 35 return fmt.Sprintf("graphite-%d-%d-%s", r.Start.Unix(), r.End.Unix(), targets) 36 } 37 38 // Query performs a request to Graphite at the given host. host specifies 39 // a hostname with optional port, and may optionally begin with a scheme 40 // (http, https) to specify the protocol (http is the default). header is 41 // the headers to send. 42 func (r *Request) Query(host string, header http.Header) (Response, error) { 43 v := url.Values{ 44 "format": []string{"json"}, 45 "target": r.Targets, 46 } 47 if r.Start != nil { 48 v.Add("from", fmt.Sprint(r.Start.Unix())) 49 } 50 if r.End != nil { 51 v.Add("until", fmt.Sprint(r.End.Unix())) 52 } 53 r.URL = &url.URL{ 54 Scheme: "http", 55 Host: host, 56 Path: "/render/", 57 RawQuery: v.Encode(), 58 } 59 60 u, err := url.Parse(host) 61 if err == nil && u.Scheme != "" && u.Host != "" { 62 r.URL.Scheme = u.Scheme 63 r.URL.Host = u.Host 64 if u.Path != "" { 65 r.URL.Path = u.Path 66 } 67 r.URL.User = u.User 68 } 69 req, err := http.NewRequest("GET", r.URL.String(), nil) 70 if err != nil { 71 return nil, fmt.Errorf(requestErrFmt, r.URL, "NewRequest failed: "+err.Error()) 72 } 73 if header != nil { 74 req.Header = header 75 } 76 resp, err := DefaultClient.Do(req) 77 if err != nil { 78 return nil, fmt.Errorf(requestErrFmt, r.URL, "Get failed: "+err.Error()) 79 } 80 defer resp.Body.Close() 81 if resp.StatusCode != http.StatusOK { 82 tb, err := readTraceback(resp) 83 if err != nil { 84 tb = &[]string{"<Could not read traceback: " + err.Error() + ">"} 85 } 86 return nil, fmt.Errorf(requestErrFmt, r.URL, fmt.Sprintf("Get failed: %s\n%s", resp.Status, strings.Join(*tb, "\n"))) 87 } 88 var series Response 89 err = json.NewDecoder(resp.Body).Decode(&series) 90 if err != nil { 91 e := fmt.Errorf(requestErrFmt, r.URL, "Json decode failed: "+err.Error()) 92 return series, e 93 } 94 return series, nil 95 } 96 97 func readTraceback(resp *http.Response) (*[]string, error) { 98 bodyBytes, err := ioutil.ReadAll(resp.Body) 99 if err != nil { 100 return nil, err 101 } 102 bodyLines := strings.Split(strings.TrimSpace(string(bodyBytes)), "\n") 103 var tracebackLines []string 104 inTraceback := false 105 for _, line := range bodyLines { 106 if strings.HasPrefix(line, "Traceback") { 107 inTraceback = true 108 } else if inTraceback && line == "" { 109 break 110 } 111 if inTraceback { 112 tracebackLines = append(tracebackLines, line) 113 } 114 } 115 if len(tracebackLines) == 0 { 116 tracebackLines = []string{"<no traceback found in response>"} 117 } 118 return &tracebackLines, nil 119 } 120 121 // DefaultClient is the default HTTP client for requests. 122 var DefaultClient = &http.Client{ 123 Timeout: time.Minute, 124 } 125 126 // Context is the interface for querying a Graphite server. 127 type Context interface { 128 Query(*Request) (Response, error) 129 } 130 131 // Host is a simple Graphite Context with no additional features. 132 type Host string 133 134 // Query performs a request to a Graphite server. 135 func (h Host) Query(r *Request) (Response, error) { 136 return r.Query(string(h), nil) 137 } 138 139 type HostHeader struct { 140 Host string 141 Header http.Header 142 } 143 144 func (h HostHeader) Query(r *Request) (Response, error) { 145 return r.Query(h.Host, h.Header) 146 }