Files
next.orly.dev/pkg/logbuffer/writer.go
mleku 678a228fb8
Some checks failed
Go / build-and-release (push) Has been cancelled
Fix log parser to match lol library format (v0.38.1)
The lol library outputs logs in format:
  1703500000000000ℹ️ message /path/to/file.go:123

Where:
- Timestamp is Unix microseconds
- Level is emoji (☠️🚨⚠️ℹ️🔎👻)
- Message text
- File:line location

Updated parser to correctly parse this format.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 14:11:29 +01:00

170 lines
4.0 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}