Skip to content
61 changes: 54 additions & 7 deletions cmd/logcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,31 @@ import (
)

var (
app = kingpin.New("logcli", "A command-line for loki.").Version(version.Print("logcli"))
quiet = app.Flag("quiet", "Suppress query metadata").Default("false").Short('q').Bool()
app = kingpin.New("logcli", "A command-line for loki.").
Version(version.Print("logcli"))
quiet = app.Flag("quiet", "Suppress query metadata").
Default("false").
Short('q').
Bool()
statistics = app.Flag("stats", "Show query statistics").Default("false").Bool()
outputMode = app.Flag("output", "Specify output mode [default, raw, jsonl]. raw suppresses log labels and timestamp.").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")
cpuProfile = app.Flag("cpuprofile", "Specify the location for writing a CPU profile.").Default("").String()
memProfile = app.Flag("memprofile", "Specify the location for writing a memory profile.").Default("").String()
stdin = app.Flag("stdin", "Take input logs from stdin").Bool()
outputMode = app.Flag("output", "Specify output mode [default, raw, jsonl]. raw suppresses log labels and timestamp.").
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")
outputTimestampFmt = app.Flag("output-timestamp-format", "Specify the format of timestamps in the default output mode [rfc3339, rfc3339nano, rfc822z, rfc1123z, stampmicro, stampmilli, stampnano,unixdate]").
Default("rfc3339").
Enum("rfc3339", "rfc3339nano", "rfc822z", "rfc1123z", "stampmicro", "stampmilli", "stampnano", "unixdate")
cpuProfile = app.Flag("cpuprofile", "Specify the location for writing a CPU profile.").
Default("").
String()
memProfile = app.Flag("memprofile", "Specify the location for writing a memory profile.").
Default("").
String()
stdin = app.Flag("stdin", "Take input logs from stdin").Bool()

queryClient = newQueryClient(app)

Expand All @@ -62,6 +79,8 @@ or provide specific start and end times with --from and --to respectively.
Notice that when using --from and --to then ensure to use RFC3339Nano
time format, but without timezone at the end. The local timezone will be added
automatically or if using --timezone flag.
In default output mode the --output-timestamp-format flag can be used to
modify the output timestamp.

Example:

Expand All @@ -72,6 +91,15 @@ Example:
--output=jsonl
'my-query'

Example with --output-timestamp-format:

logcli query
--timezone=UTC
--from="2021-01-19T10:00:00Z"
--to="2021-01-19T20:00:00Z"
--output-timestamp-format=rfc3339nano
'my-query'

The output is limited to 30 entries by default; use --limit to increase.

While "query" does support metrics queries, its output contains multiple
Expand Down Expand Up @@ -358,6 +386,25 @@ func main() {
ColoredOutput: rangeQuery.ColoredOutput,
}

switch *outputTimestampFmt {
case "rfc3339nano":
outputOptions.TimestampFormat = time.RFC3339Nano
case "rfc822z":
outputOptions.TimestampFormat = time.RFC822Z
case "rfc1123z":
outputOptions.TimestampFormat = time.RFC1123Z
case "stampmilli":
outputOptions.TimestampFormat = time.StampMilli
case "stampmicro":
outputOptions.TimestampFormat = time.StampMicro
case "stampnano":
outputOptions.TimestampFormat = time.StampNano
case "unixdate":
outputOptions.TimestampFormat = time.UnixDate
default:
outputOptions.TimestampFormat = time.RFC3339
}

out, err := output.NewLogOutput(os.Stdout, *outputMode, outputOptions)
if err != nil {
log.Fatalf("Unable to create log output: %s", err)
Expand Down
7 changes: 6 additions & 1 deletion pkg/logcli/output/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ type DefaultOutput struct {

// Format a log entry in a human readable format
func (o *DefaultOutput) FormatAndPrintln(ts time.Time, lbls loghttp.LabelSet, maxLabelsLen int, line string) {
timestamp := ts.In(o.options.Timezone).Format(time.RFC3339)
format := o.options.TimestampFormat
if format == "" {
format = time.RFC3339
}

timestamp := ts.In(o.options.Timezone).Format(format)
line = strings.TrimSpace(line)

if o.options.NoLabels {
Expand Down
18 changes: 17 additions & 1 deletion pkg/logcli/output/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func TestDefaultOutput_Format(t *testing.T) {
t.Parallel()

timestamp, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
timestamp, _ := time.Parse(time.RFC3339Nano, "2006-01-02T15:04:05.123456789+07:00")
emptyLabels := loghttp.LabelSet{}
someLabels := loghttp.LabelSet(map[string]string{
"type": "test",
Expand Down Expand Up @@ -76,6 +76,22 @@ func TestDefaultOutput_Format(t *testing.T) {
"Hello",
"2006-01-02T08:04:05Z Hello\n",
},
"TimestampFormat using RFC3339Nano": {
&LogOutputOptions{Timezone: time.UTC, TimestampFormat: time.RFC3339Nano},
timestamp,
emptyLabels,
0,
"Hello",
"2006-01-02T08:04:05.123456789Z {} Hello\n",
},
"TimestampFormat using StampMicro truncated": {
&LogOutputOptions{Timezone: time.UTC, TimestampFormat: time.StampMicro},
timestamp,
emptyLabels,
0,
"Hello",
"Jan 2 08:04:05.123456 {} Hello\n",
},
}

for testName, testData := range tests {
Expand Down
12 changes: 9 additions & 3 deletions pkg/logcli/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,23 @@ type LogOutput interface {
}

// LogOutputOptions defines options supported by LogOutput
// TimestampFormat can be one of RFC3339 (default), RFC3339Nano, RFC822Z, RFC1123Z, StampMilli, StampMicro, StampNano, UnixDate
type LogOutputOptions struct {
Timezone *time.Location
NoLabels bool
ColoredOutput bool
Timezone *time.Location
NoLabels bool
ColoredOutput bool
TimestampFormat string
}

// NewLogOutput creates a log output based on the input mode and options
func NewLogOutput(w io.Writer, mode string, options *LogOutputOptions) (LogOutput, error) {
if options.Timezone == nil {
options.Timezone = time.Local
}
// in case TimestampFormat is not set, default to RFC3339
if options.TimestampFormat == "" {
options.TimestampFormat = time.RFC3339
}

switch mode {
case "default":
Expand Down
3 changes: 2 additions & 1 deletion pkg/logcli/output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import (
)

func TestNewLogOutput(t *testing.T) {
options := &LogOutputOptions{time.UTC, false, false}
options := &LogOutputOptions{Timezone: time.UTC, NoLabels: false, ColoredOutput: false}

out, err := NewLogOutput(nil, "default", options)
assert.NoError(t, err)
assert.IsType(t, &DefaultOutput{nil, options}, out)
assert.Equal(t, time.RFC3339, out.(*DefaultOutput).options.TimestampFormat)

out, err = NewLogOutput(nil, "jsonl", options)
assert.NoError(t, err)
Expand Down