Parent directory

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}