diff --git a/README.md b/README.md index daa9efa..8fc4b66 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Main features: 4. All non-json logs are captured. 5. It understands different field names: `time`, `timestamp`, `msg`, `message`, `err`, `error` etc. 6. It supports case-insensitive filtering. -7. It is simple. +7. Colorized log levels. It uses [antonmedv/fx](https://github.com/antonmedv/fx) for viewing JSON records and [charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) for organizing the terminal UI. The tool is inspired by the project [json-log-viewer](https://github.com/gistia/json-log-viewer) which is unfortunately outdated and deserted. diff --git a/assets/example.log b/assets/example.log index 5498220..479a56f 100644 --- a/assets/example.log +++ b/assets/example.log @@ -35,11 +35,11 @@ {"time":"1970-01-01T00:00:00.00","level":"INFO","message": "Never mistake activity for achievement.","author": "John Wooden"} {"time":"1970-01-01T00:00:00.00","level":"INFO","message": "What worries you masters you.","author": "Haddon Robinson"} {"time":"1970-01-01T00:00:00.00","level":"INFO","message": "One faces the future with ones past.","author": "Pearl Buck"} -{"time":"1970-01-01T00:00:00.00","level":"INFO","message": "Goals are the fuel in the furnace of achievement.","author": "Brian Tracy"} -{"time":"1970-01-01T00:00:00.00","level":"INFO","message": "Who sows virtue reaps honour.","author": "Leonardo da Vinci"} +{"time":"1970-01-01T00:00:00.00","level":"VERBOSE","message": "Goals are the fuel in the furnace of achievement.","author": "Brian Tracy"} {"time":"1970-01-01T00:00:00.00","level":"FATAL","message": "Be the chief but never the lord.","author": "Lao Tzu"} {"time":"1970-01-01T00:00:00.00","level":"PANIC","message": "Fate is in your hands and no one elses","author": "Byron Pulsifer"} {"time":"1970-01-01T00:00:00.00","level":"ERROR","message": "Genius is one percent inspiration and ninety-nine percent perspiration.","author": "Thomas Edison"} +{"time":"1970-01-01T00:00:00.00","level":"WARN","message": "Who sows virtue reaps honour.","author": "Leonardo da Vinci"} {"time":"1970-01-01T00:00:00.00","level":"INFO","message": "You can observe a lot just by watching.","author": "Yogi Berra"} {"time":"1970-01-01T00:00:00.00","level":"DEBUG","message": "A house divided against itself cannot stand.","author": "Abraham Lincoln"} {"time":"1970-01-01T00:00:00.00","level":"TRACE","message": "Difficulties increase the nearer we get to the goal.","author": "Johann Wolfgang von Goethe"} diff --git a/go.mod b/go.mod index 4733046..4c9d23c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.20 replace github.com/antonmedv/fx => github.com/hedhyw/fx v0.0.1 +replace github.com/charmbracelet/bubbles => github.com/hedhyw/bubbles v0.0.2 + require ( github.com/antonmedv/fx v0.0.0-20230706101337-4fec4e492a52 github.com/charmbracelet/bubbles v0.16.1 @@ -19,20 +21,22 @@ 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/kr/pretty v0.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mazznoer/colorgrad v0.9.1 // indirect github.com/mazznoer/csscolorparser v0.1.3 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.15.1 // indirect + 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/sync v0.1.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/term v0.7.0 // indirect - golang.org/x/text v0.9.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 + golang.org/x/text v0.11.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c0ce3f3..f8766e6 100644 --- a/go.sum +++ b/go.sum @@ -2,22 +2,31 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= -github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= 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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/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= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/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.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -34,29 +43,34 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/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= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/handler.go b/internal/app/handler.go index 95782a7..0d0e7f6 100644 --- a/internal/app/handler.go +++ b/internal/app/handler.go @@ -6,6 +6,8 @@ import ( "github.com/hedhyw/json-log-viewer/internal/pkg/source" ) +const cellIDLogLevel = 1 + func (m Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "esc": @@ -44,12 +46,26 @@ func (m Model) handleWindowSizeMsg(msg tea.WindowSizeMsg) Model { } func (m Model) handleLogEntriesMsg(msg source.LogEntries) Model { - m.table.SetRows(msg.Rows()) - if len(m.allLogEntries) == 0 { m.allLogEntries = msg + + tableStyles := getTableStyles() + tableStyles.RenderCell = func(value string, rowID, columnID int) string { + style := tableStyles.Cell + + if columnID == cellIDLogLevel { + return removeClearSequence( + m.getLogLevelStyle(style, rowID).Render(value), + ) + } + + return style.Render(value) + } + + m.table.SetStyles(tableStyles) } + m.table.SetRows(msg.Rows()) m.filteredLogEntries = msg return m diff --git a/internal/app/helper.go b/internal/app/helper.go index f59305f..ba4b2ca 100644 --- a/internal/app/helper.go +++ b/internal/app/helper.go @@ -1,6 +1,10 @@ package app -import "github.com/charmbracelet/bubbles/table" +import ( + "strings" + + "github.com/charmbracelet/bubbles/table" +) func getColumns(width int) []table.Column { const ( @@ -14,3 +18,8 @@ func getColumns(width int) []table.Column { {Title: "Message", Width: width - widthTime - widthLevel}, } } + +func removeClearSequence(value string) string { + // https://github.com/charmbracelet/lipgloss/issues/144 + return strings.ReplaceAll(value, "\x1b[0", "\x1b[39") +} diff --git a/internal/app/style.go b/internal/app/style.go index 47a2b8c..d1f0292 100644 --- a/internal/app/style.go +++ b/internal/app/style.go @@ -3,6 +3,17 @@ package app import ( "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" + + "github.com/hedhyw/json-log-viewer/internal/pkg/source" +) + +// Possible colors. +const ( + colorMagenta lipgloss.Color = "13" + colorYellow lipgloss.Color = "11" + colorGreen lipgloss.Color = "10" + colorOrange lipgloss.Color = "214" + colorRed lipgloss.Color = "9" ) func getTableStyles() table.Styles { @@ -29,3 +40,35 @@ func getBaseStyle() lipgloss.Style { func getFooterStyle() lipgloss.Style { return lipgloss.NewStyle().Height(footerSize).PaddingLeft(2) } + +func (m Model) getLogLevelStyle(baseStyle lipgloss.Style, rowID int) lipgloss.Style { + if rowID < 0 || rowID >= len(m.filteredLogEntries) { + return baseStyle + } + + color := getColorForLogLevel(m.filteredLogEntries[rowID].Level) + if color == "" { + return baseStyle + } + + return baseStyle.Copy().Foreground(color) +} + +func getColorForLogLevel(level source.Level) lipgloss.Color { + switch level { + case source.LevelTrace: + return colorMagenta + case source.LevelDebug: + return colorYellow + case source.LevelInfo: + return colorGreen + case source.LevelWarning: + return colorOrange + case source.LevelError, + source.LevelFatal, + source.LevelPanic: + return colorRed + default: + return "" + } +} diff --git a/internal/app/style_test.go b/internal/app/style_test.go new file mode 100644 index 0000000..1f6d435 --- /dev/null +++ b/internal/app/style_test.go @@ -0,0 +1,60 @@ +package app + +import ( + "testing" + + "github.com/charmbracelet/lipgloss" + "github.com/stretchr/testify/assert" + + "github.com/hedhyw/json-log-viewer/internal/pkg/source" +) + +func TestGetColorForLogLevel(t *testing.T) { + t.Parallel() + + testCases := [...]struct { + Level source.Level + Expected lipgloss.Color + }{{ + Level: source.Level(""), + Expected: "", + }, { + Level: source.LevelUnknown, + Expected: "", + }, { + Level: source.Level("custom"), + Expected: "", + }, { + Level: source.LevelTrace, + Expected: colorMagenta, + }, { + Level: source.LevelDebug, + Expected: colorYellow, + }, { + Level: source.LevelInfo, + Expected: colorGreen, + }, { + Level: source.LevelWarning, + Expected: colorOrange, + }, { + Level: source.LevelError, + Expected: colorRed, + }, { + Level: source.LevelFatal, + Expected: colorRed, + }, { + Level: source.LevelPanic, + Expected: colorRed, + }} + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.Level.String(), func(t *testing.T) { + t.Parallel() + + actual := getColorForLogLevel(testCase.Level) + assert.Equal(t, testCase.Expected, actual) + }) + } +} diff --git a/internal/pkg/source/helper.go b/internal/pkg/source/helper.go index 281fc35..bbfc78e 100644 --- a/internal/pkg/source/helper.go +++ b/internal/pkg/source/helper.go @@ -18,11 +18,8 @@ func extractTime(value *fastjson.Value) string { func extractLevel(value *fastjson.Value) Level { level := extractValue(value, "level", "lvl") - if level != "" { - return Level(strings.TrimSpace(strings.ToLower(level))) - } - return LevelUnknown + return ParseLevel(level) } func extractValue(value *fastjson.Value, keys ...string) string { diff --git a/internal/pkg/source/level.go b/internal/pkg/source/level.go index 4552b54..12f7eb5 100644 --- a/internal/pkg/source/level.go +++ b/internal/pkg/source/level.go @@ -5,6 +5,34 @@ import "strings" // Level of the logs entity. type Level string +// ParseLevel parses level from the text value. +func ParseLevel(value string) Level { + value = strings.ToLower(value) + value = strings.TrimSpace(value) + + switch { + case value == "": + return LevelUnknown + case strings.HasPrefix(value, "t"), + strings.HasPrefix(value, "v"): // Verbose. + return LevelTrace + case strings.HasPrefix(value, "d"): + return LevelDebug + case strings.HasPrefix(value, "e"): + return LevelError + case strings.HasPrefix(value, "i"): + return LevelInfo + case strings.HasPrefix(value, "w"): + return LevelWarning + case strings.HasPrefix(value, "f"): + return LevelFatal + case strings.HasPrefix(value, "p"): + return LevelPanic + default: + return Level(value) + } +} + // String implement fmt.Stringer interface. func (l Level) String() string { return strings.ToLower(string(l)) @@ -13,4 +41,11 @@ func (l Level) String() string { // Possible log levels. const ( LevelUnknown Level = "none" + LevelTrace Level = "trace" + LevelDebug Level = "debug" + LevelInfo Level = "info" + LevelWarning Level = "warn" + LevelError Level = "error" + LevelPanic Level = "panic" + LevelFatal Level = "fatal" ) diff --git a/internal/pkg/source/level_test.go b/internal/pkg/source/level_test.go new file mode 100644 index 0000000..74f14b6 --- /dev/null +++ b/internal/pkg/source/level_test.go @@ -0,0 +1,65 @@ +package source_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/hedhyw/json-log-viewer/internal/pkg/source" +) + +func TestParseLevel(t *testing.T) { + t.Parallel() + + testCases := [...]struct { + Input string + Expected source.Level + }{{ + Input: "", + Expected: source.LevelUnknown, + }, { + Input: "INFO", + Expected: source.LevelInfo, + }, { + Input: "debug", + Expected: source.LevelDebug, + }, { + Input: "info", + Expected: source.LevelInfo, + }, { + Input: "WRN", + Expected: source.LevelWarning, + }, { + Input: "erR", + Expected: source.LevelError, + }, { + Input: "error", + Expected: source.LevelError, + }, { + Input: "panic", + Expected: source.LevelPanic, + }, { + Input: "fatal", + Expected: source.LevelFatal, + }, { + Input: "trace", + Expected: source.LevelTrace, + }, { + Input: "verbose", + Expected: source.LevelTrace, + }, { + Input: " Unknown\t\n", + Expected: source.Level("unknown"), + }} + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.Input, func(t *testing.T) { + t.Parallel() + + actual := source.ParseLevel(testCase.Input) + assert.Equal(t, testCase.Expected, actual) + }) + } +}