You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

169 lines
4.0 KiB

package logbuffer
import (
"bytes"
"io"
"regexp"
"strconv"
"strings"
"time"
)
// BufferedWriter wraps an io.Writer and captures log entries
type BufferedWriter struct {
original io.Writer
buffer *Buffer
lineBuf bytes.Buffer
}
// Log format regex patterns
// lol library format: "1703500000000000ℹ message /path/to/file.go:123"
// - Unix microseconds timestamp
// - Level emoji (☠, 🚨, ⚠, ℹ, 🔎, 👻)
// - Message
// - File:line location
var lolPattern = regexp.MustCompile(`^(\d{16})([☠🚨⚠🔎👻]+)\s*(.*?)\s+([^\s]+:\d+)$`)
// Simpler pattern for when emoji detection fails - just capture timestamp and rest
var simplePattern = regexp.MustCompile(`^(\d{13,16})\s*(.*)$`)
// NewBufferedWriter creates a new BufferedWriter
func NewBufferedWriter(original io.Writer, buffer *Buffer) *BufferedWriter {
return &BufferedWriter{
original: original,
buffer: buffer,
}
}
// Write implements io.Writer
func (w *BufferedWriter) Write(p []byte) (n int, err error) {
// Always write to original first
n, err = w.original.Write(p)
// Store in buffer if we have one
if w.buffer != nil {
// Accumulate data in line buffer
w.lineBuf.Write(p)
// Process complete lines
for {
line, lineErr := w.lineBuf.ReadString('\n')
if lineErr != nil {
// Put back incomplete line
if len(line) > 0 {
w.lineBuf.WriteString(line)
}
break
}
// Parse and store the complete line
entry := w.parseLine(strings.TrimSuffix(line, "\n"))
if entry.Message != "" {
w.buffer.Add(entry)
}
}
}
return
}
// emojiToLevel maps lol library level emojis to level strings
var emojiToLevel = map[string]string{
"☠": "FTL",
"🚨": "ERR",
"⚠": "WRN",
"ℹ": "INF",
"🔎": "DBG",
"👻": "TRC",
}
// parseLine parses a log line into a LogEntry
func (w *BufferedWriter) parseLine(line string) LogEntry {
entry := LogEntry{
Timestamp: time.Now(),
Message: line,
Level: "INF",
}
line = strings.TrimSpace(line)
if line == "" {
return entry
}
// Try lol pattern first: "1703500000000000ℹ message /path/to/file.go:123"
if matches := lolPattern.FindStringSubmatch(line); matches != nil {
// Parse Unix microseconds timestamp
if usec, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
entry.Timestamp = time.UnixMicro(usec)
}
// Map emoji to level
if level, ok := emojiToLevel[matches[2]]; ok {
entry.Level = level
}
entry.Message = strings.TrimSpace(matches[3])
// Parse file:line
loc := matches[4]
if idx := strings.LastIndex(loc, ":"); idx > 0 {
entry.File = loc[:idx]
if lineNum, err := strconv.Atoi(loc[idx+1:]); err == nil {
entry.Line = lineNum
}
}
return entry
}
// Try simple pattern - just grab timestamp and rest as message
if matches := simplePattern.FindStringSubmatch(line); matches != nil {
if usec, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
// Could be microseconds or milliseconds
if len(matches[1]) >= 16 {
entry.Timestamp = time.UnixMicro(usec)
} else {
entry.Timestamp = time.UnixMilli(usec)
}
}
rest := strings.TrimSpace(matches[2])
// Try to detect level from emoji in the rest
for emoji, level := range emojiToLevel {
if strings.HasPrefix(rest, emoji) {
entry.Level = level
rest = strings.TrimPrefix(rest, emoji)
rest = strings.TrimSpace(rest)
break
}
}
entry.Message = rest
return entry
}
// Fallback: just store the whole line as message
entry.Message = line
return entry
}
// currentLevel tracks the current log level (string)
var currentLevel = "info"
// GetCurrentLevel returns the current log level string
func GetCurrentLevel() string {
return currentLevel
}
// SetCurrentLevel sets the current log level and returns it
func SetCurrentLevel(level string) string {
level = strings.ToLower(level)
// Validate level
switch level {
case "off", "fatal", "error", "warn", "info", "debug", "trace":
currentLevel = level
default:
currentLevel = "info"
}
return currentLevel
}