Skip to content

Commit

Permalink
feat(logcli): output modes (#731)
Browse files Browse the repository at this point in the history
* feat(logcli): quiet mode

Adds a quiet mode (-q / --quiet) to suppress the debug messages to stderr

* feat(logcli): output modes

Adds two alternative output modes (-o / --output)

- raw: emits the line as parsed
- jsonl: emits the line plus all known metadata as JSONL (JSON Line)

Usage: -o [default, raw, jsonl]

* feat(logcli): quiet tailing mode

* feat(logcli): output modes while tailing

Supports the three different output modes in tail mode as well

* feat(logcli): print labels in jsonl tail mode

* refactor(logcli): clean up entry printing

Moves the entry printing into a standardized interface, implements this three
times:

- default (human readable)
- jsonl (for scripts)
- raw ('as is')
  • Loading branch information
sh0rez authored and cyriltovena committed Jul 15, 2019
1 parent 94c252b commit 9d5630d
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 30 deletions.
8 changes: 6 additions & 2 deletions cmd/logcli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))}}

Expand Down
4 changes: 3 additions & 1 deletion cmd/logcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
73 changes: 73 additions & 0 deletions cmd/logcli/output.go
Original file line number Diff line number Diff line change
@@ -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)
}
21 changes: 8 additions & 13 deletions cmd/logcli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,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, ",")))
}

Expand All @@ -79,19 +79,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 {
Expand Down
5 changes: 4 additions & 1 deletion cmd/logcli/tail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
14 changes: 1 addition & 13 deletions cmd/logcli/utils.go
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9d5630d

Please sign in to comment.