-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlog.go
More file actions
436 lines (385 loc) · 11.1 KB
/
log.go
File metadata and controls
436 lines (385 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
package logary
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
)
// Level defines the log level.
type Level int
const (
DebugLevel Level = iota
InfoLevel
WarnLevel
ErrorLevel
)
// String returns the string representation of a log level.
func (l Level) String() string {
switch l {
case DebugLevel:
return "DEBUG"
case InfoLevel:
return "INFO"
case WarnLevel:
return "WARN"
case ErrorLevel:
return "ERROR"
default:
return "UNKNOWN"
}
}
// CustomLogger defines the logger interface.
type CustomLogger interface {
Debug(args ...interface{})
Info(args ...interface{})
Warn(args ...interface{})
Error(args ...interface{})
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Warnf(format string, args ...interface{})
Errorf(format string, args ...interface{})
// New methods for structured byte data
DebugJSON(data []byte)
InfoJSON(data []byte)
WarnJSON(data []byte)
ErrorJSON(data []byte)
}
// logEntry represents a single log message.
type logEntry struct {
level Level
time time.Time
caller string
message string
rawData []byte // New field for raw structured data
}
// Logger is a thread-safe, asynchronous logger.
type Logger struct {
// --- Atomic/Immutable fields (set at creation) ---
filename string
structured bool
level Level
maxSize int64 // in bytes
maxBackups int
logChan chan logEntry
doneChan chan struct{}
wg sync.WaitGroup
// --- Fields protected by the internal goroutine ---
file *os.File
writer *bufio.Writer
}
// Config stores logger configuration.
type Config struct {
Filename string // Log filename
Structured bool // Use JSON format
Level Level // Minimum log level
MaxSizeMB int // Max size in MB before rotation
MaxBackups int // Max number of old log files to keep
BufferSize int // Size of the internal log channel
FlushFreqMS int // How often to flush to disk (in ms)
}
// NewLogger creates a new asynchronous logger instance.
func NewLogger(config Config) (*Logger, error) {
file, err := os.OpenFile(config.Filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open log file %s: %w", config.Filename, err)
}
// Default values
if config.MaxSizeMB <= 0 {
config.MaxSizeMB = 10
}
if config.MaxBackups < 0 {
config.MaxBackups = 3
}
if config.BufferSize <= 0 {
config.BufferSize = 1024 // 1024 buffered log messages
}
if config.FlushFreqMS <= 0 {
config.FlushFreqMS = 1000 // 1 second
}
logger := &Logger{
file: file,
writer: bufio.NewWriter(file),
filename: config.Filename,
structured: config.Structured,
level: config.Level,
maxSize: int64(config.MaxSizeMB) * 1024 * 1024,
maxBackups: config.MaxBackups,
logChan: make(chan logEntry, config.BufferSize),
doneChan: make(chan struct{}),
}
// Start the background logging goroutine
logger.wg.Add(1)
go logger.run(time.Duration(config.FlushFreqMS) * time.Millisecond)
return logger, nil
}
// --- Public Log Methods ---
func (l *Logger) Debug(args ...interface{}) {
l.log(DebugLevel, fmt.Sprint(args...))
}
func (l *Logger) Info(args ...interface{}) {
l.log(InfoLevel, fmt.Sprint(args...))
}
func (l *Logger) Warn(args ...interface{}) {
l.log(WarnLevel, fmt.Sprint(args...))
}
func (l *Logger) Error(args ...interface{}) {
l.log(ErrorLevel, fmt.Sprint(args...))
}
func (l *Logger) Debugf(format string, args ...interface{}) {
l.log(DebugLevel, fmt.Sprintf(format, args...))
}
func (l *Logger) Infof(format string, args ...interface{}) {
l.log(InfoLevel, fmt.Sprintf(format, args...))
}
func (l *Logger) Warnf(format string, args ...interface{}) {
l.log(WarnLevel, fmt.Sprintf(format, args...))
}
func (l *Logger) Errorf(format string, args ...interface{}) {
l.log(ErrorLevel, fmt.Sprintf(format, args...))
}
// --- New Public JSON Methods ---
func (l *Logger) DebugJSON(data []byte) {
l.logJSON(DebugLevel, data)
}
func (l *Logger) InfoJSON(data []byte) {
l.logJSON(InfoLevel, data)
}
func (l *Logger) WarnJSON(data []byte) {
l.logJSON(WarnLevel, data)
}
func (l *Logger) ErrorJSON(data []byte) {
l.logJSON(ErrorLevel, data)
}
// --- Core Logic ---
// log is the public, non-blocking method.
// It formats the entry and sends it to the channel.
func (l *Logger) log(level Level, msg string) {
if level < l.level {
return
}
// Get caller info
var caller string
if l.structured {
_, file, line, ok := runtime.Caller(2) // 2 steps up: log -> Debug/Info... -> caller
if !ok {
file = "???"
line = 0
}
caller = fmt.Sprintf("%s:%d", filepath.Base(file), line)
}
entry := logEntry{
level: level,
time: time.Now(),
caller: caller,
message: msg,
}
// Send to channel. This will block if the channel is full.
// For a more robust library, a select with a default
// could drop logs instead of blocking the application.
l.logChan <- entry
}
// logJSON is the public, non-blocking method for raw JSON data.
// It formats the entry and sends it to the channel.
func (l *Logger) logJSON(level Level, data []byte) {
if level < l.level {
return
}
// Get caller info
var caller string
if l.structured {
_, file, line, ok := runtime.Caller(2) // 2 steps up: logJSON -> DebugJSON/InfoJSON... -> caller
if !ok {
file = "???"
line = 0
}
caller = fmt.Sprintf("%s:%d", filepath.Base(file), line)
}
entry := logEntry{
level: level,
time: time.Now(),
caller: caller,
rawData: data, // Use the new field
}
// Send to channel.
l.logChan <- entry
}
// run is the background goroutine that handles all file I/O.
func (l *Logger) run(flushFrequency time.Duration) {
defer l.wg.Done()
ticker := time.NewTicker(flushFrequency)
defer ticker.Stop()
for {
select {
case entry := <-l.logChan:
// A log message was received
l.write(entry)
case <-ticker.C:
// Periodic flush
if err := l.writer.Flush(); err != nil {
fmt.Fprintf(os.Stderr, "logger: failed periodic flush: %v\n", err)
}
case <-l.doneChan:
// Shutdown signal
l.shutdown()
return
}
}
}
// shutdown drains the channel, flushes, and closes the file.
func (l *Logger) shutdown() {
// Stop accepting new logs (though channel is already closed by Close())
// Drain any remaining items in the log channel
for {
select {
case entry := <-l.logChan:
l.write(entry)
default:
// Channel is empty, proceed to final flush and close
if err := l.writer.Flush(); err != nil {
fmt.Fprintf(os.Stderr, "logger: failed final flush: %v\n", err)
}
if err := l.file.Close(); err != nil {
fmt.Fprintf(os.Stderr, "logger: failed to close file: %v\n", err)
}
return
}
}
}
// write is the internal, unsafe method that writes to the buffer.
// It is ONLY called by the `run` goroutine.
func (l *Logger) write(entry logEntry) {
// Check if rotation is needed
if err := l.checkRotate(); err != nil {
fmt.Fprintf(os.Stderr, "logger: failed to check rotation: %v\n", err)
}
var logEntryBytes []byte
if l.structured {
// --- Path for rawData (structured JSON) ---
if entry.rawData != nil {
var data map[string]interface{}
// Try to unmarshal the user's data
if err := json.Unmarshal(entry.rawData, &data); err != nil {
// Data is not valid JSON. Log as a string with an error.
jsonEntry := struct {
Time string `json:"time"`
Level string `json:"level"`
Message string `json:"message"` // Log the raw bytes as a string
Caller string `json:"caller,omitempty"`
Error string `json:"log_error,omitempty"`
}{
Time: entry.time.Format(time.RFC3339Nano),
Level: entry.level.String(),
Message: string(entry.rawData),
Caller: entry.caller,
Error: "failed to unmarshal structured data",
}
logEntryBytes, _ = json.Marshal(jsonEntry)
} else {
// Data IS valid JSON. Merge logger fields into it.
// Note: This will overwrite user fields with the same name (time, level, caller).
data["time"] = entry.time.Format(time.RFC3339Nano)
data["level"] = entry.level.String()
if entry.caller != "" {
data["caller"] = entry.caller
}
// Re-marshal the merged map
logEntryBytes, _ = json.Marshal(data)
}
// --- Path for message (standard string log) ---
} else {
jsonEntry := struct {
Time string `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
Caller string `json:"caller,omitempty"`
}{
Time: entry.time.Format(time.RFC3339Nano),
Level: entry.level.String(),
Message: entry.message,
Caller: entry.caller,
}
logEntryBytes, _ = json.Marshal(jsonEntry)
}
logEntryBytes = append(logEntryBytes, '\n')
} else {
// Plain text format
var message string
if entry.rawData != nil {
message = string(entry.rawData) // Use raw data as message
} else {
message = entry.message // Use string message
}
logEntryBytes = []byte(fmt.Sprintf("%s [%s] %s\n", entry.time.Format(time.RFC3339), entry.level.String(), message))
}
// Write to buffer (not flushed immediately)
if _, err := l.writer.Write(logEntryBytes); err != nil {
fmt.Fprintf(os.Stderr, "logger: failed to write: %v\n", err)
}
}
// checkRotate checks file size and triggers rotation if needed.
// It is ONLY called by the `run` goroutine.
func (l *Logger) checkRotate() error {
stat, err := l.file.Stat()
if err != nil {
return err
}
if stat.Size() < l.maxSize {
return nil // No rotation needed
}
return l.rotate()
}
// rotate performs the log file rotation.
// It is ONLY called by the `run` goroutine.
func (l *Logger) rotate() error {
// 1. Flush and close current file
if err := l.writer.Flush(); err != nil {
l.file.Close() // Attempt to close even if flush fails
return fmt.Errorf("failed to flush old log: %w", err)
}
if err := l.file.Close(); err != nil {
return fmt.Errorf("failed to close old log: %w", err)
}
// 2. Rename old backups
baseFilename := l.filename
ext := filepath.Ext(baseFilename)
prefix := strings.TrimSuffix(baseFilename, ext)
// Delete the oldest backup if it exists
oldestBackup := fmt.Sprintf("%s.%d%s", prefix, l.maxBackups, ext)
os.Remove(oldestBackup) // Ignore error if it doesn't exist
// Shift remaining backups
for i := l.maxBackups - 1; i > 0; i-- {
src := fmt.Sprintf("%s.%d%s", prefix, i, ext)
dst := fmt.Sprintf("%s.%d%s", prefix, i+1, ext)
if _, err := os.Stat(src); err == nil {
os.Rename(src, dst) // Ignore error
}
}
// 3. Rename current log to .1
backup1 := fmt.Sprintf("%s.1%s", prefix, ext)
if err := os.Rename(l.filename, backup1); err != nil {
fmt.Fprintf(os.Stderr, "logger: failed to rename log: %v\n", err)
}
// 4. Open a new log file
newFile, err := os.OpenFile(l.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open new log file: %w", err)
}
// 5. Update logger state
l.file = newFile
l.writer = bufio.NewWriter(newFile)
return nil
}
// Close gracefully shuts down the logger, flushing all pending logs.
func (l *Logger) Close() error {
// Signal the `run` goroutine to stop
close(l.doneChan)
// Wait for the goroutine to finish draining the channel and closing the file
l.wg.Wait()
return nil
}
//