package sneklog import ( "fmt" "strings" "time" ) // Formatter configures how log records are rendered. // // Format tokens: // - %t: time // - %N: name // - %l: level // - %L: uppercase level // - %b: first traceback frame // - %B: full traceback // - %m: message // - %M: method // - %f: filename // - %n: line number // - %s: method signature // - %p: full path // // Example format: "%t [%l] %N: %m (%f:%n %s)" // // TimeStampFormat uses strftime-like directives such as %Y, %m, %d, %H, %M, // %S, %z, and %Z. // // TraceBackFormat supports the traceback-related tokens %M, %f, %n, %s, and %p. type Formatter struct { Format string MessageSeparator string // JSONTraceBackPath enables full file paths in formatted traceback fields. JSONTraceBackPath bool // JSONEscapeHTML controls whether JSON output escapes HTML-sensitive characters. JSONEscapeHTML bool TimeStampFormat string TraceBackFormat string TraceBackSeparator string ColorOutput bool ColorOnlyStdout bool } // DefaultTextFormatter is the default formatter used by text writers. var DefaultTextFormatter = &Formatter{ Format: "%t %l %N: %m", MessageSeparator: " ", TimeStampFormat: RFC3339, TraceBackFormat: "%M (%f:%n %s)", TraceBackSeparator: "->", ColorOutput: true, ColorOnlyStdout: true, } // DefaultJsonFormatter is the default formatter used by JSON writers. var DefaultJsonFormatter = &Formatter{ Format: "%m", JSONEscapeHTML: true, MessageSeparator: " ", TimeStampFormat: RFC3339, ColorOutput: false, } // NewFormatter returns a copy of the default text formatter settings. func NewFormatter() *Formatter { return &Formatter{ Format: DefaultTextFormatter.Format, MessageSeparator: DefaultTextFormatter.MessageSeparator, JSONEscapeHTML: true, TimeStampFormat: DefaultTextFormatter.TimeStampFormat, TraceBackFormat: DefaultTextFormatter.TraceBackFormat, TraceBackSeparator: DefaultTextFormatter.TraceBackSeparator, ColorOutput: DefaultTextFormatter.ColorOutput, ColorOnlyStdout: DefaultTextFormatter.ColorOnlyStdout, } } // SetFormat sets the formatter template used for text rendering. func (f *Formatter) SetFormat(format string) *Formatter { f.Format = format return f } // SetMessageSeparator sets the separator used to join message parts. func (f *Formatter) SetMessageSeparator(separator string) *Formatter { f.MessageSeparator = separator return f } // SetJSONTraceBackPath enables or disables full file paths in formatted traceback fields. func (f *Formatter) SetJSONTraceBackPath(set bool) *Formatter { f.JSONTraceBackPath = set return f } // SetJSONEscapeHTML enables or disables HTML escaping in JSON output. func (f *Formatter) SetJSONEscapeHTML(escape bool) *Formatter { f.JSONEscapeHTML = escape return f } // SetTimeStampFormat sets the strftime-like format used for timestamps. func (f *Formatter) SetTimeStampFormat(format string) *Formatter { f.TimeStampFormat = format return f } // SetTraceBackFormat sets the format used for a single traceback frame. func (f *Formatter) SetTraceBackFormat(format string) *Formatter { f.TraceBackFormat = format return f } // SetTraceBackSeparator sets the separator used between traceback frames. func (f *Formatter) SetTraceBackSeparator(separator string) *Formatter { f.TraceBackSeparator = separator return f } // SetColorOutput enables or disables colorized output. func (f *Formatter) SetColorOutput(color bool) *Formatter { f.ColorOutput = color return f } // SetColorOnlyStdout restricts colorized output to stdout and stderr when enabled. func (f *Formatter) SetColorOnlyStdout(only bool) *Formatter { f.ColorOnlyStdout = only return f } // FormatMessage applies the formatter template to the provided log record fields. func (f *Formatter) FormatMessage(level LogLevel, prefix string, tb []*MethodTraceback, messages ...any) string { if f == nil { return fmt.Sprint(messages...) } output := f.Format output = strings.ReplaceAll(output, "%t", f.FormatTime(time.Now())) output = strings.ReplaceAll(output, "%N", prefix) output = strings.ReplaceAll(output, "%l", level.GetName()) output = strings.ReplaceAll(output, "%L", strings.ToUpper(level.GetName())) if len(tb) > 0 { formattedTraceback := f.FormatTraceback(tb[0]) output = strings.ReplaceAll(output, "%b", formattedTraceback) formattedTraceback = f.FormatTracebacks(tb) output = strings.ReplaceAll(output, "%B", formattedTraceback) } else { output = strings.ReplaceAll(output, "%b", "") output = strings.ReplaceAll(output, "%B", "") } if len(tb) > 0 { output = strings.ReplaceAll(output, "%M", tb[0].Method) output = strings.ReplaceAll(output, "%f", tb[0].Filename) output = strings.ReplaceAll(output, "%n", fmt.Sprintf("%d", tb[0].Line)) output = strings.ReplaceAll(output, "%s", tb[0].Signature) } else { output = strings.ReplaceAll(output, "%M", "") output = strings.ReplaceAll(output, "%f", "") output = strings.ReplaceAll(output, "%n", "") output = strings.ReplaceAll(output, "%s", "") } if f.JSONTraceBackPath && len(tb) > 0 { output = strings.ReplaceAll(output, "%p", tb[0].FullPath) } else { output = strings.ReplaceAll(output, "%p", "") } message := Map(messages, func(m any) string { return fmt.Sprint(m) }) output = strings.ReplaceAll(output, "%m", strings.Join(message, f.MessageSeparator)) return output } // ColorizeString wraps a string in the ANSI color sequences defined by the level. func (f *Formatter) ColorizeString(s string, level LogLevel) string { builder := NewColorStringBuilder() if level.fgRgb != nil { builder.AddForegroundRGB(level.fgRgb) } else if level.fg256 > 0 { builder.AddForeground256Color(level.fg256) } else { builder.AddFgColor(level.fg) } if level.bgRgb != nil { builder.AddBackgroundRGB(level.bgRgb) } else if level.bg256 > 0 { builder.AddBackground256Color(level.bg256) } else { builder.AddBgColor(level.bg) } for _, attr := range level.attrs { builder.AddAttribute(attr) } return builder.AddText(s).AddReset().String() } // FormatTime formats a timestamp using the formatter timestamp layout. func (f *Formatter) FormatTime(t time.Time) string { if f.TimeStampFormat == "" { return t.Format(time.RFC3339) } return t.Format(StrftimeToGo(f.TimeStampFormat)) } // FormatTraceback formats a single traceback frame. func (f *Formatter) FormatTraceback(traceback *MethodTraceback) string { formattedTraceback := f.TraceBackFormat if f.JSONTraceBackPath { formattedTraceback = strings.ReplaceAll(formattedTraceback, "%p", traceback.FullPath) } else { formattedTraceback = strings.ReplaceAll(formattedTraceback, "%p", "") } formattedTraceback = strings.ReplaceAll(formattedTraceback, "%M", traceback.Method) formattedTraceback = strings.ReplaceAll(formattedTraceback, "%f", traceback.Filename) formattedTraceback = strings.ReplaceAll(formattedTraceback, "%n", fmt.Sprintf("%d", traceback.Line)) formattedTraceback = strings.ReplaceAll(formattedTraceback, "%s", traceback.Signature) return formattedTraceback } // FormatTracebacks formats and joins multiple traceback frames. func (f *Formatter) FormatTracebacks(traceback []*MethodTraceback) string { var formattedTraceback []string for _, frame := range traceback { formattedTraceback = append(formattedTraceback, f.FormatTraceback(frame)) } return strings.Join(formattedTraceback, f.TraceBackSeparator) }