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