diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d8f9a73..6ed9755 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20.6' + go-version: '1.21.3' id: go - name: Check out code into the Go module directory diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29be0cf..c7078c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20.4' + go-version: '1.21.3' - name: Fetch vendor run: make vendor diff --git a/.gitignore b/.gitignore index 75aa064..ab24f0c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ bin # Dependency directories (remove the comment below to include it) vendor/ + +# Config +.jlv.jsonc diff --git a/.golangci.json b/.golangci.json index 4b0e305..de917df 100644 --- a/.golangci.json +++ b/.golangci.json @@ -33,7 +33,9 @@ "rowserrcheck", "depguard", "ireturn", - "gomoddirectives" + "gomoddirectives", + "tagalign", + "testifylint" ] }, "linters-settings": { diff --git a/Makefile b/Makefile index a084c33..20500cd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -GOLANG_CI_LINT_VER:=v1.53.3 +GOLANG_CI_LINT_VER:=v1.55.2 OUT_BIN?=${PWD}/bin/jlv COVER_PACKAGES=./... VERSION?=${shell git describe --tags} diff --git a/README.md b/README.md index 4438530..2d95966 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ The application is designed to help in visualization, navigation, and analyzing - [Package](#package) - [Standalone Binary](#standalone-binary) - [Source](#source) + - [Customization](#customization) - [Resources](#resources) - [Contribution](#contribution) - [License](#license) @@ -89,6 +90,69 @@ chmod +x /usr/local/bin/jlv # jlv application.log ``` +## Customization + +The application will look for the config `.jlv.jsonc` in the working directory or in the home directory: +- `$PWD/.jlv.jsonc`; +- `$HOME/.jlv.jsonc`. + +The Json path supports the described in [yalp/jsonpath](https://github.com/yalp/jsonpath#jsonpath-quick-intro) syntax. + +Example configuration: +```json +{ + // Comments are allowed. + "fields": [ + { + "title": "Time", // Max length is 32. + // Kind affects rendering. There are: + // * time; + // * level; + // * message; + // * any. + "kind": "time", + "ref": [ + // The application will display the first matched value. + "$.timestamp", + "$.time", + "$.t", + "$.ts" + ], + "width": 30 + }, + { + "title": "Level", + "kind": "level", + "ref": [ + "$.level", + "$.lvl", + "$.l" + ], + "width": 10 + }, + { + "title": "Message", + "kind": "message", + "ref": [ + "$.message", + "$.msg", + "$.error", + "$.err" + ], + "width": 0 // The width will be calculated automatically. + }, + { + "title": "Custom", + "kind": "any", + "ref": [ + "$.custom" + ], + "width": 0 + } + ] +} +``` + ## Resources Alternatives: diff --git a/cmd/jlv/main.go b/cmd/jlv/main.go index 869dc40..9d0b9b3 100644 --- a/cmd/jlv/main.go +++ b/cmd/jlv/main.go @@ -3,18 +3,27 @@ package main import ( "fmt" "os" + "path" tea "github.com/charmbracelet/bubbletea" "github.com/hedhyw/json-log-viewer/internal/app" + "github.com/hedhyw/json-log-viewer/internal/pkg/config" ) +const configFileName = ".jlv.jsonc" + func main() { if len(os.Args) != 2 { fatalf("Invalid arguments, usage: %s file.log\n", os.Args[0]) } - appModel := app.NewModel(os.Args[1]) + cfg, err := readConfig() + if err != nil { + fatalf("Error reading config: %s\n", err) + } + + appModel := app.NewModel(os.Args[1], cfg) program := tea.NewProgram(appModel, tea.WithAltScreen()) if _, err := program.Run(); err != nil { @@ -26,3 +35,21 @@ func fatalf(message string, args ...any) { fmt.Fprintf(os.Stderr, message, args...) os.Exit(1) } + +// readConfig tries to read config from working directory or home directory. +// If configs are not found, then it returns a default configuration. +func readConfig() (*config.Config, error) { + paths := []string{} + + workDir, err := os.Getwd() + if err == nil { + paths = append(paths, path.Join(workDir, configFileName)) + } + + homeDir, err := os.UserHomeDir() + if err != nil { + paths = append(paths, path.Join(homeDir, configFileName)) + } + + return config.Read(paths...) +} diff --git a/go.mod b/go.mod index 4c9d23c..17febe9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hedhyw/json-log-viewer -go 1.20 +go 1.21 replace github.com/antonmedv/fx => github.com/hedhyw/fx v0.0.1 @@ -11,9 +11,11 @@ require ( github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 github.com/charmbracelet/lipgloss v0.7.1 + github.com/go-playground/validator/v10 v10.16.0 + github.com/hedhyw/jsoncjson v1.1.0 github.com/muesli/reflow v0.3.0 github.com/stretchr/testify v1.8.4 - github.com/valyala/fastjson v1.6.4 + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 ) require ( @@ -21,7 +23,11 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/kr/pretty v0.3.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -33,6 +39,8 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/term v0.10.0 // indirect diff --git a/go.sum b/go.sum index f8766e6..208e6f4 100644 --- a/go.sum +++ b/go.sum @@ -9,12 +9,25 @@ github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNW github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/hedhyw/bubbles v0.0.2 h1:OqPNGSunmk2B8wE4RcZ4oQvhWkCZNYt8EY8UXuuXJuw= github.com/hedhyw/bubbles v0.0.2/go.mod h1:XUdibuVUiMfcfKTRla58bmY3TWsdjgF+Rp8pvimQLck= github.com/hedhyw/fx v0.0.1 h1:h1jJaDnJ6qewSKiD7yooAGZjQwS+yazFcoRgtWV0Rq8= github.com/hedhyw/fx v0.0.1/go.mod h1:mT/W/Ln5xzLNEh+wGWAsPITPpQV5w6ne7klykEUS78w= +github.com/hedhyw/jsoncjson v1.1.0 h1:uw/aqmbSXAQNJHDPLb+DpwlPNzMREGIsrs+TIwPk+f0= +github.com/hedhyw/jsoncjson v1.1.0/go.mod h1:++nXlbEXzRMcqkoDLvH5I/z5qBkacAWSZDt1u6osUPc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -23,6 +36,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= @@ -53,10 +68,20 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -72,5 +97,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/app.go b/internal/app/app.go index 9690490..517d665 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -6,11 +6,14 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/hedhyw/json-log-viewer/internal/pkg/config" "github.com/hedhyw/json-log-viewer/internal/pkg/source" ) // Model of the application. type Model struct { + config *config.Config + baseStyle lipgloss.Style footerStyle lipgloss.Style @@ -32,9 +35,9 @@ type Model struct { // NewModel initializes a new application model. It accept the path // to the file with logs. -func NewModel(path string) Model { +func NewModel(path string, cfg *config.Config) Model { tableLogs := table.New( - table.WithColumns(getColumns(100)), + table.WithColumns(getColumns(100, cfg)), table.WithFocused(true), table.WithHeight(7), ) @@ -42,6 +45,8 @@ func NewModel(path string) Model { tableLogs.SetStyles(getTableStyles()) return Model{ + config: cfg, + baseStyle: getBaseStyle(), footerStyle: getFooterStyle(), @@ -62,7 +67,7 @@ func NewModel(path string) Model { // Init implements team.Model interface. func (m Model) Init() tea.Cmd { - return source.LoadLogsFromFile(m.fileLogPath) + return source.LoadLogsFromFile(m.fileLogPath, m.config) } // Update implements team.Model interface. diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 9763e76..c38af8b 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -13,6 +13,7 @@ import ( "github.com/hedhyw/json-log-viewer/assets" "github.com/hedhyw/json-log-viewer/internal/app" + "github.com/hedhyw/json-log-viewer/internal/pkg/config" "github.com/hedhyw/json-log-viewer/internal/pkg/tests" ) @@ -220,7 +221,7 @@ func newTestModel(tb testing.TB, content []byte) app.Model { testFile := tests.RequireCreateFile(tb, content) - appModel := app.NewModel(testFile) + appModel := app.NewModel(testFile, config.GetDefaultConfig()) cmd := appModel.Init() appModel, _ = toAppModel(appModel.Update(cmd())) diff --git a/internal/app/handler.go b/internal/app/handler.go index 48afc6b..df3168f 100644 --- a/internal/app/handler.go +++ b/internal/app/handler.go @@ -61,7 +61,7 @@ func (m Model) handleWindowSizeMsg(msg tea.WindowSizeMsg) Model { x, y := m.baseStyle.GetFrameSize() m.table.SetWidth(msg.Width - x*2) m.table.SetHeight(msg.Height - y*2 - footerSize) - m.table.SetColumns(getColumns(m.table.Width() - 10)) + m.table.SetColumns(getColumns(m.table.Width()-10, m.config)) m.lastWindowSize = msg return m diff --git a/internal/app/helper.go b/internal/app/helper.go index ba4b2ca..e5026d7 100644 --- a/internal/app/helper.go +++ b/internal/app/helper.go @@ -4,22 +4,68 @@ import ( "strings" "github.com/charmbracelet/bubbles/table" + + "github.com/hedhyw/json-log-viewer/internal/pkg/config" + "github.com/hedhyw/json-log-viewer/internal/pkg/source" ) -func getColumns(width int) []table.Column { - const ( - widthTime = 30 - widthLevel = 10 - ) +func getColumns(width int, cfg *config.Config) []table.Column { + const minWidth = 10 + + flexSpace := width + flexColumns := 0 + + for _, f := range cfg.Fields { + flexSpace -= f.Width - return []table.Column{ - {Title: "Time", Width: widthTime}, - {Title: "Level", Width: widthLevel}, - {Title: "Message", Width: width - widthTime - widthLevel}, + if f.Width == 0 { + flexColumns++ + } } + + flexWidth := 0 + + if flexColumns != 0 { + flexWidth = max(minWidth, flexSpace/flexColumns) + } + + colums := make([]table.Column, 0, len(cfg.Fields)) + + for _, f := range cfg.Fields { + if f.Width == 0 { + f.Width = flexWidth + } + + colums = append(colums, table.Column{ + Title: f.Title, + Width: f.Width, + }) + } + + return colums } func removeClearSequence(value string) string { // https://github.com/charmbracelet/lipgloss/issues/144 return strings.ReplaceAll(value, "\x1b[0", "\x1b[39") } + +func getFieldByKind( + cfg *config.Config, + kind config.FieldKind, + logEntry source.LogEntry, +) string { + for i, f := range cfg.Fields { + if f.Kind != kind { + continue + } + + if i >= len(logEntry.Fields) { + return "-" + } + + return logEntry.Fields[i] + } + + return "" +} diff --git a/internal/app/style.go b/internal/app/style.go index d1f0292..0f55d6e 100644 --- a/internal/app/style.go +++ b/internal/app/style.go @@ -4,6 +4,7 @@ import ( "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" + "github.com/hedhyw/json-log-viewer/internal/pkg/config" "github.com/hedhyw/json-log-viewer/internal/pkg/source" ) @@ -46,7 +47,7 @@ func (m Model) getLogLevelStyle(baseStyle lipgloss.Style, rowID int) lipgloss.St return baseStyle } - color := getColorForLogLevel(m.filteredLogEntries[rowID].Level) + color := getColorForLogLevel(m.getLogLevelFromLogEntry(m.filteredLogEntries[rowID])) if color == "" { return baseStyle } @@ -72,3 +73,7 @@ func getColorForLogLevel(level source.Level) lipgloss.Color { return "" } } + +func (m Model) getLogLevelFromLogEntry(logEntry source.LogEntry) source.Level { + return source.Level(getFieldByKind(m.config, config.FieldKindLevel, logEntry)) +} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go new file mode 100644 index 0000000..b606f5e --- /dev/null +++ b/internal/pkg/config/config.go @@ -0,0 +1,120 @@ +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/go-playground/validator/v10" + "github.com/hedhyw/jsoncjson" +) + +// PathDefault is a fake path to the default config. +const PathDefault = "default" + +// Config contains application customization settings. +type Config struct { + // Path to the config. + Path string `json:"-"` + + Fields []Field `json:"fields" validate:"min=1"` +} + +// FieldKind describes the type of the log field. +type FieldKind string + +// Possible kinds. +const ( + FieldKindTime FieldKind = "time" + FieldKindMessage FieldKind = "message" + FieldKindLevel FieldKind = "level" + FieldKindAny FieldKind = "any" +) + +// Field customization. +type Field struct { + Title string `json:"title" validate:"required,min=1,max=32"` + Kind FieldKind `json:"kind" validate:"required,oneof=time message level any"` + References []string `json:"ref" validate:"min=1,dive,required"` + Width int `json:"width" validate:"min=0"` +} + +// GetDefaultConfig returns the configuration with default values. +func GetDefaultConfig() *Config { + return &Config{ + Path: "default", + Fields: []Field{{ + Title: "Time", + Kind: FieldKindTime, + References: []string{"$.timestamp", "$.time", "$.t", "$.ts"}, + Width: 30, + }, { + Title: "Level", + Kind: FieldKindLevel, + References: []string{"$.level", "$.lvl", "$.l"}, + Width: 10, + }, { + Title: "Message", + Kind: FieldKindMessage, + References: []string{"$.message", "$.msg", "$.error", "$.err"}, + }}, + } +} + +// Read config from the given paths. From higher priority to lower priority. +func Read(paths ...string) (*Config, error) { + cfg, err := readConfigFromPaths(paths...) + if err != nil { + return nil, fmt.Errorf("reading from paths: %w", err) + } + + err = validator.New().Struct(cfg) + if err != nil { + return nil, fmt.Errorf("validating config: %s: %w", cfg.Path, err) + } + + return cfg, nil +} + +func readConfigFromPaths(paths ...string) (*Config, error) { + for _, p := range paths { + _, err := os.Stat(p) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + continue + } + + return nil, fmt.Errorf("checking config: %w", err) + } + + cfg, err := readConfigFromFile(p) + if err != nil { + return nil, fmt.Errorf("reading config from file: %w", err) + } + + return cfg, nil + } + + return GetDefaultConfig(), nil +} + +func readConfigFromFile(path string) (cfg *Config, err error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("os opening: %w", err) + } + + defer func() { err = errors.Join(err, file.Close()) }() + + err = json.NewDecoder( + jsoncjson.NewReader(file), + ).Decode(&cfg) + if err != nil { + return nil, fmt.Errorf("decoding json: %w", err) + } + + cfg.Path = path + + return cfg, nil +} diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go new file mode 100644 index 0000000..d9949a6 --- /dev/null +++ b/internal/pkg/config/config_test.go @@ -0,0 +1,268 @@ +package config_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "strconv" + "strings" + "testing" + "time" + + "github.com/go-playground/validator/v10" + "github.com/stretchr/testify/assert" + + "github.com/hedhyw/json-log-viewer/internal/pkg/config" + "github.com/hedhyw/json-log-viewer/internal/pkg/tests" +) + +func TestReadDefault(t *testing.T) { + t.Parallel() + + cfg, err := config.Read() + if assert.NoError(t, err) { + assert.Equal(t, config.PathDefault, cfg.Path) + + def := config.GetDefaultConfig() + assert.ElementsMatch(t, cfg.Fields, def.Fields) + } +} + +func TestReadNotFound(t *testing.T) { + t.Parallel() + + cfg, err := config.Read("not_found_" + strconv.FormatInt(time.Now().UnixNano(), 10)) + if assert.NoError(t, err) { + assert.Equal(t, config.PathDefault, cfg.Path) + } +} + +func TestReadPriority(t *testing.T) { + t.Parallel() + + configJSON := tests.RequireEncodeJSON(t, config.GetDefaultConfig()) + fileFirst := tests.RequireCreateFile(t, configJSON) + fileSecond := tests.RequireCreateFile(t, configJSON) + + cfg, err := config.Read(fileFirst, fileSecond) + if assert.NoError(t, err) { + assert.Equal(t, fileFirst, cfg.Path) + } +} + +func TestReadValidated(t *testing.T) { + t.Parallel() + + cfg := config.GetDefaultConfig() + cfg.Fields = nil + + configJSON := tests.RequireEncodeJSON(t, cfg) + configFile := tests.RequireCreateFile(t, configJSON) + + _, err := config.Read(configFile) + if assert.Error(t, err) { + assert.ErrorAs(t, err, &validator.ValidationErrors{}) + } +} + +func TestReadInvalidJSON(t *testing.T) { + t.Parallel() + + configFile := tests.RequireCreateFile(t, []byte("-")) + + _, err := config.Read(configFile) + if assert.Error(t, err) { + assert.ErrorIs(t, err, io.ErrUnexpectedEOF) + } +} + +func TestReadDirectory(t *testing.T) { + t.Parallel() + + _, err := config.Read(".") + assert.Error(t, err) +} + +func ExampleGetDefaultConfig() { + cfg := config.GetDefaultConfig() + + var buf bytes.Buffer + + jsonEncoder := json.NewEncoder(&buf) + jsonEncoder.SetIndent("", "\t") + + if err := jsonEncoder.Encode(&cfg); err != nil { + log.Fatal(err) + } + + fmt.Println(buf.String()) + // Output: + // { + // "fields": [ + // { + // "title": "Time", + // "kind": "time", + // "ref": [ + // "$.timestamp", + // "$.time", + // "$.t", + // "$.ts" + // ], + // "width": 30 + // }, + // { + // "title": "Level", + // "kind": "level", + // "ref": [ + // "$.level", + // "$.lvl", + // "$.l" + // ], + // "width": 10 + // }, + // { + // "title": "Message", + // "kind": "message", + // "ref": [ + // "$.message", + // "$.msg", + // "$.error", + // "$.err" + // ], + // "width": 0 + // } + // ] + // } +} + +func TestValidateField(t *testing.T) { + t.Parallel() + + testCases := [...]struct { + Name string + Apply func(value *config.Field) + IsValid bool + }{{ + Name: "ok", + Apply: func(*config.Field) {}, + IsValid: true, + }, { + Name: "unset_title", + Apply: func(value *config.Field) { + value.Title = "" + }, + IsValid: false, + }, { + Name: "short_title", + Apply: func(value *config.Field) { + value.Title = "." + }, + IsValid: true, + }, { + Name: "almost_long_title", + Apply: func(value *config.Field) { + value.Title = strings.Repeat(".", 32) + }, + IsValid: true, + }, { + Name: "long_title", + Apply: func(value *config.Field) { + value.Title = strings.Repeat(".", 33) + }, + IsValid: false, + }, { + Name: "unset_references", + Apply: func(value *config.Field) { + value.References = []string{} + }, + IsValid: false, + }, { + Name: "empty_reference", + Apply: func(value *config.Field) { + value.References = []string{""} + }, + IsValid: false, + }, { + Name: "kind_any", + Apply: func(value *config.Field) { + value.Kind = config.FieldKindAny + }, + IsValid: true, + }, { + Name: "kind_level", + Apply: func(value *config.Field) { + value.Kind = config.FieldKindLevel + }, + IsValid: true, + }, { + Name: "kind_message", + Apply: func(value *config.Field) { + value.Kind = config.FieldKindMessage + }, + IsValid: true, + }, { + Name: "kind_time", + Apply: func(value *config.Field) { + value.Kind = config.FieldKindTime + }, + IsValid: true, + }, { + Name: "unset_kind", + Apply: func(value *config.Field) { + value.Kind = "" + }, + IsValid: false, + }, { + Name: "invalid_kind", + Apply: func(value *config.Field) { + value.Kind = "invalid" + }, + IsValid: false, + }, { + Name: "unset_width", + Apply: func(value *config.Field) { + value.Width = 0 + }, + IsValid: true, + }, { + Name: "small_width", + Apply: func(value *config.Field) { + value.Width = 1 + }, + IsValid: true, + }, { + Name: "negative_width", + Apply: func(value *config.Field) { + value.Width = -1 + }, + IsValid: false, + }} + + validator := validator.New() + + for _, testCaseNotInParallel := range testCases { + testCase := testCaseNotInParallel + + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + + value := config.Field{ + Title: "Title", + Kind: config.FieldKindAny, + References: []string{"$.test"}, + Width: 0, + } + + testCase.Apply(&value) + + err := validator.Struct(value) + if testCase.IsValid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/internal/pkg/source/entry.go b/internal/pkg/source/entry.go index 8d1d333..4bf1adb 100644 --- a/internal/pkg/source/entry.go +++ b/internal/pkg/source/entry.go @@ -3,28 +3,26 @@ package source import ( "bytes" "encoding/json" + "fmt" + "strconv" "strings" "unicode" "github.com/charmbracelet/bubbles/table" - "github.com/valyala/fastjson" + "github.com/yalp/jsonpath" + + "github.com/hedhyw/json-log-viewer/internal/pkg/config" ) // LogEntry is a single partly-parse record of the log. type LogEntry struct { - Time string - Level Level - Message string - Line json.RawMessage + Fields []string + Line json.RawMessage } // Row returns table.Row representation of the log entry. func (e LogEntry) Row() table.Row { - return table.Row{ - e.Time, - string(e.Level), - e.Message, - } + return e.Fields } // LogEntries is a helper type definition for the slice of log entries. @@ -68,29 +66,90 @@ func (entries LogEntries) Rows() []table.Row { return rows } -// ParseLogEntry parses a single log entry from the json line. -func ParseLogEntry(line json.RawMessage) LogEntry { - var jsonParser fastjson.Parser +func parseField(parsedLine any, field config.Field) string { + for _, ref := range field.References { + foundField, err := jsonpath.Read(parsedLine, ref) + if err != nil { + continue + } + + jsonField, err := json.Marshal(foundField) + if err != nil { + return fmt.Sprint(field) + } + + unquotedField, err := strconv.Unquote(string(jsonField)) + if err != nil { + return string(jsonField) + } + + return formatField(unquotedField, field.Kind) + } - lineToParse := make([]byte, len(line)) - copy(lineToParse, line) - line = lineToParse + return "-" +} + +func formatField( + value string, + kind config.FieldKind, +) string { + value = strings.TrimSpace(value) + + switch kind { + case config.FieldKindMessage: + return formatMessage(value) + case config.FieldKindLevel: + return string(ParseLevel(formatMessage(value))) + case config.FieldKindTime: + return formatMessage(value) + case config.FieldKindAny: + return formatMessage(value) + default: + return formatMessage(value) + } +} - value, err := jsonParser.ParseBytes(lineToParse) +// ParseLogEntry parses a single log entry from the json line. +func ParseLogEntry( + line json.RawMessage, + cfg *config.Config, +) LogEntry { + var parsedLine any + + err := json.Unmarshal(normalizeJSON(line), &parsedLine) if err != nil { - return LogEntry{ - Line: line, - Time: "-", - Message: formatMessage(string(line)), - Level: LevelUnknown, + return getPlainLogEntry(line, cfg) + } + + fields := make([]string, 0, len(cfg.Fields)) + + for _, f := range cfg.Fields { + fields = append(fields, parseField(parsedLine, f)) + } + + return LogEntry{ + Line: line, + Fields: fields, + } +} + +func getPlainLogEntry( + line json.RawMessage, + cfg *config.Config, +) LogEntry { + fields := make([]string, len(cfg.Fields)) + + for i, f := range cfg.Fields { + fields[i] = "-" + + if f.Kind == config.FieldKindMessage { + fields[i] = string(line) } } return LogEntry{ - Line: line, - Time: formatMessage(extractTime(value)), - Message: formatMessage(extractMessage(value)), - Level: extractLevel(value), + Fields: fields, + Line: line, } } diff --git a/internal/pkg/source/entry_test.go b/internal/pkg/source/entry_test.go index 0a8b0a5..22fd543 100644 --- a/internal/pkg/source/entry_test.go +++ b/internal/pkg/source/entry_test.go @@ -5,123 +5,160 @@ import ( "strings" "testing" + "github.com/hedhyw/json-log-viewer/internal/pkg/config" "github.com/hedhyw/json-log-viewer/internal/pkg/source" "github.com/stretchr/testify/assert" ) -func TestParseLogEntry(t *testing.T) { +func TestParseLogEntryDefault(t *testing.T) { t.Parallel() testCases := [...]struct { Name string JSON string - Assert func(tb testing.TB, logEntry source.LogEntry) + Assert func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) }{{ Name: "plain_log", JSON: "Hello World", - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "Hello World", logEntry.Message) - assert.Equal(t, source.LevelUnknown, logEntry.Level) - assert.Equal(t, "-", logEntry.Time) + assert.Equal(t, "Hello World", fieldKindToValue[config.FieldKindMessage], fieldKindToValue) + assert.Equal(t, "-", fieldKindToValue[config.FieldKindLevel], fieldKindToValue) + assert.Equal(t, "-", fieldKindToValue[config.FieldKindTime], fieldKindToValue) }, }, { Name: "time_number", JSON: `{"time":1}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "1", logEntry.Time) + assert.Equal(t, "1", fieldKindToValue[config.FieldKindTime], fieldKindToValue) }, }, { Name: "timestamp_number", JSON: `{"timestamp":1}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "1", logEntry.Time) + assert.Equal(t, "1", fieldKindToValue[config.FieldKindTime], fieldKindToValue) }, }, { Name: "ts_number", JSON: `{"ts":1}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "1", logEntry.Time) + assert.Equal(t, "1", fieldKindToValue[config.FieldKindTime], fieldKindToValue) }, }, { Name: "time_text", JSON: `{"time":"1970-01-01T00:00:00.00"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "1970-01-01T00:00:00.00", logEntry.Time) + assert.Equal(t, + "1970-01-01T00:00:00.00", + fieldKindToValue[config.FieldKindTime], + fieldKindToValue, + ) }, }, { Name: "timestamp_text", JSON: `{"timestamp":"1970-01-01T00:00:00.00"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "1970-01-01T00:00:00.00", logEntry.Time) + assert.Equal(t, + "1970-01-01T00:00:00.00", + fieldKindToValue[config.FieldKindTime], + fieldKindToValue, + ) }, }, { Name: "ts_text", JSON: `{"ts":"1970-01-01T00:00:00.00"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "1970-01-01T00:00:00.00", logEntry.Time) + assert.Equal(t, + "1970-01-01T00:00:00.00", + fieldKindToValue[config.FieldKindTime], + fieldKindToValue, + ) }, }, { Name: "message", JSON: `{"message":"message"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "message", logEntry.Message) + assert.Equal(t, + "message", + fieldKindToValue[config.FieldKindMessage], + fieldKindToValue, + ) }, }, { Name: "msg", JSON: `{"msg":"msg"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "msg", logEntry.Message) + assert.Equal(t, + "msg", + fieldKindToValue[config.FieldKindMessage], + fieldKindToValue, + ) }, }, { Name: "message_special_rune", JSON: `{"message":"mes` + string(rune(1)) + `sage"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "message", logEntry.Message) + assert.Equal(t, + "message", + fieldKindToValue[config.FieldKindMessage], + fieldKindToValue, + ) }, }, { Name: "error", JSON: `{"error":"error"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "error", logEntry.Message) + assert.Equal(t, + "error", + fieldKindToValue[config.FieldKindMessage], + fieldKindToValue, + ) }, }, { Name: "err", JSON: `{"err":"err"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "err", logEntry.Message) + assert.Equal(t, + "err", + fieldKindToValue[config.FieldKindMessage], + fieldKindToValue, + ) }, }, { Name: "level", JSON: `{"level":"INFO"}`, - Assert: func(tb testing.TB, logEntry source.LogEntry) { + Assert: func(tb testing.TB, fieldKindToValue map[config.FieldKind]string) { tb.Helper() - assert.Equal(t, "info", logEntry.Level.String()) + assert.Equal(t, + "info", + fieldKindToValue[config.FieldKindLevel], + fieldKindToValue, + ) }, }} @@ -131,8 +168,11 @@ func TestParseLogEntry(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) { t.Parallel() - actual := source.ParseLogEntry(json.RawMessage(testCase.JSON)) - testCase.Assert(t, actual) + cfg := config.GetDefaultConfig() + + actual := source.ParseLogEntry(json.RawMessage(testCase.JSON), cfg) + + testCase.Assert(t, getFieldKindToValue(cfg, actual.Fields)) }) } } @@ -143,11 +183,7 @@ func TestLogEntryRow(t *testing.T) { entry := getFakeLogEntry() row := entry.Row() - if assert.Len(t, row, 3) { - assert.Equal(t, entry.Time, row[0]) - assert.Equal(t, string(entry.Level), row[1]) - assert.Equal(t, entry.Message, row[2]) - } + assert.Equal(t, []string(row), entry.Fields) } func TestLogEntriesRows(t *testing.T) { @@ -209,10 +245,12 @@ func TestLogEntriesReverse(t *testing.T) { func getFakeLogEntry() source.LogEntry { return source.LogEntry{ - Time: "time", - Level: source.LevelUnknown, - Message: "message", - Line: []byte(`{"hello":"world"}`), + Fields: []string{ + "time", + source.LevelUnknown.String(), + "message", + }, + Line: []byte(`{"hello":"world"}`), } } @@ -222,7 +260,7 @@ func TestLogEntriesFilter(t *testing.T) { term := "special MESSAGE to search by in the test: " + t.Name() logEntry := getFakeLogEntry() - logEntry.Message = term + logEntry.Fields = append(logEntry.Fields, term) logEntry.Line = json.RawMessage(`{"message": "` + term + `"}`) logEntries := source.LogEntries{ @@ -262,3 +300,13 @@ func TestLogEntriesFilter(t *testing.T) { assert.Empty(t, filtered) }) } + +func getFieldKindToValue(cfg *config.Config, entries []string) map[config.FieldKind]string { + fieldKindToValue := make(map[config.FieldKind]string, len(entries)) + + for i, f := range cfg.Fields { + fieldKindToValue[f.Kind] = entries[i] + } + + return fieldKindToValue +} diff --git a/internal/pkg/source/helper.go b/internal/pkg/source/helper.go index 402ce44..67ae295 100644 --- a/internal/pkg/source/helper.go +++ b/internal/pkg/source/helper.go @@ -1,50 +1,17 @@ package source import ( - "strconv" - "strings" - - "github.com/valyala/fastjson" + "unicode" ) -func extractTime(value *fastjson.Value) string { - timeValue := extractValue(value, "timestamp", "time", "t", "ts") - if timeValue != "" { - return formatMessage(strings.TrimSpace(timeValue)) - } - - return "-" -} - -func extractLevel(value *fastjson.Value) Level { - level := extractValue(value, "level", "lvl") - - return ParseLevel(formatMessage(level)) -} - -func extractValue(value *fastjson.Value, keys ...string) string { - for _, k := range keys { - element := value.Get(k) - - text := string(element.GetStringBytes()) - if text != "" { - return text - } +func normalizeJSON(input []byte) []byte { + out := make([]byte, 0, len(input)) - number := element.GetInt() - if number != 0 { - return strconv.Itoa(number) + for _, r := range string(input) { + if unicode.IsPrint(r) { + out = append(out, []byte(string(r))...) } } - return "" -} - -func extractMessage(value *fastjson.Value) string { - message := extractValue(value, "message", "msg", "error", "err") - if message != "" { - return strings.TrimSpace(message) - } - - return strings.TrimSpace(value.String()) + return out } diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go index 8d7d320..804d3c8 100644 --- a/internal/pkg/source/source.go +++ b/internal/pkg/source/source.go @@ -8,6 +8,8 @@ import ( "os" tea "github.com/charmbracelet/bubbletea" + + "github.com/hedhyw/json-log-viewer/internal/pkg/config" ) const ( @@ -17,7 +19,10 @@ const ( ) // LoadLogsFromFile loads json log entries from file. -func LoadLogsFromFile(path string) func() tea.Msg { +func LoadLogsFromFile( + path string, + cfg *config.Config, +) func() tea.Msg { return func() (msg tea.Msg) { file, err := os.Open(path) if err != nil { @@ -26,7 +31,7 @@ func LoadLogsFromFile(path string) func() tea.Msg { defer file.Close() - logEntries, err := parseLogEntriesFromReader(file) + logEntries, err := parseLogEntriesFromReader(file, cfg) if err != nil { return fmt.Errorf("parsing from reader: %w", err) } @@ -35,7 +40,10 @@ func LoadLogsFromFile(path string) func() tea.Msg { } } -func parseLogEntriesFromReader(reader io.Reader) (LogEntries, error) { +func parseLogEntriesFromReader( + reader io.Reader, + cfg *config.Config, +) (LogEntries, error) { bufReader := bufio.NewReaderSize(reader, maxLineSize) logEntries := make(LogEntries, 0, logEntriesEstimateNumber) @@ -50,7 +58,7 @@ func parseLogEntriesFromReader(reader io.Reader) (LogEntries, error) { } if len(bytes.TrimSpace(line)) > 0 { - logEntries = append(logEntries, ParseLogEntry(line)) + logEntries = append(logEntries, ParseLogEntry(line, cfg)) } } diff --git a/internal/pkg/source/source_test.go b/internal/pkg/source/source_test.go index 2197dca..41db5a2 100644 --- a/internal/pkg/source/source_test.go +++ b/internal/pkg/source/source_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/hedhyw/json-log-viewer/assets" + "github.com/hedhyw/json-log-viewer/internal/pkg/config" "github.com/hedhyw/json-log-viewer/internal/pkg/source" "github.com/hedhyw/json-log-viewer/internal/pkg/tests" ) @@ -19,7 +20,10 @@ func TestLoadLogsFromFile(t *testing.T) { testFile := tests.RequireCreateFile(t, assets.ExampleJSONLog()) - msg := source.LoadLogsFromFile(testFile)() + msg := source.LoadLogsFromFile( + testFile, + config.GetDefaultConfig(), + )() logEntries, ok := msg.(source.LogEntries) if assert.Truef(t, ok, "actual type: %T", msg) { @@ -30,7 +34,10 @@ func TestLoadLogsFromFile(t *testing.T) { t.Run("not_found", func(t *testing.T) { t.Parallel() - msg := source.LoadLogsFromFile("not_found_for_" + t.Name())() + msg := source.LoadLogsFromFile( + "not_found_for_"+t.Name(), + config.GetDefaultConfig(), + )() _, ok := msg.(error) assert.Truef(t, ok, "actual type: %T", msg) @@ -42,7 +49,10 @@ func TestLoadLogsFromFile(t *testing.T) { longLine := strings.Repeat("1", 2*1024*1024) testFile := tests.RequireCreateFile(t, []byte(longLine)) - msg := source.LoadLogsFromFile(testFile)() + msg := source.LoadLogsFromFile( + testFile, + config.GetDefaultConfig(), + )() logEntries, ok := msg.(source.LogEntries) if assert.Truef(t, ok, "actual type: %T", msg) { diff --git a/internal/pkg/tests/tests.go b/internal/pkg/tests/tests.go index d2d4cc9..f7ba83c 100644 --- a/internal/pkg/tests/tests.go +++ b/internal/pkg/tests/tests.go @@ -1,6 +1,7 @@ package tests import ( + "encoding/json" "os" "testing" @@ -26,3 +27,13 @@ func RequireCreateFile(tb testing.TB, content []byte) string { return name } + +// RequireEncodeJSON marshals value to JSON. +func RequireEncodeJSON(tb testing.TB, value any) []byte { + tb.Helper() + + content, err := json.Marshal(value) + require.NoError(tb, err) + + return content +}