ffa4b0163990225a48211b3c1c6178cd242d0492
- Author
- td-jizdebski <jakub.izdebski@jrdltd.co.uk>
- Committer
- GitHub <noreply@github.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/README.md b/README.md
2index 0f6c9c808d410adba49f5cc5f45d1b0e7090611b..04e08cb25fbfa294132ead8c338add9c9b2992b8 100644
3--- a/README.md
4+++ b/README.md
5@@ -37,12 +37,13 @@ Usage:
6 logfmt [OPTIONS]
7
8 Application Options:
9- -l, --level= Log level filter. One of DEBUG, INFO, WARN, ERROR, FATAL (default: INFO)
10- -o, --output= Output field selector (comma separated)
11- -e, --exclude= Exclude field selector (comma separated)
12- -f, --filter= Filter fields (key=value comma separated)
13- -n, --no-color Disable color output
14- -c, --force-color Force color output, even when outputting to a pipe
15+ -l, --level= Log level filter. One of DEBUG, INFO, WARN, ERROR, FATAL (default: INFO)
16+ -o, --output= Output field selector (comma separated)
17+ -e, --exclude= Exclude field selector (comma separated)
18+ -f, --filter= Filter fields (key=value comma separated)
19+ -n, --no-color Disable color output
20+ -c, --force-color Force color output, even when outputting to a pipe
21+ -s, --keep-empty Keep lines with no field present selected by output or with all excluded
22
23 Help Options:
24 -h, --help Show this help message
25@@ -93,6 +94,10 @@ If you don't want to have colors on the output, set `-n`.
26 By default, logfmt will detect if the output is a pipe or redirect to a file and will automatically disable colors. If
27 you still want to have colorized output, for example when piping into `less`, you can force it using `-c`.
28
29+### Show empty lines
30+
31+By default, `logfmt` doesn't output lines when no fields are selected. To **change that behavior** and **show empty lines**, pass the `-k` flag.
32+
33 ### Sponsors
34
35diff --git a/config/config.go b/config/config.go
36index c00e3e81e43b18777e0c7ec99d5aeb07b13de60b..53ceded5163f5cffeb47e7605a180aa9127dad92 100644
37--- a/config/config.go
38+++ b/config/config.go
39@@ -27,16 +27,19 @@ type Config struct {
40 Filter map[string]string
41 // NoTime disables the time output
42 NoTime bool
43+ // Keep lines with no fields
44+ KeepEmpty bool
45 }
46
47 type rawConfig struct {
48- LogLevel string `long:"level" short:"l" description:"Log level filter. One of DEBUG, INFO, WARN, ERROR, FATAL" default:"INFO"` // nolint:lll
49- OutputFields string `long:"output" short:"o" description:"Output field selector (comma separated)"`
50- ExcludeFields string `long:"exclude" short:"e" description:"Exclude field selector (comma separated)"`
51- Filter string `long:"filter" short:"f" description:"Filter fields (key=value comma separated)"`
52- NoColor bool `long:"no-color" short:"n" description:"Disable color output"`
53- ForceColor bool `long:"force-color" short:"c" description:"Force color output, even when outputting to a pipe"`
54- NoTime bool `long:"no-time" short:"t" description:"Disable time output"`
55+ LogLevel string `long:"level" short:"l" description:"Log level filter. One of DEBUG, INFO, WARN, ERROR, FATAL" default:"INFO"` // nolint:lll
56+ OutputFields string `long:"output" short:"o" description:"Output field selector (comma separated)"`
57+ ExcludeFields string `long:"exclude" short:"e" description:"Exclude field selector (comma separated)"`
58+ Filter string `long:"filter" short:"f" description:"Filter fields (key=value comma separated)"`
59+ NoColor bool `long:"no-color" short:"n" description:"Disable color output"`
60+ ForceColor bool `long:"force-color" short:"c" description:"Force color output, even when outputting to a pipe"`
61+ NoTime bool `long:"no-time" short:"t" description:"Disable time output"`
62+ KeepEmpty bool `long:"keep-empty" short:"k" description:"Keep lines with no field present selected by output or with all excluded"`
63 }
64
65 func Parse() (*Config, error) {
66@@ -48,6 +51,10 @@ func Parse() (*Config, error) {
67 return nil, err
68 }
69
70+ if raw.ExcludeFields != "" && raw.OutputFields != "" {
71+ return nil, fmt.Errorf("cannot use both --exclude and --output")
72+ }
73+
74 cfg := Config{
75 OutputFields: parseFieldList(raw.OutputFields),
76 ExcludeFields: parseFieldList(raw.ExcludeFields),
77@@ -72,7 +79,9 @@ func Parse() (*Config, error) {
78 if raw.NoTime {
79 cfg.NoTime = true
80 }
81-
82+ if raw.KeepEmpty {
83+ cfg.KeepEmpty = true
84+ }
85 return &cfg, nil
86 }
87
88diff --git a/parser/parser.go b/parser/parser.go
89index 3221898d7bb0c260dacff1485f477067d28e55b6..90c6f266b825bba0d1618356f781444e59641b15 100644
90--- a/parser/parser.go
91+++ b/parser/parser.go
92@@ -38,10 +38,12 @@ func (p *Parser) Start() error {
93 if len(p.cfg.Filter) > 0 && !record.MatchesFilter(p.cfg.Filter) {
94 continue
95 }
96-
97- _, err = fmt.Fprintf(p.output, "%s\n", record.String(p.cfg))
98- if err != nil {
99- return fmt.Errorf("failed to print log to output: %w", err)
100+ line := record.String(p.cfg)
101+ if line != "" {
102+ _, err = fmt.Fprintf(p.output, "%s\n", record.String(p.cfg))
103+ if err != nil {
104+ return fmt.Errorf("failed to print log to output: %w", err)
105+ }
106 }
107 }
108
109diff --git a/parser/parser_test.go b/parser/parser_test.go
110index d4cd31cb41fb85ad7c1db13fc08c0a5a35d2c0d3..73d46ce514c4ec7e7ba766a0f29dfcf7860efac1 100644
111--- a/parser/parser_test.go
112+++ b/parser/parser_test.go
113@@ -33,20 +33,20 @@ time="2025-03-15T10:32:27Z" level=fatal msg="AAAAAA"`)
114 }
115
116 func TestParseTimestamps(t *testing.T) {
117- data := strings.NewReader(`timestamp="2025-03-15T10:32:23Z" level=info
118-time="2025-03-15T10:32:24Z" level=info
119-ts="2025-03-15T10:32:25Z" level=info
120-datetime="2025-03-15T10:32:26Z" level=info`)
121+ data := strings.NewReader(`timestamp="2025-03-15T10:32:23Z" level=info msg=foo
122+time="2025-03-15T10:32:24Z" level=info msg=foo
123+ts="2025-03-15T10:32:25Z" level=info msg=foo
124+datetime="2025-03-15T10:32:26Z" level=info msg=foo`)
125 w := &bytes.Buffer{}
126
127 p := parser.NewParser(&config.Config{}, data, w)
128 err := p.Start()
129 require.NoError(t, err)
130
131- assert.Equal(t, `2025-03-15 10:32:23 [INFO]
132-2025-03-15 10:32:24 [INFO]
133-2025-03-15 10:32:25 [INFO]
134-2025-03-15 10:32:26 [INFO]
135+ assert.Equal(t, `2025-03-15 10:32:23 [INFO] msg=foo
136+2025-03-15 10:32:24 [INFO] msg=foo
137+2025-03-15 10:32:25 [INFO] msg=foo
138+2025-03-15 10:32:26 [INFO] msg=foo
139 `, w.String())
140 }
141
142diff --git a/parser/record.go b/parser/record.go
143index d21a08c3eb1b02d3f85deb00c613ab09fa1773ca..3b49fe427e8b80b4c2e10fb8d7f0f659d8f4ef73 100644
144--- a/parser/record.go
145+++ b/parser/record.go
146@@ -143,18 +143,28 @@ func (r *Record) MatchesFilter(filter map[string]string) bool {
147 // String returns a formatted string representation of the Record
148 func (r *Record) String(cfg *config.Config) string {
149 line := ""
150- for _, key := range r.fieldOrder {
151- if len(cfg.OutputFields) > 0 && !slices.Contains(cfg.OutputFields, key) {
152+
153+ // If OutputFields is specified, iterate through it to determine order
154+ outFields := r.fieldOrder
155+ if len(cfg.OutputFields) > 0 {
156+ outFields = cfg.OutputFields
157+ }
158+ for _, key := range outFields {
159+ if len(cfg.ExcludeFields) > 0 && slices.Contains(cfg.ExcludeFields, key) {
160 continue
161 }
162- if len(cfg.ExcludeFields) > 0 && slices.Contains(cfg.ExcludeFields, key) {
163+ value, ok := r.fields[key]
164+ if !ok {
165 continue
166 }
167- value := r.fields[key]
168- key = color.HiBlueString(key)
169+ key := color.HiBlueString(key)
170 line += fmt.Sprintf(" %s=%s", key, getFormattedValue(value))
171 }
172
173+ if line == "" && !cfg.KeepEmpty {
174+ return ""
175+ }
176+
177 var fmtString strings.Builder
178 if !cfg.NoTime {
179 fmtString.WriteString("%s ")