esync

Directory watching and remote syncing
Log | Files | Refs | README | LICENSE

logger.go (3055B)


      1 package logger
      2 
      3 import (
      4 	"encoding/json"
      5 	"fmt"
      6 	"os"
      7 	"sort"
      8 	"sync"
      9 	"time"
     10 )
     11 
     12 // Logger writes structured log entries to a file in either JSON or text format.
     13 type Logger struct {
     14 	file   *os.File
     15 	format string // "json" or "text"
     16 	mu     sync.Mutex
     17 }
     18 
     19 // New opens (or creates) a log file at path for append-only writing and returns
     20 // a Logger. The format parameter must be "json" or "text"; an empty string
     21 // defaults to "text".
     22 func New(path string, format string) (*Logger, error) {
     23 	if format == "" {
     24 		format = "text"
     25 	}
     26 
     27 	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
     28 	if err != nil {
     29 		return nil, fmt.Errorf("logger: open %s: %w", path, err)
     30 	}
     31 
     32 	return &Logger{
     33 		file:   f,
     34 		format: format,
     35 	}, nil
     36 }
     37 
     38 // Close closes the underlying log file.
     39 func (l *Logger) Close() error {
     40 	l.mu.Lock()
     41 	defer l.mu.Unlock()
     42 	return l.file.Close()
     43 }
     44 
     45 // Info logs an info-level entry.
     46 func (l *Logger) Info(event string, fields map[string]interface{}) {
     47 	l.log("info", event, fields)
     48 }
     49 
     50 // Warn logs a warn-level entry.
     51 func (l *Logger) Warn(event string, fields map[string]interface{}) {
     52 	l.log("warn", event, fields)
     53 }
     54 
     55 // Error logs an error-level entry.
     56 func (l *Logger) Error(event string, fields map[string]interface{}) {
     57 	l.log("error", event, fields)
     58 }
     59 
     60 // Debug logs a debug-level entry.
     61 func (l *Logger) Debug(event string, fields map[string]interface{}) {
     62 	l.log("debug", event, fields)
     63 }
     64 
     65 // levelTag maps internal level names to short text-format tags.
     66 var levelTag = map[string]string{
     67 	"info":  "INF",
     68 	"warn":  "WRN",
     69 	"error": "ERR",
     70 	"debug": "DBG",
     71 }
     72 
     73 // log writes a single log entry in the configured format.
     74 func (l *Logger) log(level, event string, fields map[string]interface{}) {
     75 	l.mu.Lock()
     76 	defer l.mu.Unlock()
     77 
     78 	ts := time.Now().Format("15:04:05")
     79 
     80 	switch l.format {
     81 	case "json":
     82 		l.writeJSON(ts, level, event, fields)
     83 	default:
     84 		l.writeText(ts, level, event, fields)
     85 	}
     86 }
     87 
     88 // writeJSON writes a single JSON log line.
     89 func (l *Logger) writeJSON(ts, level, event string, fields map[string]interface{}) {
     90 	entry := make(map[string]interface{}, len(fields)+3)
     91 	entry["time"] = ts
     92 	entry["level"] = level
     93 	entry["event"] = event
     94 	for k, v := range fields {
     95 		entry[k] = v
     96 	}
     97 
     98 	data, err := json.Marshal(entry)
     99 	if err != nil {
    100 		// Best-effort fallback: write the error itself.
    101 		fmt.Fprintf(l.file, `{"time":%q,"level":"error","event":"log_marshal_error","error":%q}`+"\n", ts, err.Error())
    102 		return
    103 	}
    104 	l.file.Write(data)
    105 	l.file.Write([]byte("\n"))
    106 }
    107 
    108 // writeText writes a single text log line in the format:
    109 //
    110 //	15:04:05 INF event key=value key2=value2
    111 func (l *Logger) writeText(ts, level, event string, fields map[string]interface{}) {
    112 	tag := levelTag[level]
    113 
    114 	// Sort field keys for deterministic output.
    115 	keys := make([]string, 0, len(fields))
    116 	for k := range fields {
    117 		keys = append(keys, k)
    118 	}
    119 	sort.Strings(keys)
    120 
    121 	line := fmt.Sprintf("%s %s %s", ts, tag, event)
    122 	for _, k := range keys {
    123 		line += fmt.Sprintf(" %s=%v", k, fields[k])
    124 	}
    125 	line += "\n"
    126 
    127 	l.file.WriteString(line)
    128 }