d11011728a2d4f37ffb4096593e9653a5a687d03
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
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) {