ffa4b0163990225a48211b3c1c6178cd242d0492

Author
td-jizdebski <jakub.izdebski@jrdltd.co.uk>
Committer
GitHub <noreply@github.com>
Date

Message

Add option to skip empty and enforce fields order (#9)

* add option to skip empty and enforce fields order

* readme update

* fix indent

* naming consistency

* drop backwards compatibilty

* readme fix

* fix comment

* Simplify output logic

* Fix tests

---------

Co-authored-by: TheEdgeOfRage <git@theedgeofrage.com>

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 ")