record.go
4877 bytes
1package parser
2
3import (
4 "fmt"
5 "slices"
6 "strconv"
7 "strings"
8 "time"
9
10 "github.com/fatih/color"
11 "github.com/go-logfmt/logfmt"
12
13 "github.com/TheEdgeOfRage/logfmt/config"
14)
15
16var (
17 levelStrings map[int]string
18 levelColors map[int]*color.Color
19 timestampLabels = []string{"time", "timestamp", "datetime", "ts", "t"}
20)
21
22func init() {
23 levelColors = map[int]*color.Color{
24 config.Trace: color.New(color.FgBlack).Add(color.BgHiBlue).Add(color.Bold),
25 config.Debug: color.New(color.FgBlack).Add(color.BgCyan).Add(color.Bold),
26 config.Info: color.New(color.FgBlack).Add(color.BgGreen).Add(color.Bold),
27 config.Warning: color.New(color.FgBlack).Add(color.BgYellow).Add(color.Bold),
28 config.Error: color.New(color.FgBlack).Add(color.BgRed).Add(color.Bold),
29 config.Fatal: color.New(color.FgRed).Add(color.BgBlack).Add(color.Bold),
30 }
31 levelStrings = map[int]string{
32 config.Trace: "TRACE",
33 config.Debug: "DEBUG",
34 config.Info: "INFO",
35 config.Warning: "WARN",
36 config.Error: "ERROR",
37 config.Fatal: "FATAL",
38 }
39}
40
41// Record represents a single log line
42type Record struct {
43 level int
44 time time.Time
45 fields map[string]string
46 fieldOrder []string
47}
48
49// NewRecord parses a new Record from a logfmt.Decoder
50func NewRecord(decoder *logfmt.Decoder, cfg *config.Config) (*Record, error) {
51 var record Record
52 record.fields = make(map[string]string)
53 record.fieldOrder = make([]string, 0)
54
55 for decoder.ScanKeyval() {
56 err := decoder.Err()
57 if err != nil {
58 return nil, fmt.Errorf("failed to parse log line: %w", decoder.Err())
59 }
60 key, value := string(decoder.Key()), string(decoder.Value())
61 if key == "level" {
62 record.parseLevel(value)
63 if !cfg.Raw {
64 continue
65 }
66 }
67 if slices.Contains(timestampLabels, key) {
68 err := record.parseTime(value)
69 if err != nil {
70 return nil, err
71 }
72 if !cfg.Raw {
73 continue
74 }
75 }
76 record.fields[key] = value
77 record.fieldOrder = append(record.fieldOrder, key)
78 }
79
80 return &record, nil
81}
82
83func isNumeric(s string) bool {
84 _, err := strconv.ParseFloat(s, 64)
85 return err == nil
86}
87
88func isBoolean(s string) bool {
89 _, err := strconv.ParseBool(s)
90 return err == nil
91}
92
93func isNull(s string) bool {
94 return s == "null" || s == "NULL" || s == "nil" || s == "<nil>" || s == "None"
95}
96
97func getFormattedValue(value string) string {
98 if isNumeric(value) || isBoolean(value) {
99 return color.MagentaString(value)
100 }
101 if isNull(value) {
102 return color.YellowString(value)
103 }
104 if strings.Contains(value, " ") {
105 value = fmt.Sprintf(`"%s"`, value)
106 }
107 return color.HiGreenString(value)
108}
109
110func getFormattedLevel(level int) string {
111 return levelColors[level].Sprintf("[%s]", levelStrings[level])
112}
113
114func (r *Record) parseLevel(level string) {
115 switch strings.ToUpper(level) {
116 case "TRACE":
117 r.level = config.Trace
118 case "DBUG", "DEBUG":
119 r.level = config.Debug
120 case "INFO", "INFORMATION", "INFORMATIONAL", "NOTICE":
121 r.level = config.Info
122 case "WARN", "WARNING":
123 r.level = config.Warning
124 case "ERR", "EROR", "ERROR":
125 r.level = config.Error
126 case "EMERG", "FATAL", "ALERT", "CRIT", "CRITICAL":
127 r.level = config.Fatal
128 default:
129 r.level = config.Info
130 }
131}
132
133func (r *Record) parseTime(timeStr string) error {
134 t, err := time.Parse(time.RFC3339, timeStr)
135 if err != nil {
136 return fmt.Errorf("failed to parse timestamp: %w", err)
137 }
138
139 r.time = t
140 return nil
141}
142
143func (r *Record) MatchesFilter(filter map[string]string) bool {
144 for key, value := range filter {
145 if r.fields[key] != value {
146 return false
147 }
148 }
149 return true
150}
151
152// String returns a formatted string representation of the Record
153func (r *Record) String(cfg *config.Config) string {
154 line := ""
155
156 outFields := r.fieldOrder
157 if len(cfg.OutputFields) > 0 {
158 if cfg.All {
159 reorderedFields := cfg.OutputFields
160
161 for _, key := range r.fieldOrder {
162 if !slices.Contains(reorderedFields, key) {
163 reorderedFields = append(reorderedFields, key)
164 }
165 }
166 outFields = reorderedFields
167 } else {
168 outFields = cfg.OutputFields
169 }
170 }
171
172 for _, key := range outFields {
173 if len(cfg.ExcludeFields) > 0 && slices.Contains(cfg.ExcludeFields, key) {
174 continue
175 }
176 value, ok := r.fields[key]
177 if !ok {
178 continue
179 }
180 key := color.HiBlueString(key)
181 if cfg.Raw {
182 line += fmt.Sprintf(" %s", value)
183 } else {
184 line += fmt.Sprintf(" %s=%s", key, getFormattedValue(value))
185 }
186 }
187
188 if line == "" && !cfg.KeepEmpty {
189 return ""
190 }
191
192 if cfg.Raw {
193 return strings.TrimSpace(line)
194 }
195
196 var fmtString strings.Builder
197 if !cfg.NoTime {
198 fmtString.WriteString("%s ")
199 }
200
201 if color.NoColor {
202 fmtString.WriteString("%7s%s")
203 } else {
204 fmtString.WriteString("%26s%s")
205 }
206
207 if cfg.NoTime {
208 return fmt.Sprintf(fmtString.String(), getFormattedLevel(r.level), line)
209 } else {
210 return fmt.Sprintf(fmtString.String(), r.time.Format("2006-01-02 15:04:05"), getFormattedLevel(r.level), line)
211 }
212}