d11011728a2d4f37ffb4096593e9653a5a687d03

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Implement level and field filtering, and output field selection

Diff

This diff is truncated to protect this page.

  1diff --git a/config/config.go b/config/config.go
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..0b7ab673d6401787bf31bd3b2b3ac134f461baeb
  4--- /dev/null
  5+++ b/config/config.go
  6@@ -0,0 +1,98 @@
  7+package config
  8+
  9+import (
 10+	"fmt"
 11+	"strings"
 12+
 13+	"github.com/go-logfmt/logfmt"
 14+	flags "github.com/jessevdk/go-flags"
 15+)
 16+
 17+const (
 18+	Debug = iota
 19+	Info
 20+	Warning
 21+	Error
 22+	Fatal
 23+)
 24+
 25+type Config struct {
 26+	// LogLevel is the level filter for the log output
 27+	LogLevel int
 28+	// LogFormat is a list of fields to include on the log output
 29+	OutputFields []string
 30+	// Filter is a map of fields and values which are used to filter the log output
 31+	Filter map[string]string
 32+	// Color is a flag to enable or disable color on the output
 33+	Color bool
 34+}
 35+
 36+type rawConfig struct {
 37+	LogLevel     string `long:"level" short:"l" description:"Log level filter. One of DEBUG, INFO, WARN, ERROR, FATAL" default:"INFO"` // nolint:lll
 38+	OutputFields string `long:"output" short:"o" description:"Output field selector (space separated)"`
 39+	Filter       string `long:"filter" short:"f" description:"Filter fields (key=value space separated)"`
 40+	NoColor      bool   `long:"no-color" short:"n" description:"Disable color output"`
 41+}
 42+
 43+func Parse() (*Config, error) {
 44+	var raw rawConfig
 45+
 46+	_, err := flags.Parse(&raw)
 47+	if err != nil {
 48+		return nil, err
 49+	}
 50+
 51+	cfg := Config{
 52+		Color: !raw.NoColor,
 53+	}
 54+	cfg.setOutputFields(raw.OutputFields)
 55+	err = cfg.setFilter(raw.Filter)
 56+	if err != nil {
 57+		return nil, err
 58+	}
 59+	err = cfg.setLevel(raw.LogLevel)
 60+	if err != nil {
 61+		return nil, err
 62+	}
 63+
 64+	return &cfg, nil
 65+}
 66+
 67+func (c *Config) setLevel(level string) error {
 68+	switch level {
 69+	case "DEBUG":
 70+		c.LogLevel = Debug
 71+	case "INFO":
 72+		c.LogLevel = Info
 73+	case "WARN":
 74+		c.LogLevel = Warning
 75+	case "ERROR":
 76+		c.LogLevel = Error
 77+	case "FATAL":
 78+		c.LogLevel = Fatal
 79+	default:
 80+		return fmt.Errorf("invalid log level: %s", level)
 81+	}
 82+	return nil
 83+}
 84+
 85+func (c *Config) setOutputFields(fields string) {
 86+	fields = strings.Trim(fields, " ")
 87+	c.OutputFields = strings.Split(fields, " ")
 88+	if len(c.OutputFields) == 1 && c.OutputFields[0] == "" {
 89+		c.OutputFields = nil
 90+	}
 91+}
 92+
 93+func (c *Config) setFilter(filter string) error {
 94+	dec := logfmt.NewDecoder(strings.NewReader(filter))
 95+	c.Filter = make(map[string]string)
 96+	dec.ScanRecord()
 97+	for dec.ScanKeyval() {
 98+		if dec.Err() != nil {
 99+			return dec.Err()
100+		}
101+		c.Filter[string(dec.Key())] = string(dec.Value())
102+	}
103+	return nil
104+}
105diff --git a/go.mod b/go.mod
106index 031cfca89a1f005ab8044e67a0fe62da86371e75..314db4c0646a7cce491624a52f53465232282863 100644
107--- a/go.mod
108+++ b/go.mod
109@@ -5,6 +5,7 @@ go 1.22.0
110 require (
111 	github.com/fatih/color v1.16.0
112 	github.com/go-logfmt/logfmt v0.6.0
113+	github.com/jessevdk/go-flags v1.5.0
114 )
115 
116 require (
117diff --git a/go.sum b/go.sum
118index 9fad359a24bf325299396920c3de725ae0c5341b..e594d0fb25879ea01ac4554b5b61b727215f5b29 100644
119--- a/go.sum
120+++ b/go.sum
121@@ -2,11 +2,14 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
122 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
123 github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
124 github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
125+github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
126+github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
127 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
128 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
129 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
130 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
131 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
132+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
133 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
134 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
135 golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
136diff --git a/main.go b/main.go
137index 35a979dfa89d8395a798288aded0f72fc82cb569..afa4a0765e3954d304a14e89ae6bddf2bab33ae6 100644
138--- a/main.go
139+++ b/main.go
140@@ -3,88 +3,22 @@ package main
141 import (
142 	"fmt"
143 	"os"
144-	"strconv"
145-	"strings"
146 
147-	"github.com/fatih/color"
148-	"github.com/go-logfmt/logfmt"
149+	"github.com/TheEdgeOfRage/logfmt/config"
150+	"github.com/TheEdgeOfRage/logfmt/parser"
151 )
152 
153-const (
154-	Debug   = "DEBUG"
155-	Info    = "INFO"
156-	Warning = "WARN"
157-	Error   = "ERROR"
158-	Fatal   = "FATAL"
159-)
160-
161-var levelColors map[string]*color.Color
162-
163-func init() {
164-	levelColors = map[string]*color.Color{
165-		Debug:   color.New(color.FgBlack).Add(color.BgCyan).Add(color.Bold),
166-		Info:    color.New(color.FgBlack).Add(color.BgGreen).Add(color.Bold),
167-		Warning: color.New(color.FgBlack).Add(color.BgYellow).Add(color.Bold),
168-		Error:   color.New(color.FgBlack).Add(color.BgRed).Add(color.Bold),
169-		Fatal:   color.New(color.FgBlack).Add(color.BgRed).Add(color.Bold),
170-	}
171-}
172-
173-func isNumeric(s string) bool {
174-	_, err := strconv.ParseFloat(s, 64)
175-	return err == nil
176-}
177-
178-func isBoolean(s string) bool {
179-	_, err := strconv.ParseBool(s)
180-	return err == nil
181-}
182-
183-func isNull(s string) bool {
184-	return s == "null" || s == "NULL" || s == "nil" || s == "<nil>" || s == "None"
185-}
186-
187-func getLevel(level string) string {
188-	level = strings.ToUpper(level)
189-	if level == "WARNING" {
190-		level = Warning
191-	}
192-	val, ok := levelColors[level]
193-	if !ok {
194-		return levelColors[Info].Sprintf("[%s]", Info)
195-	}
196-	return val.Sprintf("[%s]", level)
197-}
198-
199-func getValue(value string) string {
200-	if isNumeric(value) || isBoolean(value) {
201-		return color.MagentaString(value)
202-	}
203-	if isNull(value) {
204-		return color.YellowString(value)
205-	}
206-	return color.HiGreenString(value)
207-}
208-
209 func main() {
210-	decoder := logfmt.NewDecoder(os.Stdin)
211-	for decoder.ScanRecord() {
212-		line := ""
213-		level := Info
214-		timestamp := ""
215-		for decoder.ScanKeyval() {
216-			key, value := string(decoder.Key()), string(decoder.Value())
217-			if key == "level" {
218-				level = getLevel(value)
219-				continue
220-			}
221-			if key == "time" || key == "timestamp" {
222-				timestamp = value
223-				continue
224-			}
225-			line += fmt.Sprintf("%s=%s ", color.HiBlueString(key), getValue(value))
226-		}
227+	cfg, err := config.Parse()
228+	if err != nil {
229+		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
230+		os.Exit(1)
231+	}
232 
233-		fmt.Printf("%s %26s %s\n", timestamp, level, line)
234+	p := parser.NewParser(cfg, os.Stdin, os.Stdout)
235+	err = p.Start()
236+	if err != nil {
237+		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
238+		os.Exit(1)
239 	}
240diff --git a/parser/parser.go b/parser/parser.go
241new file mode 100644
242index 0000000000000000000000000000000000000000..75cc148e190122cd62c009806eb7c4ff0c18295d
243--- /dev/null
244+++ b/parser/parser.go
245@@ -0,0 +1,47 @@
246+package parser
247+
248+import (
249+	"fmt"
250+	"io"
251+
252+	"github.com/TheEdgeOfRage/logfmt/config"
253+	"github.com/go-logfmt/logfmt"
254+)
255+
256+type Parser struct {
257+	cfg    *config.Config
258+	input  io.Reader
259+	output io.Writer
260+}
261+
262+func NewParser(cfg *config.Config, input io.Reader, output io.Writer) *Parser {
263+	return &Parser{
264+		cfg:    cfg,
265+		input:  input,
266+		output: output,
267+	}
268+}
269+
270+// Start starts the parser, reading from the input stream and printing the log output to the output stream line by line
271+func (p *Parser) Start() error {
272+	decoder := logfmt.NewDecoder(p.input)
273+	for decoder.ScanRecord() {
274+		record, err := NewRecord(decoder)
275+		if err != nil {
276+			return err
277+		}
278+		if record.level < p.cfg.LogLevel {
279+			continue
280+		}
281+		if len(p.cfg.Filter) > 0 && !record.MatchesFilter(p.cfg.Filter) {
282+			continue
283+		}
284+
285+		_, err = fmt.Fprintf(p.output, "%s\n", record.String(p.cfg.OutputFields, p.cfg.Color))
286+		if err != nil {
287+			return fmt.Errorf("failed to print log to output: %w", err)
288+		}
289+	}
290+
291+	return nil
292+}
293diff --git a/parser/record.go b/parser/record.go
294new file mode 100644
295index 0000000000000000000000000000000000000000..1e4da18aa5c7cb7e6d2f30cf1b13ef3b0781f211
296--- /dev/null
297+++ b/parser/record.go
298@@ -0,0 +1,170 @@
299+package parser
300+
301+import (
302+	"fmt"
303+	"slices"
304+	"strconv"
305+	"strings"
306+	"time"
307+
308+	"github.com/TheEdgeOfRage/logfmt/config"
309+	"github.com/fatih/color"
310+	"github.com/go-logfmt/logfmt"
311+)
312+
313+var (
314+	levelStrings map[int]string
315+	levelColors  map[int]*color.Color
316+)
317+
318+func init() {
319+	levelColors = map[int]*color.Color{
320+		config.Debug:   color.New(color.FgBlack).Add(color.BgCyan).Add(color.Bold),
321+		config.Info:    color.New(color.FgBlack).Add(color.BgGreen).Add(color.Bold),
322+		config.Warning: color.New(color.FgBlack).Add(color.BgYellow).Add(color.Bold),
323+		config.Error:   color.New(color.FgBlack).Add(color.BgRed).Add(color.Bold),
324+		config.Fatal:   color.New(color.FgRed).Add(color.BgBlack).Add(color.Bold),
325+	}
326+	levelStrings = map[int]string{
327+		config.Debug:   "DEBUG",
328+		config.Info:    "INFO",
329+		config.Warning: "WARN",
330+		config.Error:   "ERROR",
331+		config.Fatal:   "FATAL",
332+	}
333+}
334+
335+// Record represents a single log line
336+type Record struct {
337+	level      int
338+	time       time.Time
339+	fields     map[string]string
340+	fieldOrder []string
341+}
342+
343+// NewRecord parses a new Record from a logfmt.Decoder
344+func NewRecord(decoder *logfmt.Decoder) (*Record, error) {
345+	var record Record
346+	record.fields = make(map[string]string)
347+	record.fieldOrder = make([]string, 0)
348+
349+	for decoder.ScanKeyval() {
350+		err := decoder.Err()
351+		if err != nil {
352+			return nil, fmt.Errorf("failed to parse log line: %w", decoder.Err())
353+		}
354+		key, value := string(decoder.Key()), string(decoder.Value())
355+		if key == "level" {
356+			record.parseLevel(value)
357+			continue
358+		}
359+		if key == "time" || key == "timestamp" {
360+			err := record.parseTime(value)
361+			if err != nil {
362+				return nil, err
363+			}
364+			continue
365+		}
366+		record.fields[key] = value
367+		record.fieldOrder = append(record.fieldOrder, key)
368+	}
369+
370+	return &record, nil
371+}
372+
373+func isNumeric(s string) bool {
374+	_, err := strconv.ParseFloat(s, 64)
375+	return err == nil
376+}
377+
378+func isBoolean(s string) bool {
379+	_, err := strconv.ParseBool(s)
380+	return err == nil
381+}
382+
383+func isNull(s string) bool {
384+	return s == "null" || s == "NULL" || s == "nil" || s == "<nil>" || s == "None"
385+}
386+
387+func getFormattedValue(value string, colorize bool) string {
388+	if strings.Contains(value, " ") {
389+		value = fmt.Sprintf(`"%s"`, value)
390+	}
391+	if !colorize {
392+		return value
393+	}
394+	if isNumeric(value) || isBoolean(value) {
395+		return color.MagentaString(value)
396+	}
397+	if isNull(value) {