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 }