github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/logcli/client/client.go (about) 1 package client 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "net/http" 9 "net/url" 10 "path" 11 "strings" 12 "time" 13 14 "github.com/gorilla/websocket" 15 json "github.com/json-iterator/go" 16 "github.com/prometheus/common/config" 17 18 "github.com/grafana/loki/pkg/loghttp" 19 "github.com/grafana/loki/pkg/logproto" 20 "github.com/grafana/loki/pkg/util" 21 "github.com/grafana/loki/pkg/util/build" 22 ) 23 24 const ( 25 queryPath = "/loki/api/v1/query" 26 queryRangePath = "/loki/api/v1/query_range" 27 labelsPath = "/loki/api/v1/labels" 28 labelValuesPath = "/loki/api/v1/label/%s/values" 29 seriesPath = "/loki/api/v1/series" 30 tailPath = "/loki/api/v1/tail" 31 defaultAuthHeader = "Authorization" 32 ) 33 34 var userAgent = fmt.Sprintf("loki-logcli/%s", build.Version) 35 36 // Client contains all the methods to query a Loki instance, it's an interface to allow multiple implementations. 37 type Client interface { 38 Query(queryStr string, limit int, time time.Time, direction logproto.Direction, quiet bool) (*loghttp.QueryResponse, error) 39 QueryRange(queryStr string, limit int, start, end time.Time, direction logproto.Direction, step, interval time.Duration, quiet bool) (*loghttp.QueryResponse, error) 40 ListLabelNames(quiet bool, start, end time.Time) (*loghttp.LabelResponse, error) 41 ListLabelValues(name string, quiet bool, start, end time.Time) (*loghttp.LabelResponse, error) 42 Series(matchers []string, start, end time.Time, quiet bool) (*loghttp.SeriesResponse, error) 43 LiveTailQueryConn(queryStr string, delayFor time.Duration, limit int, start time.Time, quiet bool) (*websocket.Conn, error) 44 GetOrgID() string 45 } 46 47 // Tripperware can wrap a roundtripper. 48 type Tripperware func(http.RoundTripper) http.RoundTripper 49 50 // Client contains fields necessary to query a Loki instance 51 type DefaultClient struct { 52 TLSConfig config.TLSConfig 53 Username string 54 Password string 55 Address string 56 OrgID string 57 Tripperware Tripperware 58 BearerToken string 59 BearerTokenFile string 60 Retries int 61 QueryTags string 62 AuthHeader string 63 ProxyURL string 64 } 65 66 // Query uses the /api/v1/query endpoint to execute an instant query 67 // excluding interfacer b/c it suggests taking the interface promql.Node instead of logproto.Direction b/c it happens to have a String() method 68 // nolint:interfacer 69 func (c *DefaultClient) Query(queryStr string, limit int, time time.Time, direction logproto.Direction, quiet bool) (*loghttp.QueryResponse, error) { 70 qsb := util.NewQueryStringBuilder() 71 qsb.SetString("query", queryStr) 72 qsb.SetInt("limit", int64(limit)) 73 qsb.SetInt("time", time.UnixNano()) 74 qsb.SetString("direction", direction.String()) 75 76 return c.doQuery(queryPath, qsb.Encode(), quiet) 77 } 78 79 // QueryRange uses the /api/v1/query_range endpoint to execute a range query 80 // excluding interfacer b/c it suggests taking the interface promql.Node instead of logproto.Direction b/c it happens to have a String() method 81 // nolint:interfacer 82 func (c *DefaultClient) QueryRange(queryStr string, limit int, start, end time.Time, direction logproto.Direction, step, interval time.Duration, quiet bool) (*loghttp.QueryResponse, error) { 83 params := util.NewQueryStringBuilder() 84 params.SetString("query", queryStr) 85 params.SetInt32("limit", limit) 86 params.SetInt("start", start.UnixNano()) 87 params.SetInt("end", end.UnixNano()) 88 params.SetString("direction", direction.String()) 89 90 // The step is optional, so we do set it only if provided, 91 // otherwise we do leverage on the API defaults 92 if step != 0 { 93 params.SetFloat("step", step.Seconds()) 94 } 95 96 if interval != 0 { 97 params.SetFloat("interval", interval.Seconds()) 98 } 99 100 return c.doQuery(queryRangePath, params.Encode(), quiet) 101 } 102 103 // ListLabelNames uses the /api/v1/label endpoint to list label names 104 func (c *DefaultClient) ListLabelNames(quiet bool, start, end time.Time) (*loghttp.LabelResponse, error) { 105 var labelResponse loghttp.LabelResponse 106 params := util.NewQueryStringBuilder() 107 params.SetInt("start", start.UnixNano()) 108 params.SetInt("end", end.UnixNano()) 109 110 if err := c.doRequest(labelsPath, params.Encode(), quiet, &labelResponse); err != nil { 111 return nil, err 112 } 113 return &labelResponse, nil 114 } 115 116 // ListLabelValues uses the /api/v1/label endpoint to list label values 117 func (c *DefaultClient) ListLabelValues(name string, quiet bool, start, end time.Time) (*loghttp.LabelResponse, error) { 118 path := fmt.Sprintf(labelValuesPath, url.PathEscape(name)) 119 var labelResponse loghttp.LabelResponse 120 params := util.NewQueryStringBuilder() 121 params.SetInt("start", start.UnixNano()) 122 params.SetInt("end", end.UnixNano()) 123 if err := c.doRequest(path, params.Encode(), quiet, &labelResponse); err != nil { 124 return nil, err 125 } 126 return &labelResponse, nil 127 } 128 129 func (c *DefaultClient) Series(matchers []string, start, end time.Time, quiet bool) (*loghttp.SeriesResponse, error) { 130 params := util.NewQueryStringBuilder() 131 params.SetInt("start", start.UnixNano()) 132 params.SetInt("end", end.UnixNano()) 133 params.SetStringArray("match", matchers) 134 135 var seriesResponse loghttp.SeriesResponse 136 if err := c.doRequest(seriesPath, params.Encode(), quiet, &seriesResponse); err != nil { 137 return nil, err 138 } 139 return &seriesResponse, nil 140 } 141 142 // LiveTailQueryConn uses /api/prom/tail to set up a websocket connection and returns it 143 func (c *DefaultClient) LiveTailQueryConn(queryStr string, delayFor time.Duration, limit int, start time.Time, quiet bool) (*websocket.Conn, error) { 144 params := util.NewQueryStringBuilder() 145 params.SetString("query", queryStr) 146 if delayFor != 0 { 147 params.SetInt("delay_for", int64(delayFor.Seconds())) 148 } 149 params.SetInt("limit", int64(limit)) 150 params.SetInt("start", start.UnixNano()) 151 152 return c.wsConnect(tailPath, params.Encode(), quiet) 153 } 154 155 func (c *DefaultClient) GetOrgID() string { 156 return c.OrgID 157 } 158 159 func (c *DefaultClient) doQuery(path string, query string, quiet bool) (*loghttp.QueryResponse, error) { 160 var err error 161 var r loghttp.QueryResponse 162 163 if err = c.doRequest(path, query, quiet, &r); err != nil { 164 return nil, err 165 } 166 167 return &r, nil 168 } 169 170 func (c *DefaultClient) doRequest(path, query string, quiet bool, out interface{}) error { 171 us, err := buildURL(c.Address, path, query) 172 if err != nil { 173 return err 174 } 175 if !quiet { 176 log.Print(us) 177 } 178 179 req, err := http.NewRequest("GET", us, nil) 180 if err != nil { 181 return err 182 } 183 184 h, err := c.getHTTPRequestHeader() 185 if err != nil { 186 return err 187 } 188 req.Header = h 189 190 // Parse the URL to extract the host 191 clientConfig := config.HTTPClientConfig{ 192 TLSConfig: c.TLSConfig, 193 } 194 195 if c.ProxyURL != "" { 196 prox, err := url.Parse(c.ProxyURL) 197 if err != nil { 198 return err 199 } 200 clientConfig.ProxyURL = config.URL{URL: prox} 201 } 202 203 client, err := config.NewClientFromConfig(clientConfig, "promtail", config.WithHTTP2Disabled()) 204 if err != nil { 205 return err 206 } 207 if c.Tripperware != nil { 208 client.Transport = c.Tripperware(client.Transport) 209 } 210 211 var resp *http.Response 212 attempts := c.Retries + 1 213 success := false 214 215 for attempts > 0 { 216 attempts-- 217 218 resp, err = client.Do(req) 219 if err != nil { 220 log.Println("error sending request", err) 221 continue 222 } 223 if resp.StatusCode/100 != 2 { 224 buf, _ := ioutil.ReadAll(resp.Body) // nolint 225 log.Printf("Error response from server: %s (%v) attempts remaining: %d", string(buf), err, attempts) 226 if err := resp.Body.Close(); err != nil { 227 log.Println("error closing body", err) 228 } 229 continue 230 } 231 success = true 232 break 233 } 234 if !success { 235 return fmt.Errorf("Run out of attempts while querying the server") 236 } 237 238 defer func() { 239 if err := resp.Body.Close(); err != nil { 240 log.Println("error closing body", err) 241 } 242 }() 243 return json.NewDecoder(resp.Body).Decode(out) 244 } 245 246 func (c *DefaultClient) getHTTPRequestHeader() (http.Header, error) { 247 h := make(http.Header) 248 249 if c.Username != "" && c.Password != "" { 250 if c.AuthHeader == "" { 251 c.AuthHeader = defaultAuthHeader 252 } 253 h.Set( 254 c.AuthHeader, 255 "Basic "+base64.StdEncoding.EncodeToString([]byte(c.Username+":"+c.Password)), 256 ) 257 } 258 259 h.Set("User-Agent", userAgent) 260 261 if c.OrgID != "" { 262 h.Set("X-Scope-OrgID", c.OrgID) 263 } 264 265 if c.QueryTags != "" { 266 h.Set("X-Query-Tags", c.QueryTags) 267 } 268 269 if (c.Username != "" || c.Password != "") && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) { 270 return nil, fmt.Errorf("at most one of HTTP basic auth (username/password), bearer-token & bearer-token-file is allowed to be configured") 271 } 272 273 if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 { 274 return nil, fmt.Errorf("at most one of the options bearer-token & bearer-token-file is allowed to be configured") 275 } 276 277 if c.BearerToken != "" { 278 if c.AuthHeader == "" { 279 c.AuthHeader = defaultAuthHeader 280 } 281 282 h.Set(c.AuthHeader, "Bearer "+c.BearerToken) 283 } 284 285 if c.BearerTokenFile != "" { 286 b, err := ioutil.ReadFile(c.BearerTokenFile) 287 if err != nil { 288 return nil, fmt.Errorf("unable to read authorization credentials file %s: %s", c.BearerTokenFile, err) 289 } 290 bearerToken := strings.TrimSpace(string(b)) 291 if c.AuthHeader == "" { 292 c.AuthHeader = defaultAuthHeader 293 } 294 h.Set(c.AuthHeader, "Bearer "+bearerToken) 295 } 296 return h, nil 297 } 298 299 func (c *DefaultClient) wsConnect(path, query string, quiet bool) (*websocket.Conn, error) { 300 us, err := buildURL(c.Address, path, query) 301 if err != nil { 302 return nil, err 303 } 304 305 tlsConfig, err := config.NewTLSConfig(&c.TLSConfig) 306 if err != nil { 307 return nil, err 308 } 309 310 if strings.HasPrefix(us, "http") { 311 us = strings.Replace(us, "http", "ws", 1) 312 } 313 314 if !quiet { 315 log.Println(us) 316 } 317 318 h, err := c.getHTTPRequestHeader() 319 if err != nil { 320 return nil, err 321 } 322 323 ws := websocket.Dialer{ 324 TLSClientConfig: tlsConfig, 325 } 326 327 if c.ProxyURL != "" { 328 ws.Proxy = func(req *http.Request) (*url.URL, error) { 329 return url.Parse(c.ProxyURL) 330 } 331 } 332 333 conn, resp, err := ws.Dial(us, h) 334 if err != nil { 335 if resp == nil { 336 return nil, err 337 } 338 buf, _ := ioutil.ReadAll(resp.Body) // nolint 339 return nil, fmt.Errorf("Error response from server: %s (%v)", string(buf), err) 340 } 341 342 return conn, nil 343 } 344 345 // buildURL concats a url `http://foo/bar` with a path `/buzz`. 346 func buildURL(u, p, q string) (string, error) { 347 url, err := url.Parse(u) 348 if err != nil { 349 return "", err 350 } 351 url.Path = path.Join(url.Path, p) 352 url.RawQuery = q 353 return url.String(), nil 354 }