writer.go raw
1 package logbuffer
2
3 import (
4 "bytes"
5 "io"
6 "regexp"
7 "strconv"
8 "strings"
9 "sync"
10 "time"
11 )
12
13 // BufferedWriter wraps an io.Writer and captures log entries
14 type BufferedWriter struct {
15 original io.Writer
16 buffer *Buffer
17 lineBuf bytes.Buffer
18 mu sync.Mutex
19 }
20
21 // Log format regex patterns
22 // lol library format: "1703500000000000âšī¸ message /path/to/file.go:123"
23 // - Unix microseconds timestamp
24 // - Level emoji (â ī¸, đ¨, â ī¸, âšī¸, đ, đģ)
25 // - Message
26 // - File:line location
27 var lolPattern = regexp.MustCompile(`^(\d{16})([â ī¸đ¨â ī¸âšī¸đđģ]+)\s*(.*?)\s+([^\s]+:\d+)$`)
28
29 // Simpler pattern for when emoji detection fails - just capture timestamp and rest
30 var simplePattern = regexp.MustCompile(`^(\d{13,16})\s*(.*)$`)
31
32 // NewBufferedWriter creates a new BufferedWriter
33 func NewBufferedWriter(original io.Writer, buffer *Buffer) *BufferedWriter {
34 return &BufferedWriter{
35 original: original,
36 buffer: buffer,
37 }
38 }
39
40 // Write implements io.Writer
41 func (w *BufferedWriter) Write(p []byte) (n int, err error) {
42 // Always write to original first
43 n, err = w.original.Write(p)
44
45 // Store in buffer if we have one
46 if w.buffer != nil {
47 w.mu.Lock()
48 // Accumulate data in line buffer
49 w.lineBuf.Write(p)
50
51 // Process complete lines
52 var entries []LogEntry
53 for {
54 line, lineErr := w.lineBuf.ReadString('\n')
55 if lineErr != nil {
56 // Put back incomplete line
57 if len(line) > 0 {
58 w.lineBuf.WriteString(line)
59 }
60 break
61 }
62
63 // Parse the complete line
64 entry := w.parseLine(strings.TrimSuffix(line, "\n"))
65 if entry.Message != "" {
66 entries = append(entries, entry)
67 }
68 }
69 w.mu.Unlock()
70
71 // Add entries outside the lock to avoid holding it during buffer.Add
72 for _, entry := range entries {
73 w.buffer.Add(entry)
74 }
75 }
76
77 return
78 }
79
80 // emojiToLevel maps lol library level emojis to level strings
81 var emojiToLevel = map[string]string{
82 "â ī¸": "FTL",
83 "đ¨": "ERR",
84 "â ī¸": "WRN",
85 "âšī¸": "INF",
86 "đ": "DBG",
87 "đģ": "TRC",
88 }
89
90 // parseLine parses a log line into a LogEntry
91 func (w *BufferedWriter) parseLine(line string) LogEntry {
92 entry := LogEntry{
93 Timestamp: time.Now(),
94 Message: line,
95 Level: "INF",
96 }
97
98 line = strings.TrimSpace(line)
99 if line == "" {
100 return entry
101 }
102
103 // Try lol pattern first: "1703500000000000âšī¸ message /path/to/file.go:123"
104 if matches := lolPattern.FindStringSubmatch(line); matches != nil {
105 // Parse Unix microseconds timestamp
106 if usec, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
107 entry.Timestamp = time.UnixMicro(usec)
108 }
109
110 // Map emoji to level
111 if level, ok := emojiToLevel[matches[2]]; ok {
112 entry.Level = level
113 }
114
115 entry.Message = strings.TrimSpace(matches[3])
116
117 // Parse file:line
118 loc := matches[4]
119 if idx := strings.LastIndex(loc, ":"); idx > 0 {
120 entry.File = loc[:idx]
121 if lineNum, err := strconv.Atoi(loc[idx+1:]); err == nil {
122 entry.Line = lineNum
123 }
124 }
125 return entry
126 }
127
128 // Try simple pattern - just grab timestamp and rest as message
129 if matches := simplePattern.FindStringSubmatch(line); matches != nil {
130 if usec, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
131 // Could be microseconds or milliseconds
132 if len(matches[1]) >= 16 {
133 entry.Timestamp = time.UnixMicro(usec)
134 } else {
135 entry.Timestamp = time.UnixMilli(usec)
136 }
137 }
138 rest := strings.TrimSpace(matches[2])
139
140 // Try to detect level from emoji in the rest
141 for emoji, level := range emojiToLevel {
142 if strings.HasPrefix(rest, emoji) {
143 entry.Level = level
144 rest = strings.TrimPrefix(rest, emoji)
145 rest = strings.TrimSpace(rest)
146 break
147 }
148 }
149
150 entry.Message = rest
151 return entry
152 }
153
154 // Fallback: just store the whole line as message
155 entry.Message = line
156 return entry
157 }
158
159 // currentLevel tracks the current log level (string)
160 var currentLevel = "info"
161
162 // GetCurrentLevel returns the current log level string
163 func GetCurrentLevel() string {
164 return currentLevel
165 }
166
167 // SetCurrentLevel sets the current log level and returns it
168 func SetCurrentLevel(level string) string {
169 level = strings.ToLower(level)
170 // Validate level
171 switch level {
172 case "off", "fatal", "error", "warn", "info", "debug", "trace":
173 currentLevel = level
174 default:
175 currentLevel = "info"
176 }
177 return currentLevel
178 }
179
180