Files
Erupe/tools/loganalyzer/parser.go
2026-02-06 13:02:38 +01:00

320 lines
9.0 KiB
Go

// Package main provides a comprehensive suite of tools for analyzing Erupe server logs.
//
// The log analyzer supports both JSON-formatted logs and tab-delimited timestamp logs,
// providing commands for filtering, error analysis, connection tracking, statistics
// generation, and real-time log following.
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"strings"
"time"
)
// LogEntry represents a parsed log entry from either JSON or timestamp-based format.
//
// The parser supports two log formats:
// 1. JSON format: {"level":"info","ts":1762989571.547817,"logger":"main","msg":"Starting"}
// 2. Timestamp format: 2025-11-12T23:19:31.546Z INFO commands Command Help: Enabled
type LogEntry struct {
Raw string // Original log line
Level string // Log level: info, warn, error, fatal
Timestamp time.Time // Parsed timestamp
Logger string // Logger name
Caller string // Caller file:line
Message string // Log message
Error string // Error message (if present)
StackTrace string // Stack trace (if present)
ExtraData map[string]interface{} // Additional fields
IsJSON bool // True if parsed from JSON format
}
// ParseLogFile reads and parses an entire log file into memory.
//
// This function loads all log entries into memory and is suitable for smaller log files
// or when random access to entries is needed. For large files or streaming operations,
// use StreamLogFile instead.
//
// The function automatically handles both JSON and timestamp-based log formats,
// skips empty lines and "nohup: ignoring input" messages, and uses a large buffer
// (1MB) to handle long lines like stack traces.
//
// Parameters:
// - filename: Path to the log file to parse
//
// Returns:
// - A slice of LogEntry pointers containing all parsed entries
// - An error if the file cannot be opened or read
//
// Example:
//
// entries, err := ParseLogFile("erupe.log")
// if err != nil {
// log.Fatal(err)
// }
// fmt.Printf("Parsed %d entries\n", len(entries))
func ParseLogFile(filename string) ([]*LogEntry, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
defer file.Close()
var entries []*LogEntry
scanner := bufio.NewScanner(file)
// Increase buffer size for long lines (like stack traces)
const maxCapacity = 1024 * 1024 // 1MB
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
for scanner.Scan() {
line := scanner.Text()
if line == "" || line == "nohup: ignoring input" {
continue
}
entry := ParseLogLine(line)
if entry != nil {
entries = append(entries, entry)
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading log file: %w", err)
}
return entries, nil
}
// ParseLogLine parses a single log line into a LogEntry.
//
// This function attempts to parse the line in the following order:
// 1. JSON format: Lines starting with '{' are parsed as JSON objects
// 2. Timestamp format: Tab-delimited lines with RFC3339 timestamps
// 3. Unknown format: Lines that don't match either format are marked as "unknown" level
//
// For JSON logs, all standard fields (level, ts, logger, caller, msg, error, stacktrace)
// are extracted, and any additional fields are stored in ExtraData.
//
// Parameters:
// - line: A single line from the log file
//
// Returns:
// - A LogEntry pointer containing the parsed data, or nil if the line is invalid
//
// Example:
//
// entry := ParseLogLine(`{"level":"info","ts":1762989571.547817,"msg":"Starting"}`)
// fmt.Println(entry.Level, entry.Message)
func ParseLogLine(line string) *LogEntry {
entry := &LogEntry{
Raw: line,
ExtraData: make(map[string]interface{}),
}
// Try parsing as JSON first
if strings.HasPrefix(line, "{") {
var jsonData map[string]interface{}
if err := json.Unmarshal([]byte(line), &jsonData); err == nil {
entry.IsJSON = true
// Extract standard fields
if level, ok := jsonData["level"].(string); ok {
entry.Level = level
}
if ts, ok := jsonData["ts"].(float64); ok {
entry.Timestamp = time.Unix(int64(ts), int64((ts-float64(int64(ts)))*1e9))
}
if logger, ok := jsonData["logger"].(string); ok {
entry.Logger = logger
}
if caller, ok := jsonData["caller"].(string); ok {
entry.Caller = caller
}
if msg, ok := jsonData["msg"].(string); ok {
entry.Message = msg
}
if errMsg, ok := jsonData["error"].(string); ok {
entry.Error = errMsg
}
if stackTrace, ok := jsonData["stacktrace"].(string); ok {
entry.StackTrace = stackTrace
}
// Store any extra fields
for k, v := range jsonData {
if k != "level" && k != "ts" && k != "logger" && k != "caller" &&
k != "msg" && k != "error" && k != "stacktrace" {
entry.ExtraData[k] = v
}
}
return entry
}
}
// Try parsing as timestamp-based log (2025-11-12T23:19:31.546Z INFO commands ...)
parts := strings.SplitN(line, "\t", 4)
if len(parts) >= 3 {
// Parse timestamp
if ts, err := time.Parse(time.RFC3339Nano, parts[0]); err == nil {
entry.Timestamp = ts
entry.Level = strings.ToLower(parts[1])
entry.Logger = parts[2]
if len(parts) == 4 {
entry.Message = parts[3]
}
return entry
}
}
// If we can't parse it, return a basic entry
entry.Level = "unknown"
entry.Message = line
return entry
}
// StreamLogFile reads a log file line by line and calls the callback for each entry.
//
// This function is memory-efficient and suitable for processing large log files as it
// processes entries one at a time without loading the entire file into memory. The
// callback function is called for each successfully parsed log entry.
//
// The function uses a 1MB buffer to handle long lines such as those containing stack traces.
// Empty lines and "nohup: ignoring input" messages are automatically skipped.
//
// If the callback returns an error, processing stops immediately and that error is returned.
//
// Parameters:
// - filename: Path to the log file to process
// - callback: Function to call for each parsed LogEntry
//
// Returns:
// - An error if the file cannot be opened, read, or if the callback returns an error
//
// Example:
//
// err := StreamLogFile("erupe.log", func(entry *LogEntry) error {
// if entry.Level == "error" {
// fmt.Println(entry.Message)
// }
// return nil
// })
func StreamLogFile(filename string, callback func(*LogEntry) error) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open log file: %w", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
// Increase buffer size for long lines
const maxCapacity = 1024 * 1024 // 1MB
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
for scanner.Scan() {
line := scanner.Text()
if line == "" || line == "nohup: ignoring input" {
continue
}
entry := ParseLogLine(line)
if entry != nil {
if err := callback(entry); err != nil {
return err
}
}
}
return scanner.Err()
}
// FormatLogEntry formats a log entry for human-readable display.
//
// The output format is: "TIMESTAMP LEVEL [LOGGER] MESSAGE key=value ..."
//
// Timestamps are formatted as "2006-01-02 15:04:05.000". Log levels can be colorized
// for terminal display:
// - Errors (error, fatal, panic): Red
// - Warnings: Yellow
// - Info: Green
//
// If the entry contains an error message, it's appended as error="message".
// Any extra fields in ExtraData are appended as key=value pairs.
//
// Parameters:
// - entry: The LogEntry to format
// - colorize: Whether to add ANSI color codes for terminal display
//
// Returns:
// - A formatted string representation of the log entry
//
// Example:
//
// formatted := FormatLogEntry(entry, true)
// fmt.Println(formatted)
// // Output: 2025-11-12 23:19:31.546 INFO [main] Starting Erupe
func FormatLogEntry(entry *LogEntry, colorize bool) string {
var sb strings.Builder
// Format timestamp
if !entry.Timestamp.IsZero() {
sb.WriteString(entry.Timestamp.Format("2006-01-02 15:04:05.000"))
sb.WriteString(" ")
}
// Format level with colors
levelStr := strings.ToUpper(entry.Level)
if colorize {
switch entry.Level {
case "error", "fatal", "panic":
levelStr = fmt.Sprintf("\033[31m%s\033[0m", levelStr) // Red
case "warn":
levelStr = fmt.Sprintf("\033[33m%s\033[0m", levelStr) // Yellow
case "info":
levelStr = fmt.Sprintf("\033[32m%s\033[0m", levelStr) // Green
}
}
sb.WriteString(fmt.Sprintf("%-5s ", levelStr))
// Format logger
if entry.Logger != "" {
sb.WriteString(fmt.Sprintf("[%s] ", entry.Logger))
}
// Format message
sb.WriteString(entry.Message)
// Add error if present
if entry.Error != "" {
sb.WriteString(fmt.Sprintf(" error=%q", entry.Error))
}
// Add extra data
if len(entry.ExtraData) > 0 {
sb.WriteString(" ")
first := true
for k, v := range entry.ExtraData {
if !first {
sb.WriteString(" ")
}
sb.WriteString(fmt.Sprintf("%s=%v", k, v))
first = false
}
}
return sb.String()
}