Skip to content

Commit e0a876b

Browse files
chirinohedhyw
andauthored
feat: Fast stdin display, lower memory usage, follow/reverse mode, home/end key support. long help support (#85)
Signed-off-by: Hiram Chirino <[email protected]> Co-authored-by: Maksym Kryvchun <[email protected]>
1 parent 906283d commit e0a876b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1094
-1115
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ bin
88

99
# IDE
1010
.vscode
11+
.idea
1112

1213
# Test binary, built with `go test -c`
1314
*.test

.golangci.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,18 @@
2525
"ireturn",
2626
"gomoddirectives",
2727
"execinquery",
28-
"tagalign"
28+
"tagalign",
29+
"mnd",
30+
"nlreturn"
2931
]
3032
},
3133
"linters-settings": {
3234
"goimports": {
3335
"local-prefixes": "github.com/hedhyw/json-log-viewer/"
3436
},
37+
"cyclop": {
38+
"max-complexity": 15
39+
},
3540
"revive": {}
3641
}
3742
}

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ lint: bin/golangci-lint-${GOLANG_CI_LINT_VER}
3333
./bin/golangci-lint-${GOLANG_CI_LINT_VER} run
3434
.PHONY: lint
3535

36+
fix: bin/golangci-lint-${GOLANG_CI_LINT_VER}
37+
gofumpt -l -w .
38+
./bin/golangci-lint-${GOLANG_CI_LINT_VER} run --fix
39+
.PHONY: lint-fix
40+
3641
test:
3742
go test \
3843
-coverpkg=${COVER_PACKAGES} \

README.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,19 @@ docker logs -f 000000000000 2>&1 | jlv
7575

7676
### Hotkeys
7777

78-
| Key | Action |
79-
| ------ | -------------- |
80-
| Enter | Open/Close log |
81-
| F | Filter |
82-
| Ctrl+C | Exit |
83-
| F10 | Exit |
84-
| Esc | Back |
85-
| ↑↓ | Navigation |
86-
87-
> \[\] Click Up on the first row to reload the file.
78+
| Key | Action |
79+
|--------|-------------------|
80+
| Enter | Open log |
81+
| Esc | Back |
82+
| F | Filter |
83+
| R | Reverse |
84+
| Ctrl+C | Exit |
85+
| F10 | Exit |
86+
| ↑↓ | Line Up / Down |
87+
| Home | Navigate to Start |
88+
| End | Navigate to End |
89+
90+
> Attempting to navigate past the last line in the log will put you in follow mode.
8891
8992
## Install
9093

assets/jlv.log

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
time=2024-08-19T15:06:16.848-04:00 level=INFO msg="case <-eofEvent"

cmd/jlv/main.go

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
package main
22

33
import (
4-
"bytes"
4+
"context"
55
"flag"
66
"fmt"
7-
"io/fs"
87
"os"
98
"path"
109

1110
tea "github.com/charmbracelet/bubbletea"
1211

1312
"github.com/hedhyw/json-log-viewer/internal/app"
1413
"github.com/hedhyw/json-log-viewer/internal/pkg/config"
14+
"github.com/hedhyw/json-log-viewer/internal/pkg/events"
1515
"github.com/hedhyw/json-log-viewer/internal/pkg/source"
16-
"github.com/hedhyw/json-log-viewer/internal/pkg/source/fileinput"
17-
"github.com/hedhyw/json-log-viewer/internal/pkg/source/readerinput"
1816
)
1917

2018
// version will be set on build.
@@ -39,41 +37,54 @@ func main() {
3937
fatalf("Error reading config: %s\n", err)
4038
}
4139

42-
var sourceInput source.Input
40+
fileName := ""
41+
var inputSource *source.Source
4342

4443
switch flag.NArg() {
4544
case 0:
46-
sourceInput, err = getStdinSource(cfg, os.Stdin)
45+
// Tee stdin to a temp file, so that we can
46+
// lazy load the log entries using random access.
47+
fileName = "-"
48+
49+
stdIn, err := getStdinReader(os.Stdin)
4750
if err != nil {
4851
fatalf("Stdin: %s\n", err)
4952
}
53+
54+
inputSource, err = source.Reader(stdIn, cfg)
55+
if err != nil {
56+
fatalf("Could not create temp flie: %s\n", err)
57+
}
58+
defer inputSource.Close()
59+
5060
case 1:
51-
sourceInput = fileinput.New(flag.Arg(0))
61+
fileName = flag.Arg(0)
62+
inputSource, err = source.File(fileName, cfg)
63+
if err != nil {
64+
fatalf("Could not create temp flie: %s\n", err)
65+
}
66+
defer inputSource.Close()
67+
5268
default:
5369
fatalf("Invalid arguments, usage: %s file.log\n", os.Args[0])
5470
}
5571

56-
appModel := app.NewModel(sourceInput, cfg, version)
72+
appModel := app.NewModel(fileName, cfg, version)
5773
program := tea.NewProgram(appModel, tea.WithInputTTY(), tea.WithAltScreen())
5874

75+
inputSource.StartStreaming(context.Background(), func(entries source.LazyLogEntries, err error) {
76+
if err != nil {
77+
program.Send(events.ErrorOccuredMsg{Err: err})
78+
} else {
79+
program.Send(events.LogEntriesUpdateMsg(entries))
80+
}
81+
})
82+
5983
if _, err := program.Run(); err != nil {
6084
fatalf("Error running program: %s\n", err)
6185
}
6286
}
6387

64-
func getStdinSource(cfg *config.Config, defaultInput fs.File) (source.Input, error) {
65-
stat, err := defaultInput.Stat()
66-
if err != nil {
67-
return nil, fmt.Errorf("stat: %w", err)
68-
}
69-
70-
if stat.Mode()&os.ModeCharDevice != 0 {
71-
return readerinput.New(bytes.NewReader(nil), cfg.StdinReadTimeout), nil
72-
}
73-
74-
return readerinput.New(defaultInput, cfg.StdinReadTimeout), nil
75-
}
76-
7788
func fatalf(message string, args ...any) {
7889
fmt.Fprintf(os.Stderr, message, args...)
7990
os.Exit(1)

cmd/jlv/main_test.go

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,19 @@ package main
22

33
import (
44
"bytes"
5-
"context"
65
"errors"
76
"io"
87
"io/fs"
98
"os"
109
"testing"
1110

12-
"github.com/hedhyw/json-log-viewer/internal/pkg/config"
13-
1411
"github.com/stretchr/testify/assert"
1512
"github.com/stretchr/testify/require"
1613
)
1714

1815
func TestGetStdinSource(t *testing.T) {
1916
t.Parallel()
2017

21-
ctx := context.Background()
22-
2318
t.Run("ModeNamedPipe", func(t *testing.T) {
2419
t.Parallel()
2520

@@ -32,15 +27,10 @@ func TestGetStdinSource(t *testing.T) {
3227
},
3328
}
3429

35-
input, err := getStdinSource(config.GetDefaultConfig(), file)
36-
require.NoError(t, err)
37-
38-
readCloser, err := input.ReadCloser(ctx)
30+
input, err := getStdinReader(file)
3931
require.NoError(t, err)
4032

41-
t.Cleanup(func() { assert.NoError(t, readCloser.Close()) })
42-
43-
data, err := io.ReadAll(readCloser)
33+
data, err := io.ReadAll(input)
4434
require.NoError(t, err)
4535
assert.Equal(t, content, string(data))
4636
})
@@ -55,15 +45,10 @@ func TestGetStdinSource(t *testing.T) {
5545
},
5646
}
5747

58-
input, err := getStdinSource(config.GetDefaultConfig(), file)
59-
require.NoError(t, err)
60-
61-
readCloser, err := input.ReadCloser(ctx)
48+
input, err := getStdinReader(file)
6249
require.NoError(t, err)
6350

64-
t.Cleanup(func() { assert.NoError(t, readCloser.Close()) })
65-
66-
data, err := io.ReadAll(readCloser)
51+
data, err := io.ReadAll(input)
6752
require.NoError(t, err)
6853
assert.Empty(t, data)
6954
})
@@ -76,7 +61,7 @@ func TestGetStdinSource(t *testing.T) {
7661

7762
file := fakeFile{ErrStat: errStat}
7863

79-
_, err := getStdinSource(config.GetDefaultConfig(), file)
64+
_, err := getStdinReader(file)
8065
require.Error(t, err)
8166
require.ErrorIs(t, err, errStat)
8267
})

cmd/jlv/stdin_reader.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//go:build !mock_stdin
2+
3+
package main
4+
5+
import (
6+
"bytes"
7+
"fmt"
8+
"io"
9+
"io/fs"
10+
"os"
11+
)
12+
13+
func getStdinReader(defaultInput fs.File) (io.Reader, error) {
14+
stat, err := defaultInput.Stat()
15+
if err != nil {
16+
return nil, fmt.Errorf("stat: %w", err)
17+
}
18+
19+
if stat.Mode()&os.ModeCharDevice != 0 {
20+
return bytes.NewReader(nil), nil
21+
}
22+
23+
return defaultInput, nil
24+
}

cmd/jlv/stdin_reader_mock.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//go:build mock_stdin
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"io"
8+
"io/fs"
9+
"os"
10+
"time"
11+
)
12+
13+
func getStdinReader(defaultInput fs.File) (io.Reader, error) {
14+
r, w, err := os.Pipe()
15+
if err != nil {
16+
return nil, fmt.Errorf("pipe: %w", err)
17+
}
18+
go func() {
19+
defer w.Close()
20+
for i := 0; ; i++ {
21+
_, err := w.Write([]byte(fmt.Sprintf(`{"message": "Line %d"}
22+
`, i)))
23+
if err != nil {
24+
fatalf("Write failed: %s\n", err)
25+
}
26+
time.Sleep(10 * time.Millisecond)
27+
}
28+
}()
29+
return r, nil
30+
}

example.jlv.jsonc

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,7 @@
6363
"50": "error",
6464
"60": "fatal"
6565
},
66-
// The number of rows to pre-render.
67-
"prerenderRows": 100,
68-
// The number nanoseconds between manual file reloads.
69-
"reloadThreshold": 1000000000,
7066
// The maximum size of the file in bytes.
7167
// The rest of the file will be ignored.
7268
"maxFileSizeBytes": 1073741824,
73-
// StdinReadTimeout is the timeout (in nanoseconds) of reading from the standart input.
74-
"stdinReadTimeout": 1000000000
7569
}

0 commit comments

Comments
 (0)