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 }