github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/cmd/logcli/main.go (about)

     1  package main
     2  
     3  import (
     4  	"log"
     5  	"math"
     6  	"net/url"
     7  	"os"
     8  	"runtime/pprof"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/prometheus/common/config"
    13  	"github.com/prometheus/common/version"
    14  	"gopkg.in/alecthomas/kingpin.v2"
    15  
    16  	"github.com/grafana/loki/pkg/logcli/client"
    17  	"github.com/grafana/loki/pkg/logcli/labelquery"
    18  	"github.com/grafana/loki/pkg/logcli/output"
    19  	"github.com/grafana/loki/pkg/logcli/query"
    20  	"github.com/grafana/loki/pkg/logcli/seriesquery"
    21  	_ "github.com/grafana/loki/pkg/util/build"
    22  )
    23  
    24  var (
    25  	app        = kingpin.New("logcli", "A command-line for loki.").Version(version.Print("logcli"))
    26  	quiet      = app.Flag("quiet", "Suppress query metadata").Default("false").Short('q').Bool()
    27  	statistics = app.Flag("stats", "Show query statistics").Default("false").Bool()
    28  	outputMode = app.Flag("output", "Specify output mode [default, raw, jsonl]. raw suppresses log labels and timestamp.").Default("default").Short('o').Enum("default", "raw", "jsonl")
    29  	timezone   = app.Flag("timezone", "Specify the timezone to use when formatting output timestamps [Local, UTC]").Default("Local").Short('z').Enum("Local", "UTC")
    30  	cpuProfile = app.Flag("cpuprofile", "Specify the location for writing a CPU profile.").Default("").String()
    31  	memProfile = app.Flag("memprofile", "Specify the location for writing a memory profile.").Default("").String()
    32  	stdin      = app.Flag("stdin", "Take input logs from stdin").Bool()
    33  
    34  	queryClient = newQueryClient(app)
    35  
    36  	queryCmd = app.Command("query", `Run a LogQL query.
    37  
    38  The "query" command is useful for querying for logs. Logs can be
    39  returned in a few output modes:
    40  
    41  	raw: log line
    42  	default: log timestamp + log labels + log line
    43  	jsonl: JSON response from Loki API of log line
    44  
    45  The output of the log can be specified with the "-o" flag, for
    46  example, "-o raw" for the raw output format.
    47  
    48  The "query" command will output extra information about the query
    49  and its results, such as the API URL, set of common labels, and set
    50  of excluded labels. This extra information can be suppressed with the
    51  --quiet flag.
    52  
    53  By default we look over the last hour of data; use --since to modify
    54  or provide specific start and end times with --from and --to respectively.
    55  
    56  Notice that when using --from and --to then ensure to use RFC3339Nano
    57  time format, but without timezone at the end. The local timezone will be added
    58  automatically or if using  --timezone flag.
    59  
    60  Example:
    61  
    62  	logcli query
    63  	   --timezone=UTC
    64  	   --from="2021-01-19T10:00:00Z"
    65  	   --to="2021-01-19T20:00:00Z"
    66  	   --output=jsonl
    67  	   'my-query'
    68  
    69  The output is limited to 30 entries by default; use --limit to increase.
    70  
    71  While "query" does support metrics queries, its output contains multiple
    72  data points between the start and end query time. This output is used to
    73  build graphs, similar to what is seen in the Grafana Explore graph view.
    74  If you are querying metrics and just want the most recent data point
    75  (like what is seen in the Grafana Explore table view), then you should use
    76  the "instant-query" command instead.`)
    77  	rangeQuery = newQuery(false, queryCmd)
    78  	tail       = queryCmd.Flag("tail", "Tail the logs").Short('t').Default("false").Bool()
    79  	follow     = queryCmd.Flag("follow", "Alias for --tail").Short('f').Default("false").Bool()
    80  	delayFor   = queryCmd.Flag("delay-for", "Delay in tailing by number of seconds to accumulate logs for re-ordering").Default("0").Int()
    81  
    82  	instantQueryCmd = app.Command("instant-query", `Run an instant LogQL query.
    83  
    84  The "instant-query" command is useful for evaluating a metric query for
    85  a single point in time. This is equivalent to the Grafana Explore table
    86  view; if you want a metrics query that is used to build a Grafana graph,
    87  you should use the "query" command instead.
    88  
    89  This command does not produce useful output when querying for log lines;
    90  you should always use the "query" command when you are running log queries.
    91  
    92  For more information about log queries and metric queries, refer to the
    93  LogQL documentation:
    94  
    95  https://grafana.com/docs/loki/latest/logql/`)
    96  	instantQuery = newQuery(true, instantQueryCmd)
    97  
    98  	labelsCmd   = app.Command("labels", "Find values for a given label.")
    99  	labelsQuery = newLabelQuery(labelsCmd)
   100  
   101  	seriesCmd = app.Command("series", `Run series query.
   102  
   103  The "series" command will take the provided label matcher
   104  and return all the log streams found in the time window.
   105  
   106  It is possible to send an empty label matcher '{}' to return all streams.
   107  
   108  Use the --analyze-labels flag to get a summary of the labels found in all streams.
   109  This is helpful to find high cardinality labels.
   110  `)
   111  	seriesQuery = newSeriesQuery(seriesCmd)
   112  )
   113  
   114  func main() {
   115  	log.SetOutput(os.Stderr)
   116  
   117  	cmd := kingpin.MustParse(app.Parse(os.Args[1:]))
   118  
   119  	if cpuProfile != nil && *cpuProfile != "" {
   120  		cpuFile, err := os.Create(*cpuProfile)
   121  		if err != nil {
   122  			log.Fatal("could not create CPU profile: ", err)
   123  		}
   124  		defer cpuFile.Close()
   125  		if err := pprof.StartCPUProfile(cpuFile); err != nil {
   126  			log.Fatal("could not start CPU profile: ", err)
   127  		}
   128  		defer pprof.StopCPUProfile()
   129  	}
   130  
   131  	if memProfile != nil && *memProfile != "" {
   132  		memFile, err := os.Create(*memProfile)
   133  		if err != nil {
   134  			log.Fatal("could not create memory profile: ", err)
   135  		}
   136  		defer memFile.Close()
   137  		defer func() {
   138  			if err := pprof.WriteHeapProfile(memFile); err != nil {
   139  				log.Fatal("could not write memory profile: ", err)
   140  			}
   141  		}()
   142  	}
   143  
   144  	if *stdin {
   145  		queryClient = client.NewFileClient(os.Stdin)
   146  		if rangeQuery.Step.Seconds() == 0 {
   147  			// Set default value for `step` based on `start` and `end`.
   148  			// In non-stdin case, this is set on Loki server side.
   149  			// If this is not set, then `step` will have default value of 1 nanosecond and `STepEvaluator` will go through every nanosecond when applying aggregation during metric queries.
   150  			rangeQuery.Step = defaultQueryRangeStep(rangeQuery.Start, rangeQuery.End)
   151  		}
   152  
   153  		// When `--stdin` flag is set, stream selector is optional in the query.
   154  		// But logQL package throw parser error if stream selector is not provided.
   155  		// So we inject "dummy" stream selector if not provided by user already.
   156  		// Which brings down to two ways of using LogQL query under `--stdin`.
   157  		// 1. Query with stream selector(e.g: `{foo="bar"}|="error"`)
   158  		// 2. Query without stream selector (e.g: `|="error"`)
   159  
   160  		qs := rangeQuery.QueryString
   161  		if strings.HasPrefix(strings.TrimSpace(qs), "|") {
   162  			// inject the dummy stream selector
   163  			qs = `{source="logcli"}` + qs
   164  			rangeQuery.QueryString = qs
   165  		}
   166  
   167  		// `--limit` doesn't make sense when using `--stdin` flag.
   168  		rangeQuery.Limit = math.MaxInt // TODO(kavi): is it a good idea?
   169  	}
   170  
   171  	switch cmd {
   172  	case queryCmd.FullCommand():
   173  		location, err := time.LoadLocation(*timezone)
   174  		if err != nil {
   175  			log.Fatalf("Unable to load timezone '%s': %s", *timezone, err)
   176  		}
   177  
   178  		outputOptions := &output.LogOutputOptions{
   179  			Timezone:      location,
   180  			NoLabels:      rangeQuery.NoLabels,
   181  			ColoredOutput: rangeQuery.ColoredOutput,
   182  		}
   183  
   184  		out, err := output.NewLogOutput(os.Stdout, *outputMode, outputOptions)
   185  		if err != nil {
   186  			log.Fatalf("Unable to create log output: %s", err)
   187  		}
   188  
   189  		if *tail || *follow {
   190  			rangeQuery.TailQuery(time.Duration(*delayFor)*time.Second, queryClient, out)
   191  		} else {
   192  			rangeQuery.DoQuery(queryClient, out, *statistics)
   193  		}
   194  	case instantQueryCmd.FullCommand():
   195  		location, err := time.LoadLocation(*timezone)
   196  		if err != nil {
   197  			log.Fatalf("Unable to load timezone '%s': %s", *timezone, err)
   198  		}
   199  
   200  		outputOptions := &output.LogOutputOptions{
   201  			Timezone:      location,
   202  			NoLabels:      instantQuery.NoLabels,
   203  			ColoredOutput: instantQuery.ColoredOutput,
   204  		}
   205  
   206  		out, err := output.NewLogOutput(os.Stdout, *outputMode, outputOptions)
   207  		if err != nil {
   208  			log.Fatalf("Unable to create log output: %s", err)
   209  		}
   210  
   211  		instantQuery.DoQuery(queryClient, out, *statistics)
   212  	case labelsCmd.FullCommand():
   213  		labelsQuery.DoLabels(queryClient)
   214  	case seriesCmd.FullCommand():
   215  		seriesQuery.DoSeries(queryClient)
   216  	}
   217  }
   218  
   219  func newQueryClient(app *kingpin.Application) client.Client {
   220  
   221  	client := &client.DefaultClient{
   222  		TLSConfig: config.TLSConfig{},
   223  	}
   224  
   225  	// extract host
   226  	addressAction := func(c *kingpin.ParseContext) error {
   227  		// If a proxy is to be used do not set TLS ServerName. In the case of HTTPS proxy this ensures
   228  		// the http client validates both the proxy's cert and the cert used by loki behind the proxy
   229  		// using the ServerName's from the provided --addr and --proxy-url flags.
   230  		if client.ProxyURL != "" {
   231  			return nil
   232  		}
   233  
   234  		u, err := url.Parse(client.Address)
   235  		if err != nil {
   236  			return err
   237  		}
   238  		client.TLSConfig.ServerName = strings.Split(u.Host, ":")[0]
   239  		return nil
   240  	}
   241  
   242  	app.Flag("addr", "Server address. Can also be set using LOKI_ADDR env var.").Default("http://localhost:3100").Envar("LOKI_ADDR").Action(addressAction).StringVar(&client.Address)
   243  	app.Flag("username", "Username for HTTP basic auth. Can also be set using LOKI_USERNAME env var.").Default("").Envar("LOKI_USERNAME").StringVar(&client.Username)
   244  	app.Flag("password", "Password for HTTP basic auth. Can also be set using LOKI_PASSWORD env var.").Default("").Envar("LOKI_PASSWORD").StringVar(&client.Password)
   245  	app.Flag("ca-cert", "Path to the server Certificate Authority. Can also be set using LOKI_CA_CERT_PATH env var.").Default("").Envar("LOKI_CA_CERT_PATH").StringVar(&client.TLSConfig.CAFile)
   246  	app.Flag("tls-skip-verify", "Server certificate TLS skip verify.").Default("false").Envar("LOKI_TLS_SKIP_VERIFY").BoolVar(&client.TLSConfig.InsecureSkipVerify)
   247  	app.Flag("cert", "Path to the client certificate. Can also be set using LOKI_CLIENT_CERT_PATH env var.").Default("").Envar("LOKI_CLIENT_CERT_PATH").StringVar(&client.TLSConfig.CertFile)
   248  	app.Flag("key", "Path to the client certificate key. Can also be set using LOKI_CLIENT_KEY_PATH env var.").Default("").Envar("LOKI_CLIENT_KEY_PATH").StringVar(&client.TLSConfig.KeyFile)
   249  	app.Flag("org-id", "adds X-Scope-OrgID to API requests for representing tenant ID. Useful for requesting tenant data when bypassing an auth gateway.").Default("").Envar("LOKI_ORG_ID").StringVar(&client.OrgID)
   250  	app.Flag("query-tags", "adds X-Query-Tags http header to API requests. This header value will be part of `metrics.go` statistics. Useful for tracking the query.").Default("").Envar("LOKI_QUERY_TAGS").StringVar(&client.QueryTags)
   251  	app.Flag("bearer-token", "adds the Authorization header to API requests for authentication purposes. Can also be set using LOKI_BEARER_TOKEN env var.").Default("").Envar("LOKI_BEARER_TOKEN").StringVar(&client.BearerToken)
   252  	app.Flag("bearer-token-file", "adds the Authorization header to API requests for authentication purposes. Can also be set using LOKI_BEARER_TOKEN_FILE env var.").Default("").Envar("LOKI_BEARER_TOKEN_FILE").StringVar(&client.BearerTokenFile)
   253  	app.Flag("retries", "How many times to retry each query when getting an error response from Loki. Can also be set using LOKI_CLIENT_RETRIES").Default("0").Envar("LOKI_CLIENT_RETRIES").IntVar(&client.Retries)
   254  	app.Flag("auth-header", "The authorization header used. Can also be set using LOKI_AUTH_HEADER").Default("Authorization").Envar("LOKI_AUTH_HEADER").StringVar(&client.AuthHeader)
   255  	app.Flag("proxy-url", "The http or https proxy to use when making requests. Can also be set using LOKI_HTTP_PROXY_URL env var.").Default("").Envar("LOKI_HTTP_PROXY_URL").StringVar(&client.ProxyURL)
   256  
   257  	return client
   258  }
   259  
   260  func newLabelQuery(cmd *kingpin.CmdClause) *labelquery.LabelQuery {
   261  	var labelName, from, to string
   262  	var since time.Duration
   263  
   264  	q := &labelquery.LabelQuery{}
   265  
   266  	// executed after all command flags are parsed
   267  	cmd.Action(func(c *kingpin.ParseContext) error {
   268  
   269  		defaultEnd := time.Now()
   270  		defaultStart := defaultEnd.Add(-since)
   271  
   272  		q.Start = mustParse(from, defaultStart)
   273  		q.End = mustParse(to, defaultEnd)
   274  		q.LabelName = labelName
   275  		q.Quiet = *quiet
   276  		return nil
   277  	})
   278  
   279  	cmd.Arg("label", "The name of the label.").Default("").StringVar(&labelName)
   280  	cmd.Flag("since", "Lookback window.").Default("1h").DurationVar(&since)
   281  	cmd.Flag("from", "Start looking for labels at this absolute time (inclusive)").StringVar(&from)
   282  	cmd.Flag("to", "Stop looking for labels at this absolute time (exclusive)").StringVar(&to)
   283  
   284  	return q
   285  }
   286  
   287  func newSeriesQuery(cmd *kingpin.CmdClause) *seriesquery.SeriesQuery {
   288  	// calculate series range from cli params
   289  	var from, to string
   290  	var since time.Duration
   291  
   292  	q := &seriesquery.SeriesQuery{}
   293  
   294  	// executed after all command flags are parsed
   295  	cmd.Action(func(c *kingpin.ParseContext) error {
   296  
   297  		defaultEnd := time.Now()
   298  		defaultStart := defaultEnd.Add(-since)
   299  
   300  		q.Start = mustParse(from, defaultStart)
   301  		q.End = mustParse(to, defaultEnd)
   302  		q.Quiet = *quiet
   303  		return nil
   304  	})
   305  
   306  	cmd.Arg("matcher", "eg '{foo=\"bar\",baz=~\".*blip\"}'").Required().StringVar(&q.Matcher)
   307  	cmd.Flag("since", "Lookback window.").Default("1h").DurationVar(&since)
   308  	cmd.Flag("from", "Start looking for logs at this absolute time (inclusive)").StringVar(&from)
   309  	cmd.Flag("to", "Stop looking for logs at this absolute time (exclusive)").StringVar(&to)
   310  	cmd.Flag("analyze-labels", "Printout a summary of labels including count of label value combinations, useful for debugging high cardinality series").BoolVar(&q.AnalyzeLabels)
   311  
   312  	return q
   313  }
   314  
   315  func newQuery(instant bool, cmd *kingpin.CmdClause) *query.Query {
   316  	// calculate query range from cli params
   317  	var now, from, to string
   318  	var since time.Duration
   319  
   320  	q := &query.Query{}
   321  
   322  	// executed after all command flags are parsed
   323  	cmd.Action(func(c *kingpin.ParseContext) error {
   324  
   325  		if instant {
   326  			q.SetInstant(mustParse(now, time.Now()))
   327  		} else {
   328  			defaultEnd := time.Now()
   329  			defaultStart := defaultEnd.Add(-since)
   330  
   331  			q.Start = mustParse(from, defaultStart)
   332  			q.End = mustParse(to, defaultEnd)
   333  		}
   334  		q.Quiet = *quiet
   335  		return nil
   336  	})
   337  
   338  	cmd.Flag("limit", "Limit on number of entries to print.").Default("30").IntVar(&q.Limit)
   339  	if instant {
   340  		cmd.Arg("query", "eg 'rate({foo=\"bar\"} |~ \".*error.*\" [5m])'").Required().StringVar(&q.QueryString)
   341  		cmd.Flag("now", "Time at which to execute the instant query.").StringVar(&now)
   342  	} else {
   343  		cmd.Arg("query", "eg '{foo=\"bar\",baz=~\".*blip\"} |~ \".*error.*\"'").Required().StringVar(&q.QueryString)
   344  		cmd.Flag("since", "Lookback window.").Default("1h").DurationVar(&since)
   345  		cmd.Flag("from", "Start looking for logs at this absolute time (inclusive)").StringVar(&from)
   346  		cmd.Flag("to", "Stop looking for logs at this absolute time (exclusive)").StringVar(&to)
   347  		cmd.Flag("step", "Query resolution step width, for metric queries. Evaluate the query at the specified step over the time range.").DurationVar(&q.Step)
   348  		cmd.Flag("interval", "Query interval, for log queries. Return entries at the specified interval, ignoring those between. **This parameter is experimental, please see Issue 1779**").DurationVar(&q.Interval)
   349  		cmd.Flag("batch", "Query batch size to use until 'limit' is reached").Default("1000").IntVar(&q.BatchSize)
   350  
   351  	}
   352  
   353  	cmd.Flag("forward", "Scan forwards through logs.").Default("false").BoolVar(&q.Forward)
   354  	cmd.Flag("no-labels", "Do not print any labels").Default("false").BoolVar(&q.NoLabels)
   355  	cmd.Flag("exclude-label", "Exclude labels given the provided key during output.").StringsVar(&q.IgnoreLabelsKey)
   356  	cmd.Flag("include-label", "Include labels given the provided key during output.").StringsVar(&q.ShowLabelsKey)
   357  	cmd.Flag("labels-length", "Set a fixed padding to labels").Default("0").IntVar(&q.FixedLabelsLen)
   358  	cmd.Flag("store-config", "Execute the current query using a configured storage from a given Loki configuration file.").Default("").StringVar(&q.LocalConfig)
   359  	cmd.Flag("remote-schema", "Execute the current query using a remote schema retrieved using the configured storage in the given Loki configuration file.").Default("false").BoolVar(&q.FetchSchemaFromStorage)
   360  	cmd.Flag("colored-output", "Show output with colored labels").Default("false").BoolVar(&q.ColoredOutput)
   361  
   362  	return q
   363  }
   364  
   365  func mustParse(t string, defaultTime time.Time) time.Time {
   366  	if t == "" {
   367  		return defaultTime
   368  	}
   369  
   370  	ret, err := time.Parse(time.RFC3339Nano, t)
   371  
   372  	if err != nil {
   373  		log.Fatalf("Unable to parse time %v", err)
   374  	}
   375  
   376  	return ret
   377  }
   378  
   379  // This method is to duplicate the same logic of `step` value from `start` and `end`
   380  // done on the loki server side.
   381  // https://github.com/grafana/loki/blob/main/pkg/loghttp/params.go
   382  func defaultQueryRangeStep(start, end time.Time) time.Duration {
   383  	step := int(math.Max(math.Floor(end.Sub(start).Seconds()/250), 1))
   384  	return time.Duration(step) * time.Second
   385  }