Skip to content

Commit

Permalink
feat: add time formatting configuration (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
hedhyw authored Oct 27, 2024
1 parent 2e2585a commit 984a4b3
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 27 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Example configuration: [example.jlv.jsonc](example.jlv.jsonc).

### Time Formats
JSON Log Viewer can handle a variety of datetime formats when parsing your logs.
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).

#### `time`
This will return the exact value that was set in the JSON document.
Expand Down
14 changes: 13 additions & 1 deletion example.jlv.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,19 @@
"$.t",
"$.ts"
],
"width": 30
"width": 30,
// Year: "2006" "06"
// Month: "Jan" "January" "01" "1"
// Day of the week: "Mon" "Monday"
// Day of the month: "2" "_2" "02"
// Day of the year: "__2" "002"
// Hour: "15" "3" "03" (PM or AM)
// Minute: "4" "04"
// Second: "5" "05"
// AM/PM mark: "PM"
//
// More details: https://go.dev/src/time/format.go.
"time_format": "2006-01-02T15:04:05Z07:00"
},
{
"title": "Level",
Expand Down
8 changes: 8 additions & 0 deletions internal/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import (
"fmt"
"os"
"strconv"
"time"

units "github.com/docker/go-units"
"github.com/go-playground/validator/v10"
"github.com/hedhyw/jsoncjson"
)

// DefaultTimeFormat is a time format used in formatting timestamps by default.
const DefaultTimeFormat = time.RFC3339

// PathDefault is a fake path to the default config.
const PathDefault = "default"

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

// GetDefaultConfig returns the configuration with default values.
func GetDefaultConfig() *Config {
defaultTimeFormat := DefaultTimeFormat

// nolint: mnd // Default config.
return &Config{
Path: "default",
Expand All @@ -63,6 +70,7 @@ func GetDefaultConfig() *Config {
Kind: FieldKindNumericTime,
References: []string{"$.timestamp", "$.time", "$.t", "$.ts"},
Width: 30,
TimeFormat: &defaultTimeFormat,
}, {
Title: "Level",
Kind: FieldKindLevel,
Expand Down
3 changes: 2 additions & 1 deletion internal/pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ func ExampleGetDefaultConfig() {
// "$.t",
// "$.ts"
// ],
// "width": 30
// "width": 30,
// "time_format": "2006-01-02T15:04:05Z07:00"
// },
// {
// "title": "Level",
Expand Down
31 changes: 22 additions & 9 deletions internal/pkg/source/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (
"github.com/hedhyw/json-log-viewer/internal/pkg/config"
)

const (
unitSeconds = "s"
unitMilli = "ms"
unitMicro = "us"
)

// LazyLogEntry holds unredenred LogEntry. Use `LogEntry` getter.
type LazyLogEntry struct {
offset int64
Expand Down Expand Up @@ -129,7 +135,7 @@ func parseField(
unquotedField = string(jsonField)
}

return formatField(unquotedField, field.Kind, cfg)
return formatField(unquotedField, field, cfg)
}

return "-"
Expand All @@ -138,11 +144,18 @@ func parseField(
//nolint:cyclop // The cyclomatic complexity here is so high because of the number of FieldKinds.
func formatField(
value string,
kind config.FieldKind,
field config.Field,
cfg *config.Config,
) string {
kind := field.Kind
value = strings.TrimSpace(value)

timeFormat := config.DefaultTimeFormat

if field.TimeFormat != nil {
timeFormat = *field.TimeFormat
}

// Numeric time attempts to infer the duration based on the length of the string
if kind == config.FieldKindNumericTime {
kind = guessTimeFieldKind(value)
Expand All @@ -156,11 +169,11 @@ func formatField(
case config.FieldKindTime:
return formatMessage(value)
case config.FieldKindSecondTime:
return formatMessage(formatTimeString(value, "s"))
return formatMessage(formatTimeValue(value, unitSeconds, timeFormat))
case config.FieldKindMilliTime:
return formatMessage(formatTimeString(value, "ms"))
return formatMessage(formatTimeValue(value, unitMilli, timeFormat))
case config.FieldKindMicroTime:
return formatMessage(formatTimeString(value, "us"))
return formatMessage(formatTimeValue(value, unitMicro, timeFormat))
case config.FieldKindAny:
return formatMessage(value)
default:
Expand Down Expand Up @@ -262,11 +275,11 @@ func guessTimeFieldKind(timeStr string) config.FieldKind {
}
}

func formatTimeString(timeStr string, unit string) string {
duration, err := time.ParseDuration(timeStr + unit)
func formatTimeValue(timeValue string, unit string, format string) string {
duration, err := time.ParseDuration(timeValue + unit)
if err != nil {
return timeStr
return timeValue
}

return time.UnixMilli(0).Add(duration).UTC().Format(time.RFC3339)
return time.UnixMilli(0).Add(duration).UTC().Format(format)
}
90 changes: 74 additions & 16 deletions internal/pkg/source/entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,9 @@ func TestLazyLogEntriesFilter(t *testing.T) {
func TestSecondTimeFormatting(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKindSecondTime)

expectedOutput := time.Unix(1, 0).UTC().Format(time.RFC3339)

secondsTestCases := [...]TimeFormattingTestCase{{
secondsTestCases := [...]timeFormattingTestCase{{
TestName: "Seconds (float)",
JSON: `{"timestamp":1.0}`,
ExpectedOutput: expectedOutput,
Expand All @@ -369,6 +367,8 @@ func TestSecondTimeFormatting(t *testing.T) {
t.Run(testCase.TestName, func(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKindSecondTime, testCase.Format)

actual := parseTableRow(t, testCase.JSON, cfg)
assert.Equal(t, testCase.ExpectedOutput, actual[0])
})
Expand All @@ -378,11 +378,9 @@ func TestSecondTimeFormatting(t *testing.T) {
func TestMillisecondTimeFormatting(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKindMilliTime)

expectedOutput := time.Unix(2, 0).UTC().Format(time.RFC3339)

millisecondTestCases := [...]TimeFormattingTestCase{{
millisecondTestCases := [...]timeFormattingTestCase{{
TestName: "Milliseconds (float)",
JSON: `{"timestamp":2000.0}`,
ExpectedOutput: expectedOutput,
Expand All @@ -404,6 +402,8 @@ func TestMillisecondTimeFormatting(t *testing.T) {
t.Run(testCase.TestName, func(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKindMilliTime, testCase.Format)

actual := parseTableRow(t, testCase.JSON, cfg)
assert.Equal(t, testCase.ExpectedOutput, actual[0])
})
Expand All @@ -413,11 +413,9 @@ func TestMillisecondTimeFormatting(t *testing.T) {
func TestMicrosecondTimeFormatting(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKindMicroTime)

expectedOutput := time.Unix(4, 0).UTC().Format(time.RFC3339)

microsecondTestCases := [...]TimeFormattingTestCase{{
microsecondTestCases := [...]timeFormattingTestCase{{
TestName: "Microseconds (float)",
JSON: `{"timestamp":4000000.0}`,
ExpectedOutput: expectedOutput,
Expand All @@ -439,6 +437,8 @@ func TestMicrosecondTimeFormatting(t *testing.T) {
t.Run(testCase.TestName, func(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKindMicroTime, testCase.Format)

actual := parseTableRow(t, testCase.JSON, cfg)
assert.Equal(t, testCase.ExpectedOutput, actual[0])
})
Expand All @@ -448,7 +448,7 @@ func TestMicrosecondTimeFormatting(t *testing.T) {
func TestFormattingUnknown(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKind("unknown"))
cfg := getTimestampFormattingConfig(config.FieldKind("unknown"), config.DefaultTimeFormat)

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

cfg := getTimestampFormattingConfig(config.FieldKindAny)
cfg := getTimestampFormattingConfig(config.FieldKindAny, config.DefaultTimeFormat)

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

cfg := getTimestampFormattingConfig(config.FieldKindNumericTime)

numericKindCases := [...]TimeFormattingTestCase{{
numericKindCases := [...]timeFormattingTestCase{{
TestName: "Date passthru",
JSON: `{"timestamp":"2023-10-08 20:00:00"}`,
ExpectedOutput: "2023-10-08 20:00:00",
Expand Down Expand Up @@ -522,6 +520,8 @@ func TestNumericKindTimeFormatting(t *testing.T) {
t.Run(testCase.TestName, func(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKindNumericTime, testCase.Format)

actual := parseTableRow(t, testCase.JSON, cfg)
assert.Equal(t, testCase.ExpectedOutput, actual[0])
})
Expand Down Expand Up @@ -613,6 +613,56 @@ func TestLazyLogEntryLogEntry(t *testing.T) {
})
}

func TestTimeFormat(t *testing.T) {
t.Parallel()

logDate := time.Date(
2000, // Year.
time.January,
2, // Day.
3, // Hour.
4, // Minutes.
5, // Seconds.
0, // Nanoseconds.
time.UTC,
)

jsonContent := fmt.Sprintf(`{"timestamp":"%d"}`, logDate.Unix())

numericKindCases := [...]timeFormattingTestCase{{
TestName: "RFC3339",
JSON: jsonContent,
ExpectedOutput: logDate.Format(time.RFC3339),
Format: time.RFC3339,
}, {
TestName: "RFC1123",
JSON: jsonContent,
ExpectedOutput: logDate.Format(time.RFC1123),
Format: time.RFC1123,
}, {
TestName: "TimeOnly",
JSON: jsonContent,
ExpectedOutput: logDate.Format(time.TimeOnly),
Format: time.TimeOnly,
}, {
TestName: "TimeOnly",
JSON: jsonContent,
ExpectedOutput: "invalid",
Format: "invalid",
}}

for _, testCase := range numericKindCases {
t.Run(testCase.TestName, func(t *testing.T) {
t.Parallel()

cfg := getTimestampFormattingConfig(config.FieldKindSecondTime, testCase.Format)

actual := parseTableRow(t, testCase.JSON, cfg)
assert.Equal(t, testCase.ExpectedOutput, actual[0])
})
}
}

func parseLazyLogEntry(tb testing.TB, value string, cfg *config.Config) source.LazyLogEntry {
tb.Helper()

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

type TimeFormattingTestCase struct {
type timeFormattingTestCase struct {
TestName string
JSON string
ExpectedOutput string
Format string
}

func getTimestampFormattingConfig(fieldKind config.FieldKind) *config.Config {
func getTimestampFormattingConfig(fieldKind config.FieldKind, format string) *config.Config {
cfg := config.GetDefaultConfig()

var timeFormat *string

if format != "" {
timeFormat = &format
}

cfg.Fields = []config.Field{{
Title: "Time",
Kind: fieldKind,
References: []string{"$.timestamp", "$.time", "$.t", "$.ts"},
Width: 30,
TimeFormat: timeFormat,
}}

return cfg
Expand Down

0 comments on commit 984a4b3

Please sign in to comment.