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  }