Skip to content

Commit 984a4b3

Browse files
authored
feat: add time formatting configuration (#109)
1 parent 2e2585a commit 984a4b3

File tree

6 files changed

+120
-27
lines changed

6 files changed

+120
-27
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ Example configuration: [example.jlv.jsonc](example.jlv.jsonc).
134134

135135
### Time Formats
136136
JSON Log Viewer can handle a variety of datetime formats when parsing your logs.
137+
The value is formatted by default in the "[RFC3339](https://www.rfc-editor.org/rfc/rfc3339)" format. The format is configurable, see the `time_format` field in the [config](example.jlv.jsonc).
137138

138139
#### `time`
139140
This will return the exact value that was set in the JSON document.

example.jlv.jsonc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,19 @@
2020
"$.t",
2121
"$.ts"
2222
],
23-
"width": 30
23+
"width": 30,
24+
// Year: "2006" "06"
25+
// Month: "Jan" "January" "01" "1"
26+
// Day of the week: "Mon" "Monday"
27+
// Day of the month: "2" "_2" "02"
28+
// Day of the year: "__2" "002"
29+
// Hour: "15" "3" "03" (PM or AM)
30+
// Minute: "4" "04"
31+
// Second: "5" "05"
32+
// AM/PM mark: "PM"
33+
//
34+
// More details: https://go.dev/src/time/format.go.
35+
"time_format": "2006-01-02T15:04:05Z07:00"
2436
},
2537
{
2638
"title": "Level",

internal/pkg/config/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import (
66
"fmt"
77
"os"
88
"strconv"
9+
"time"
910

1011
units "github.com/docker/go-units"
1112
"github.com/go-playground/validator/v10"
1213
"github.com/hedhyw/jsoncjson"
1314
)
1415

16+
// DefaultTimeFormat is a time format used in formatting timestamps by default.
17+
const DefaultTimeFormat = time.RFC3339
18+
1519
// PathDefault is a fake path to the default config.
1620
const PathDefault = "default"
1721

@@ -49,10 +53,13 @@ type Field struct {
4953
Kind FieldKind `json:"kind" validate:"required,oneof=time message numerictime secondtime millitime microtime level any"`
5054
References []string `json:"ref" validate:"min=1,dive,required"`
5155
Width int `json:"width" validate:"min=0"`
56+
TimeFormat *string `json:"time_format,omitempty"`
5257
}
5358

5459
// GetDefaultConfig returns the configuration with default values.
5560
func GetDefaultConfig() *Config {
61+
defaultTimeFormat := DefaultTimeFormat
62+
5663
// nolint: mnd // Default config.
5764
return &Config{
5865
Path: "default",
@@ -63,6 +70,7 @@ func GetDefaultConfig() *Config {
6370
Kind: FieldKindNumericTime,
6471
References: []string{"$.timestamp", "$.time", "$.t", "$.ts"},
6572
Width: 30,
73+
TimeFormat: &defaultTimeFormat,
6674
}, {
6775
Title: "Level",
6876
Kind: FieldKindLevel,

internal/pkg/config/config_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ func ExampleGetDefaultConfig() {
112112
// "$.t",
113113
// "$.ts"
114114
// ],
115-
// "width": 30
115+
// "width": 30,
116+
// "time_format": "2006-01-02T15:04:05Z07:00"
116117
// },
117118
// {
118119
// "title": "Level",

internal/pkg/source/entry.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import (
1616
"github.com/hedhyw/json-log-viewer/internal/pkg/config"
1717
)
1818

19+
const (
20+
unitSeconds = "s"
21+
unitMilli = "ms"
22+
unitMicro = "us"
23+
)
24+
1925
// LazyLogEntry holds unredenred LogEntry. Use `LogEntry` getter.
2026
type LazyLogEntry struct {
2127
offset int64
@@ -129,7 +135,7 @@ func parseField(
129135
unquotedField = string(jsonField)
130136
}
131137

132-
return formatField(unquotedField, field.Kind, cfg)
138+
return formatField(unquotedField, field, cfg)
133139
}
134140

135141
return "-"
@@ -138,11 +144,18 @@ func parseField(
138144
//nolint:cyclop // The cyclomatic complexity here is so high because of the number of FieldKinds.
139145
func formatField(
140146
value string,
141-
kind config.FieldKind,
147+
field config.Field,
142148
cfg *config.Config,
143149
) string {
150+
kind := field.Kind
144151
value = strings.TrimSpace(value)
145152

153+
timeFormat := config.DefaultTimeFormat
154+
155+
if field.TimeFormat != nil {
156+
timeFormat = *field.TimeFormat
157+
}
158+
146159
// Numeric time attempts to infer the duration based on the length of the string
147160
if kind == config.FieldKindNumericTime {
148161
kind = guessTimeFieldKind(value)
@@ -156,11 +169,11 @@ func formatField(
156169
case config.FieldKindTime:
157170
return formatMessage(value)
158171
case config.FieldKindSecondTime:
159-
return formatMessage(formatTimeString(value, "s"))
172+
return formatMessage(formatTimeValue(value, unitSeconds, timeFormat))
160173
case config.FieldKindMilliTime:
161-
return formatMessage(formatTimeString(value, "ms"))
174+
return formatMessage(formatTimeValue(value, unitMilli, timeFormat))
162175
case config.FieldKindMicroTime:
163-
return formatMessage(formatTimeString(value, "us"))
176+
return formatMessage(formatTimeValue(value, unitMicro, timeFormat))
164177
case config.FieldKindAny:
165178
return formatMessage(value)
166179
default:
@@ -262,11 +275,11 @@ func guessTimeFieldKind(timeStr string) config.FieldKind {
262275
}
263276
}
264277

265-
func formatTimeString(timeStr string, unit string) string {
266-
duration, err := time.ParseDuration(timeStr + unit)
278+
func formatTimeValue(timeValue string, unit string, format string) string {
279+
duration, err := time.ParseDuration(timeValue + unit)
267280
if err != nil {
268-
return timeStr
281+
return timeValue
269282
}
270283

271-
return time.UnixMilli(0).Add(duration).UTC().Format(time.RFC3339)
284+
return time.UnixMilli(0).Add(duration).UTC().Format(format)
272285
}

internal/pkg/source/entry_test.go

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,9 @@ func TestLazyLogEntriesFilter(t *testing.T) {
339339
func TestSecondTimeFormatting(t *testing.T) {
340340
t.Parallel()
341341

342-
cfg := getTimestampFormattingConfig(config.FieldKindSecondTime)
343-
344342
expectedOutput := time.Unix(1, 0).UTC().Format(time.RFC3339)
345343

346-
secondsTestCases := [...]TimeFormattingTestCase{{
344+
secondsTestCases := [...]timeFormattingTestCase{{
347345
TestName: "Seconds (float)",
348346
JSON: `{"timestamp":1.0}`,
349347
ExpectedOutput: expectedOutput,
@@ -369,6 +367,8 @@ func TestSecondTimeFormatting(t *testing.T) {
369367
t.Run(testCase.TestName, func(t *testing.T) {
370368
t.Parallel()
371369

370+
cfg := getTimestampFormattingConfig(config.FieldKindSecondTime, testCase.Format)
371+
372372
actual := parseTableRow(t, testCase.JSON, cfg)
373373
assert.Equal(t, testCase.ExpectedOutput, actual[0])
374374
})
@@ -378,11 +378,9 @@ func TestSecondTimeFormatting(t *testing.T) {
378378
func TestMillisecondTimeFormatting(t *testing.T) {
379379
t.Parallel()
380380

381-
cfg := getTimestampFormattingConfig(config.FieldKindMilliTime)
382-
383381
expectedOutput := time.Unix(2, 0).UTC().Format(time.RFC3339)
384382

385-
millisecondTestCases := [...]TimeFormattingTestCase{{
383+
millisecondTestCases := [...]timeFormattingTestCase{{
386384
TestName: "Milliseconds (float)",
387385
JSON: `{"timestamp":2000.0}`,
388386
ExpectedOutput: expectedOutput,
@@ -404,6 +402,8 @@ func TestMillisecondTimeFormatting(t *testing.T) {
404402
t.Run(testCase.TestName, func(t *testing.T) {
405403
t.Parallel()
406404

405+
cfg := getTimestampFormattingConfig(config.FieldKindMilliTime, testCase.Format)
406+
407407
actual := parseTableRow(t, testCase.JSON, cfg)
408408
assert.Equal(t, testCase.ExpectedOutput, actual[0])
409409
})
@@ -413,11 +413,9 @@ func TestMillisecondTimeFormatting(t *testing.T) {
413413
func TestMicrosecondTimeFormatting(t *testing.T) {
414414
t.Parallel()
415415

416-
cfg := getTimestampFormattingConfig(config.FieldKindMicroTime)
417-
418416
expectedOutput := time.Unix(4, 0).UTC().Format(time.RFC3339)
419417

420-
microsecondTestCases := [...]TimeFormattingTestCase{{
418+
microsecondTestCases := [...]timeFormattingTestCase{{
421419
TestName: "Microseconds (float)",
422420
JSON: `{"timestamp":4000000.0}`,
423421
ExpectedOutput: expectedOutput,
@@ -439,6 +437,8 @@ func TestMicrosecondTimeFormatting(t *testing.T) {
439437
t.Run(testCase.TestName, func(t *testing.T) {
440438
t.Parallel()
441439

440+
cfg := getTimestampFormattingConfig(config.FieldKindMicroTime, testCase.Format)
441+
442442
actual := parseTableRow(t, testCase.JSON, cfg)
443443
assert.Equal(t, testCase.ExpectedOutput, actual[0])
444444
})
@@ -448,7 +448,7 @@ func TestMicrosecondTimeFormatting(t *testing.T) {
448448
func TestFormattingUnknown(t *testing.T) {
449449
t.Parallel()
450450

451-
cfg := getTimestampFormattingConfig(config.FieldKind("unknown"))
451+
cfg := getTimestampFormattingConfig(config.FieldKind("unknown"), config.DefaultTimeFormat)
452452

453453
actual := parseTableRow(t, `{"timestamp": 1}`, cfg)
454454
assert.Equal(t, "1", actual[0])
@@ -457,7 +457,7 @@ func TestFormattingUnknown(t *testing.T) {
457457
func TestFormattingAny(t *testing.T) {
458458
t.Parallel()
459459

460-
cfg := getTimestampFormattingConfig(config.FieldKindAny)
460+
cfg := getTimestampFormattingConfig(config.FieldKindAny, config.DefaultTimeFormat)
461461

462462
actual := parseTableRow(t, `{"timestamp": 1}`, cfg)
463463
assert.Equal(t, "1", actual[0])
@@ -466,9 +466,7 @@ func TestFormattingAny(t *testing.T) {
466466
func TestNumericKindTimeFormatting(t *testing.T) {
467467
t.Parallel()
468468

469-
cfg := getTimestampFormattingConfig(config.FieldKindNumericTime)
470-
471-
numericKindCases := [...]TimeFormattingTestCase{{
469+
numericKindCases := [...]timeFormattingTestCase{{
472470
TestName: "Date passthru",
473471
JSON: `{"timestamp":"2023-10-08 20:00:00"}`,
474472
ExpectedOutput: "2023-10-08 20:00:00",
@@ -522,6 +520,8 @@ func TestNumericKindTimeFormatting(t *testing.T) {
522520
t.Run(testCase.TestName, func(t *testing.T) {
523521
t.Parallel()
524522

523+
cfg := getTimestampFormattingConfig(config.FieldKindNumericTime, testCase.Format)
524+
525525
actual := parseTableRow(t, testCase.JSON, cfg)
526526
assert.Equal(t, testCase.ExpectedOutput, actual[0])
527527
})
@@ -613,6 +613,56 @@ func TestLazyLogEntryLogEntry(t *testing.T) {
613613
})
614614
}
615615

616+
func TestTimeFormat(t *testing.T) {
617+
t.Parallel()
618+
619+
logDate := time.Date(
620+
2000, // Year.
621+
time.January,
622+
2, // Day.
623+
3, // Hour.
624+
4, // Minutes.
625+
5, // Seconds.
626+
0, // Nanoseconds.
627+
time.UTC,
628+
)
629+
630+
jsonContent := fmt.Sprintf(`{"timestamp":"%d"}`, logDate.Unix())
631+
632+
numericKindCases := [...]timeFormattingTestCase{{
633+
TestName: "RFC3339",
634+
JSON: jsonContent,
635+
ExpectedOutput: logDate.Format(time.RFC3339),
636+
Format: time.RFC3339,
637+
}, {
638+
TestName: "RFC1123",
639+
JSON: jsonContent,
640+
ExpectedOutput: logDate.Format(time.RFC1123),
641+
Format: time.RFC1123,
642+
}, {
643+
TestName: "TimeOnly",
644+
JSON: jsonContent,
645+
ExpectedOutput: logDate.Format(time.TimeOnly),
646+
Format: time.TimeOnly,
647+
}, {
648+
TestName: "TimeOnly",
649+
JSON: jsonContent,
650+
ExpectedOutput: "invalid",
651+
Format: "invalid",
652+
}}
653+
654+
for _, testCase := range numericKindCases {
655+
t.Run(testCase.TestName, func(t *testing.T) {
656+
t.Parallel()
657+
658+
cfg := getTimestampFormattingConfig(config.FieldKindSecondTime, testCase.Format)
659+
660+
actual := parseTableRow(t, testCase.JSON, cfg)
661+
assert.Equal(t, testCase.ExpectedOutput, actual[0])
662+
})
663+
}
664+
}
665+
616666
func parseLazyLogEntry(tb testing.TB, value string, cfg *config.Config) source.LazyLogEntry {
617667
tb.Helper()
618668

@@ -653,20 +703,28 @@ func getFieldKindToValue(cfg *config.Config, entries []string) map[config.FieldK
653703
return fieldKindToValue
654704
}
655705

656-
type TimeFormattingTestCase struct {
706+
type timeFormattingTestCase struct {
657707
TestName string
658708
JSON string
659709
ExpectedOutput string
710+
Format string
660711
}
661712

662-
func getTimestampFormattingConfig(fieldKind config.FieldKind) *config.Config {
713+
func getTimestampFormattingConfig(fieldKind config.FieldKind, format string) *config.Config {
663714
cfg := config.GetDefaultConfig()
664715

716+
var timeFormat *string
717+
718+
if format != "" {
719+
timeFormat = &format
720+
}
721+
665722
cfg.Fields = []config.Field{{
666723
Title: "Time",
667724
Kind: fieldKind,
668725
References: []string{"$.timestamp", "$.time", "$.t", "$.ts"},
669726
Width: 30,
727+
TimeFormat: timeFormat,
670728
}}
671729

672730
return cfg

0 commit comments

Comments
 (0)