From 47637852fbf5a00eddadf0c3a2df2f0915af0494 Mon Sep 17 00:00:00 2001 From: Joe Elliott Date: Thu, 12 Sep 2019 12:02:04 -0400 Subject: [PATCH] Logcli: Add Support for New Query Path (#987) Signed-off-by: Joe Elliott --- cmd/logcli/client.go | 167 ------------- cmd/logcli/labels.go | 32 --- cmd/logcli/main.go | 171 +++++++++++--- cmd/logcli/query.go | 114 --------- docs/logcli.md | 18 +- pkg/iter/iterator.go | 9 + pkg/logcli/client/client.go | 223 ++++++++++++++++++ pkg/logcli/labelquery/labels.go | 39 +++ pkg/logcli/query/query.go | 164 +++++++++++++ .../logcli => pkg/logcli/query}/query_test.go | 2 +- {cmd/logcli => pkg/logcli/query}/tail.go | 28 ++- {cmd/logcli => pkg/logcli/query}/utils.go | 12 +- 12 files changed, 607 insertions(+), 372 deletions(-) delete mode 100644 cmd/logcli/client.go delete mode 100644 cmd/logcli/labels.go delete mode 100644 cmd/logcli/query.go create mode 100644 pkg/logcli/client/client.go create mode 100644 pkg/logcli/labelquery/labels.go create mode 100644 pkg/logcli/query/query.go rename {cmd/logcli => pkg/logcli/query}/query_test.go (99%) rename {cmd/logcli => pkg/logcli/query}/tail.go (60%) rename {cmd/logcli => pkg/logcli/query}/utils.go (82%) diff --git a/cmd/logcli/client.go b/cmd/logcli/client.go deleted file mode 100644 index 777b54498b12..000000000000 --- a/cmd/logcli/client.go +++ /dev/null @@ -1,167 +0,0 @@ -package main - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "strings" - "time" - - "github.com/gorilla/websocket" - "github.com/prometheus/common/config" - - "github.com/grafana/loki/pkg/logproto" -) - -const ( - queryPath = "/api/prom/query?query=%s&limit=%d&start=%d&end=%d&direction=%s" - labelsPath = "/api/prom/label" - labelValuesPath = "/api/prom/label/%s/values" - tailPath = "/api/prom/tail?query=%s&delay_for=%d&limit=%d&start=%d" -) - -func query(from, through time.Time, direction logproto.Direction) (*logproto.QueryResponse, error) { - path := fmt.Sprintf(queryPath, - url.QueryEscape(*queryStr), // query - *limit, // limit - from.UnixNano(), // start - through.UnixNano(), // end - direction.String(), // direction - ) - - var resp logproto.QueryResponse - if err := doRequest(path, &resp); err != nil { - return nil, err - } - - return &resp, nil -} - -func listLabelNames() (*logproto.LabelResponse, error) { - var labelResponse logproto.LabelResponse - if err := doRequest(labelsPath, &labelResponse); err != nil { - return nil, err - } - return &labelResponse, nil -} - -func listLabelValues(name string) (*logproto.LabelResponse, error) { - path := fmt.Sprintf(labelValuesPath, url.PathEscape(name)) - var labelResponse logproto.LabelResponse - if err := doRequest(path, &labelResponse); err != nil { - return nil, err - } - return &labelResponse, nil -} - -func doRequest(path string, out interface{}) error { - us := *addr + path - if !*quiet { - log.Print(us) - } - - req, err := http.NewRequest("GET", us, nil) - if err != nil { - return err - } - - req.SetBasicAuth(*username, *password) - - // Parse the URL to extract the host - u, err := url.Parse(us) - if err != nil { - return err - } - clientConfig := config.HTTPClientConfig{ - TLSConfig: config.TLSConfig{ - CAFile: *tlsCACertPath, - CertFile: *tlsClientCertPath, - KeyFile: *tlsClientCertKeyPath, - ServerName: u.Host, - InsecureSkipVerify: *tlsSkipVerify, - }, - } - - client, err := config.NewClientFromConfig(clientConfig, "logcli") - if err != nil { - return err - } - - resp, err := client.Do(req) - if err != nil { - return err - } - defer func() { - if err := resp.Body.Close(); err != nil { - log.Println("error closing body", err) - } - }() - - if resp.StatusCode/100 != 2 { - buf, _ := ioutil.ReadAll(resp.Body) // nolint - return fmt.Errorf("Error response from server: %s (%v)", string(buf), err) - } - - return json.NewDecoder(resp.Body).Decode(out) -} - -func liveTailQueryConn() (*websocket.Conn, error) { - path := fmt.Sprintf(tailPath, - url.QueryEscape(*queryStr), // query - *delayFor, // delay_for - *limit, // limit - getStart(time.Now()).UnixNano(), // start - ) - return wsConnect(path) -} - -func wsConnect(path string) (*websocket.Conn, error) { - us := *addr + path - - // Parse the URL to extract the host - u, err := url.Parse(us) - if err != nil { - return nil, err - } - tlsConfig, err := config.NewTLSConfig(&config.TLSConfig{ - CAFile: *tlsCACertPath, - CertFile: *tlsClientCertPath, - KeyFile: *tlsClientCertKeyPath, - ServerName: u.Host, - InsecureSkipVerify: *tlsSkipVerify, - }) - if err != nil { - return nil, err - } - - if strings.HasPrefix(us, "https") { - us = strings.Replace(us, "https", "wss", 1) - } else if strings.HasPrefix(us, "http") { - us = strings.Replace(us, "http", "ws", 1) - } - if !*quiet { - log.Println(us) - } - - h := http.Header{"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte(*username+":"+*password))}} - - ws := websocket.Dialer{ - TLSClientConfig: tlsConfig, - } - - c, resp, err := ws.Dial(us, h) - - if err != nil { - if resp == nil { - return nil, err - } - buf, _ := ioutil.ReadAll(resp.Body) // nolint - return nil, fmt.Errorf("Error response from server: %s (%v)", string(buf), err) - } - - return c, nil -} diff --git a/cmd/logcli/labels.go b/cmd/logcli/labels.go deleted file mode 100644 index 4f52fa422859..000000000000 --- a/cmd/logcli/labels.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "github.com/grafana/loki/pkg/logproto" -) - -func doLabels() { - var labelResponse *logproto.LabelResponse - var err error - if len(*labelName) > 0 { - labelResponse, err = listLabelValues(*labelName) - } else { - labelResponse, err = listLabelNames() - } - if err != nil { - log.Fatalf("Error doing request: %+v", err) - } - for _, value := range labelResponse.Values { - fmt.Println(value) - } -} - -func listLabels() []string { - labelResponse, err := listLabelNames() - if err != nil { - log.Fatalf("Error fetching labels: %+v", err) - } - return labelResponse.Values -} diff --git a/cmd/logcli/main.go b/cmd/logcli/main.go index 6c2b48fbf280..23ba98edea28 100644 --- a/cmd/logcli/main.go +++ b/cmd/logcli/main.go @@ -2,10 +2,16 @@ package main import ( "log" + "net/url" "os" "time" + "github.com/grafana/loki/pkg/logcli/client" + "github.com/grafana/loki/pkg/logcli/labelquery" "github.com/grafana/loki/pkg/logcli/output" + "github.com/grafana/loki/pkg/logcli/query" + "github.com/prometheus/common/config" + "gopkg.in/alecthomas/kingpin.v2" ) @@ -15,31 +21,18 @@ var ( outputMode = app.Flag("output", "specify output mode [default, raw, jsonl]").Default("default").Short('o').Enum("default", "raw", "jsonl") timezone = app.Flag("timezone", "Specify the timezone to use when formatting output timestamps [Local, UTC]").Default("Local").Short('z').Enum("Local", "UTC") - addr = app.Flag("addr", "Server address.").Default("https://logs-us-west1.grafana.net").Envar("GRAFANA_ADDR").String() - - username = app.Flag("username", "Username for HTTP basic auth.").Default("").Envar("GRAFANA_USERNAME").String() - password = app.Flag("password", "Password for HTTP basic auth.").Default("").Envar("GRAFANA_PASSWORD").String() - - tlsCACertPath = app.Flag("ca-cert", "Path to the server Certificate Authority.").Default("").Envar("LOKI_CA_CERT_PATH").String() - tlsSkipVerify = app.Flag("tls-skip-verify", "Server certificate TLS skip verify.").Default("false").Bool() - tlsClientCertPath = app.Flag("cert", "Path to the client certificate.").Default("").Envar("LOKI_CLIENT_CERT_PATH").String() - tlsClientCertKeyPath = app.Flag("key", "Path to the client certificate key.").Default("").Envar("LOKI_CLIENT_KEY_PATH").String() - - queryCmd = app.Command("query", "Run a LogQL query.") - queryStr = queryCmd.Arg("query", "eg '{foo=\"bar\",baz=\"blip\"}'").Required().String() - limit = queryCmd.Flag("limit", "Limit on number of entries to print.").Default("30").Int() - since = queryCmd.Flag("since", "Lookback window.").Default("1h").Duration() - from = queryCmd.Flag("from", "Start looking for logs at this absolute time (inclusive)").String() - to = queryCmd.Flag("to", "Stop looking for logs at this absolute time (exclusive)").String() - forward = queryCmd.Flag("forward", "Scan forwards through logs.").Default("false").Bool() - tail = queryCmd.Flag("tail", "Tail the logs").Short('t').Default("false").Bool() - delayFor = queryCmd.Flag("delay-for", "Delay in tailing by number of seconds to accumulate logs for re-ordering").Default("0").Int() - noLabels = queryCmd.Flag("no-labels", "Do not print any labels").Default("false").Bool() - ignoreLabelsKey = queryCmd.Flag("exclude-label", "Exclude labels given the provided key during output.").Strings() - showLabelsKey = queryCmd.Flag("include-label", "Include labels given the provided key during output.").Strings() - fixedLabelsLen = queryCmd.Flag("labels-length", "Set a fixed padding to labels").Default("0").Int() - labelsCmd = app.Command("labels", "Find values for a given label.") - labelName = labelsCmd.Arg("label", "The name of the label.").HintAction(listLabels).String() + queryClient = newQueryClient(app) + + queryCmd = app.Command("query", "Run a LogQL query.") + rangeQuery = newQuery(false, queryCmd) + tail = queryCmd.Flag("tail", "Tail the logs").Short('t').Default("false").Bool() + delayFor = queryCmd.Flag("delay-for", "Delay in tailing by number of seconds to accumulate logs for re-ordering").Default("0").Int() + + instantQueryCmd = app.Command("instant-query", "Run an instant LogQL query") + instantQuery = newQuery(true, instantQueryCmd) + + labelsCmd = app.Command("labels", "Find values for a given label.") + labelName = labelsCmd.Arg("label", "The name of the label.").HintAction(hintActionLabelNames).String() ) func main() { @@ -47,10 +40,6 @@ func main() { cmd := kingpin.MustParse(app.Parse(os.Args[1:])) - if *addr == "" { - log.Fatalln("Server address cannot be empty") - } - switch cmd { case queryCmd.FullCommand(): location, err := time.LoadLocation(*timezone) @@ -60,7 +49,28 @@ func main() { outputOptions := &output.LogOutputOptions{ Timezone: location, - NoLabels: *noLabels, + NoLabels: rangeQuery.NoLabels, + } + + out, err := output.NewLogOutput(*outputMode, outputOptions) + if err != nil { + log.Fatalf("Unable to create log output: %s", err) + } + + if *tail { + rangeQuery.TailQuery(*delayFor, queryClient, out) + } else { + rangeQuery.DoQuery(queryClient, out) + } + case instantQueryCmd.FullCommand(): + location, err := time.LoadLocation(*timezone) + if err != nil { + log.Fatalf("Unable to load timezone '%s': %s", *timezone, err) + } + + outputOptions := &output.LogOutputOptions{ + Timezone: location, + NoLabels: instantQuery.NoLabels, } out, err := output.NewLogOutput(*outputMode, outputOptions) @@ -68,8 +78,105 @@ func main() { log.Fatalf("Unable to create log output: %s", err) } - doQuery(out) + instantQuery.DoQuery(queryClient, out) case labelsCmd.FullCommand(): - doLabels() + q := newLabelQuery(*labelName, *quiet) + + q.DoLabels(queryClient) + } +} + +func hintActionLabelNames() []string { + q := newLabelQuery("", *quiet) + + return q.ListLabels(queryClient) +} + +func newQueryClient(app *kingpin.Application) *client.Client { + client := &client.Client{ + TLSConfig: config.TLSConfig{}, } + + // extract host + addressAction := func(c *kingpin.ParseContext) error { + u, err := url.Parse(client.Address) + if err != nil { + return err + } + client.TLSConfig.ServerName = u.Host + return nil + } + + app.Flag("addr", "Server address.").Default("https://logs-us-west1.grafana.net").Envar("GRAFANA_ADDR").Action(addressAction).StringVar(&client.Address) + app.Flag("username", "Username for HTTP basic auth.").Default("").Envar("GRAFANA_USERNAME").StringVar(&client.Username) + app.Flag("password", "Password for HTTP basic auth.").Default("").Envar("GRAFANA_PASSWORD").StringVar(&client.Password) + app.Flag("ca-cert", "Path to the server Certificate Authority.").Default("").Envar("LOKI_CA_CERT_PATH").StringVar(&client.TLSConfig.CAFile) + app.Flag("tls-skip-verify", "Server certificate TLS skip verify.").Default("false").BoolVar(&client.TLSConfig.InsecureSkipVerify) + app.Flag("cert", "Path to the client certificate.").Default("").Envar("LOKI_CLIENT_CERT_PATH").StringVar(&client.TLSConfig.CertFile) + app.Flag("key", "Path to the client certificate key.").Default("").Envar("LOKI_CLIENT_KEY_PATH").StringVar(&client.TLSConfig.KeyFile) + + return client +} + +func newLabelQuery(labelName string, quiet bool) *labelquery.LabelQuery { + return &labelquery.LabelQuery{ + LabelName: labelName, + Quiet: quiet, + } +} + +func newQuery(instant bool, cmd *kingpin.CmdClause) *query.Query { + // calculcate query range from cli params + var now, from, to string + var since time.Duration + + query := &query.Query{} + + // executed after all command flags are parsed + cmd.Action(func(c *kingpin.ParseContext) error { + + if instant { + query.SetInstant(mustParse(now, time.Now())) + } else { + defaultEnd := time.Now() + defaultStart := defaultEnd.Add(-since) + + query.Start = mustParse(from, defaultStart) + query.End = mustParse(to, defaultEnd) + } + + return nil + }) + + cmd.Arg("query", "eg '{foo=\"bar\",baz=~\".*blip\"} |~ \".*error.*\"'").Required().StringVar(&query.QueryString) + cmd.Flag("limit", "Limit on number of entries to print.").Default("30").IntVar(&query.Limit) + if instant { + cmd.Flag("now", "Time at which to execute the instant query.").StringVar(&now) + } else { + cmd.Flag("since", "Lookback window.").Default("1h").DurationVar(&since) + cmd.Flag("from", "Start looking for logs at this absolute time (inclusive)").StringVar(&from) + cmd.Flag("to", "Stop looking for logs at this absolute time (exclusive)").StringVar(&to) + } + + cmd.Flag("forward", "Scan forwards through logs.").Default("false").BoolVar(&query.Forward) + cmd.Flag("no-labels", "Do not print any labels").Default("false").BoolVar(&query.NoLabels) + cmd.Flag("exclude-label", "Exclude labels given the provided key during output.").StringsVar(&query.IgnoreLabelsKey) + cmd.Flag("include-label", "Include labels given the provided key during output.").StringsVar(&query.ShowLabelsKey) + cmd.Flag("labels-length", "Set a fixed padding to labels").Default("0").IntVar(&query.FixedLabelsLen) + + return query +} + +func mustParse(t string, defaultTime time.Time) time.Time { + if t == "" { + return defaultTime + } + + ret, err := time.Parse(time.RFC3339Nano, t) + + if err != nil { + log.Fatalf("Unable to parse time %v", err) + } + + return ret } diff --git a/cmd/logcli/query.go b/cmd/logcli/query.go deleted file mode 100644 index a6b6c611a0b8..000000000000 --- a/cmd/logcli/query.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "fmt" - "log" - "strings" - "time" - - "github.com/fatih/color" - "github.com/prometheus/prometheus/pkg/labels" - - "github.com/grafana/loki/pkg/iter" - "github.com/grafana/loki/pkg/logcli/output" - "github.com/grafana/loki/pkg/logproto" -) - -func getStart(end time.Time) time.Time { - start := end.Add(-*since) - if *from != "" { - var err error - start, err = time.Parse(time.RFC3339Nano, *from) - if err != nil { - log.Fatalf("error parsing date '%s': %s", *from, err) - } - } - return start -} - -func doQuery(out output.LogOutput) { - if *tail { - tailQuery(out) - return - } - - var ( - i iter.EntryIterator - common labels.Labels - ) - - end := time.Now() - start := getStart(end) - - if *to != "" { - var err error - end, err = time.Parse(time.RFC3339Nano, *to) - if err != nil { - log.Fatalf("error parsing --to date '%s': %s", *to, err) - } - } - - d := logproto.BACKWARD - if *forward { - d = logproto.FORWARD - } - - resp, err := query(start, end, d) - if err != nil { - log.Fatalf("Query failed: %+v", err) - } - - cache, lss := parseLabels(resp) - - labelsCache := func(labels string) labels.Labels { - return cache[labels] - } - - common = commonLabels(lss) - - // Remove the labels we want to show from common - if len(*showLabelsKey) > 0 { - common = common.MatchLabels(false, *showLabelsKey...) - } - - if len(common) > 0 && !*quiet { - log.Println("Common labels:", color.RedString(common.String())) - } - - if len(*ignoreLabelsKey) > 0 && !*quiet { - log.Println("Ignoring labels key:", color.RedString(strings.Join(*ignoreLabelsKey, ","))) - } - - // Remove ignored and common labels from the cached labels and - // calculate the max labels length - maxLabelsLen := *fixedLabelsLen - for key, ls := range cache { - // Remove common labels - ls = subtract(ls, common) - - // Remove ignored labels - if len(*ignoreLabelsKey) > 0 { - ls = ls.MatchLabels(false, *ignoreLabelsKey...) - } - - // Update cached labels - cache[key] = ls - - // Update max labels length - len := len(ls.String()) - if maxLabelsLen < len { - maxLabelsLen = len - } - } - - i = iter.NewQueryResponseIterator(resp, d) - - for i.Next() { - ls := labelsCache(i.Labels()) - fmt.Println(out.Format(i.Entry().Timestamp, &ls, maxLabelsLen, i.Entry().Line)) - } - - if err := i.Error(); err != nil { - log.Fatalf("Error from iterator: %v", err) - } -} diff --git a/docs/logcli.md b/docs/logcli.md index 4a137365ca89..495fa74aef9f 100644 --- a/docs/logcli.md +++ b/docs/logcli.md @@ -48,7 +48,7 @@ cortex-ops/cortex-gw ... $ logcli query '{job="cortex-ops/consul"}' -https://logs-dev-ops-tools1.grafana.net/api/prom/query?query=%7Bjob%3D%22cortex-ops%2Fconsul%22%7D&limit=30&start=1529928228&end=1529931828&direction=backward®exp= +https://logs-dev-ops-tools1.grafana.net/api/v1/query_range?query=%7Bjob%3D%22cortex-ops%2Fconsul%22%7D&limit=30&start=1529928228&end=1529931828&direction=backward®exp= Common labels: {job="cortex-ops/consul", namespace="cortex-ops"} 2018-06-25T12:52:09Z {instance="consul-8576459955-pl75w"} 2018/06/25 12:52:09 [INFO] raft: Snapshot to 475409 complete 2018-06-25T12:52:09Z {instance="consul-8576459955-pl75w"} 2018/06/25 12:52:09 [INFO] raft: Compacting logs from 456973 to 465169 @@ -75,6 +75,7 @@ Flags: --help Show context-sensitive help (also try --help-long and --help-man). -q, --quiet suppress everything but log lines -o, --output=default specify output mode [default, raw, jsonl] + -z, --timezone=Local Specify the timezone to use when formatting output timestamps [Local, UTC] --addr="https://logs-us-west1.grafana.net" Server address. --username="" Username for HTTP basic auth. @@ -88,14 +89,17 @@ Commands: help [...] Show help. - query [] [] + query [] Run a LogQL query. + instant-query [] + Run an instant LogQL query + labels [