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 }