diff --git a/cmd/logcli/client.go b/cmd/logcli/client.go index 14b53a752774..ed4bd3d9c83a 100644 --- a/cmd/logcli/client.go +++ b/cmd/logcli/client.go @@ -55,7 +55,9 @@ func listLabelValues(name string) (*logproto.LabelResponse, error) { func doRequest(path string, out interface{}) error { url := *addr + path - log.Print(url) + if !*quiet { + log.Print(url) + } req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -121,7 +123,9 @@ func wsConnect(path string) (*websocket.Conn, error) { } else if strings.HasPrefix(url, "http") { url = strings.Replace(url, "http", "ws", 1) } - log.Println(url) + if !*quiet { + log.Println(url) + } h := http.Header{"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte(*username+":"+*password))}} diff --git a/cmd/logcli/main.go b/cmd/logcli/main.go index 6350964a2efb..35a301f0fedd 100644 --- a/cmd/logcli/main.go +++ b/cmd/logcli/main.go @@ -8,7 +8,9 @@ import ( ) var ( - app = kingpin.New("logcli", "A command-line for loki.") + app = kingpin.New("logcli", "A command-line for loki.") + quiet = app.Flag("quiet", "suppress everything but log lines").Default("false").Short('q').Bool() + outputMode = app.Flag("output", "specify output mode [default, raw, jsonl]").Default("default").Short('o').Enum("default", "raw", "jsonl") 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() diff --git a/cmd/logcli/output.go b/cmd/logcli/output.go new file mode 100644 index 000000000000..d822775058f2 --- /dev/null +++ b/cmd/logcli/output.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "strings" + "time" + + "github.com/fatih/color" + "github.com/prometheus/prometheus/pkg/labels" +) + +// Outputs is an enum with all possible output modes +var Outputs = map[string]LogOutput{ + "default": &DefaultOutput{}, + "jsonl": &JSONLOutput{}, + "raw": &RawOutput{}, +} + +// LogOutput is the interface any output mode must implement +type LogOutput interface { + Print(ts time.Time, lbls *labels.Labels, line string) +} + +// DefaultOutput provides logs and metadata in human readable format +type DefaultOutput struct { + MaxLabelsLen int + CommonLabels labels.Labels +} + +// Print a log entry in a human readable format +func (f DefaultOutput) Print(ts time.Time, lbls *labels.Labels, line string) { + ls := subtract(*lbls, f.CommonLabels) + if len(*ignoreLabelsKey) > 0 { + ls = ls.MatchLabels(false, *ignoreLabelsKey...) + } + + labels := "" + if !*noLabels { + labels = padLabel(ls, f.MaxLabelsLen) + } + fmt.Println( + color.BlueString(ts.Format(time.RFC3339)), + color.RedString(labels), + strings.TrimSpace(line), + ) +} + +// JSONLOutput prints logs and metadata as JSON Lines, suitable for scripts +type JSONLOutput struct{} + +// Print a log entry as json line +func (f JSONLOutput) Print(ts time.Time, lbls *labels.Labels, line string) { + entry := map[string]interface{}{ + "timestamp": ts, + "labels": lbls, + "line": line, + } + out, err := json.Marshal(entry) + if err != nil { + log.Fatalf("error marshalling entry: %s", err) + } + fmt.Println(string(out)) +} + +// RawOutput prints logs in their original form, without any metadata +type RawOutput struct{} + +// Print a log entry as is +func (f RawOutput) Print(ts time.Time, lbls *labels.Labels, line string) { + fmt.Println(line) +} diff --git a/cmd/logcli/query.go b/cmd/logcli/query.go index f5b571627947..b16b0d3fda12 100644 --- a/cmd/logcli/query.go +++ b/cmd/logcli/query.go @@ -48,11 +48,11 @@ func doQuery() { common = common.MatchLabels(false, *showLabelsKey...) } - if len(common) > 0 { + if len(common) > 0 && !*quiet { log.Println("Common labels:", color.RedString(common.String())) } - if len(*ignoreLabelsKey) > 0 { + if len(*ignoreLabelsKey) > 0 && !*quiet { log.Println("Ignoring labels key:", color.RedString(strings.Join(*ignoreLabelsKey, ","))) } @@ -71,19 +71,14 @@ func doQuery() { i = iter.NewQueryResponseIterator(resp, d) + Outputs["default"] = DefaultOutput{ + MaxLabelsLen: maxLabelsLen, + CommonLabels: common, + } + for i.Next() { ls := labelsCache(i.Labels()) - ls = subtract(ls, common) - if len(*ignoreLabelsKey) > 0 { - ls = ls.MatchLabels(false, *ignoreLabelsKey...) - } - - labels := "" - if !*noLabels { - labels = padLabel(ls, maxLabelsLen) - } - - printLogEntry(i.Entry().Timestamp, labels, i.Entry().Line) + Outputs[*outputMode].Print(i.Entry().Timestamp, &ls, i.Entry().Line) } if err := i.Error(); err != nil { diff --git a/cmd/logcli/tail.go b/cmd/logcli/tail.go index 6349e3b29788..94ee75b3432e 100644 --- a/cmd/logcli/tail.go +++ b/cmd/logcli/tail.go @@ -55,9 +55,12 @@ func tailQuery() { labels = stream.Labels } } + for _, entry := range stream.Entries { - printLogEntry(entry.Timestamp, labels, entry.Line) + lbls := mustParseLabels(labels) + Outputs[*outputMode].Print(entry.Timestamp, &lbls, entry.Line) } + } if len(tailReponse.DroppedEntries) != 0 { log.Println("Server dropped following entries due to slow client") diff --git a/cmd/logcli/utils.go b/cmd/logcli/utils.go index 807c3a618487..6054f86ff6e7 100644 --- a/cmd/logcli/utils.go +++ b/cmd/logcli/utils.go @@ -1,27 +1,15 @@ package main import ( - "fmt" "log" "sort" "strings" - "time" - "github.com/fatih/color" "github.com/grafana/loki/pkg/logproto" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/promql" ) -// print a log entry -func printLogEntry(ts time.Time, lbls string, line string) { - fmt.Println( - color.BlueString(ts.Format(time.RFC3339)), - color.RedString(lbls), - strings.TrimSpace(line), - ) -} - // add some padding after labels func padLabel(ls labels.Labels, maxLabelsLen int) string { labels := ls.String() @@ -52,7 +40,7 @@ func parseLabels(resp *logproto.QueryResponse) (map[string]labels.Labels, []labe return cache, lss } -// return common labels between given lavels set +// return commonLabels labels between given lavels set func commonLabels(lss []labels.Labels) labels.Labels { if len(lss) == 0 { return nil