/
colog.go
534 lines (440 loc) · 12.3 KB
/
colog.go
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
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
// Package colog implements prefix based logging by setting itself as output of the standard library
// and parsing the log messages. Level prefixes are called headers in CoLog terms to not confuse with
// log.Prefix() which is independent.
// Basic usage only requires registering:
// func main() {
// colog.Register()
// log.Print("info: that's all it takes!")
// }
//
// CoLog requires the standard logger to submit messages without prefix or flags. So it resets them
// while registering and assigns them to itself, unfortunately CoLog cannot be aware of any output
// previously set.
package colog
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"os"
"runtime"
"sync"
"time"
)
// std is the global singleton
// analog of the standard log.std
var std = NewCoLog(os.Stderr, "", 0)
// CoLog encapsulates our log writer
type CoLog struct {
mu sync.Mutex
host string
prefix string
minLevel Level
defaultLevel Level
headers HeaderMap
extractor Extractor
formatter Formatter
customFmt bool
parseFields bool
fixed Fields
hooks hookPool
out io.Writer
}
// Entry represents a message being logged and all attached data
type Entry struct {
Level Level // severity: trace, debug, info, warning, error, alert
Time time.Time // time of the event
Host string // host origin of the message
Prefix string // Prefix set to the logger
File string // file where the log was called
Line int // line in the file where the log was called
Message []byte // logged message
Fields Fields // map of key-value data parsed from the message
}
// Level represents severity level
type Level uint8
// LevelMap links levels with output header bytes
type LevelMap map[Level][]byte
// HeaderMap links input header strings with levels
type HeaderMap map[string]Level
// hookPool is a list of registered pool, grouped by Level
type hookPool map[Level][]Hook
// Fields is the key-value map for extracted data
type Fields map[string]interface{}
const (
// Unknown severity level
unknown Level = iota
// LTrace represents trace severity level
LTrace
// LDebug represents debug severity level
LDebug
// LInfo represents info severity level
LInfo
// LWarning represents warning severity level
LWarning
// LError represents error severity level
LError
// LAlert represents alert severity level
LAlert
)
// String implements the Stringer interface for levels
func (level Level) String() string {
switch level {
case LTrace:
return "trace"
case LDebug:
return "debug"
case LInfo:
return "info"
case LWarning:
return "warning"
case LError:
return "error"
case LAlert:
return "alert"
}
return "unknown"
}
var initialMinLevel = LTrace
var initialDefaultLevel = LInfo
var defaultHeaders = HeaderMap{
"t: ": LTrace,
"trc: ": LTrace,
"trace: ": LTrace,
"d: ": LDebug,
"dbg: ": LDebug,
"debug: ": LDebug,
"i: ": LInfo,
"inf: ": LInfo,
"info: ": LInfo,
"w: ": LWarning,
"wrn: ": LWarning,
"warn: ": LWarning,
"warning: ": LWarning,
"e: ": LError,
"err: ": LError,
"error: ": LError,
"a: ": LAlert,
"alr: ": LAlert,
"alert: ": LAlert,
"panic: ": LAlert,
}
// NewCoLog returns CoLog instance ready to be used in logger.SetOutput()
func NewCoLog(out io.Writer, prefix string, flags int) *CoLog {
cl := new(CoLog)
cl.minLevel = initialMinLevel
cl.defaultLevel = initialDefaultLevel
cl.hooks = make(hookPool)
cl.fixed = make(Fields)
cl.headers = defaultHeaders
cl.prefix = prefix
cl.formatter = &StdFormatter{Flag: flags}
cl.extractor = &StdExtractor{}
cl.SetOutput(out)
if host, err := os.Hostname(); err != nil {
cl.host = host
}
return cl
}
// Register sets CoLog as output for the default logger.
// It "hijacks" the standard logger flags and prefix previously set.
// It's not possible to know the output previously set, so the
// default os.Stderr is assumed.
func Register() {
// Inherit standard logger flags and prefix if appropriate
if !std.customFmt {
std.formatter.SetFlags(log.Flags())
}
if log.Prefix() != "" && std.prefix == "" {
std.SetPrefix(log.Prefix())
}
// Disable all extras
log.SetPrefix("")
log.SetFlags(0)
// Set CoLog as output
log.SetOutput(std)
}
// AddHook adds a hook to be fired on every event with
// matching level being logged. See the hook interface
func (cl *CoLog) AddHook(hook Hook) {
cl.mu.Lock()
defer cl.mu.Unlock()
for _, l := range hook.Levels() {
cl.hooks[l] = append(cl.hooks[l], hook)
}
}
// SetHost sets the logger hostname assigned to the entries
func (cl *CoLog) SetHost(host string) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.host = host
}
// SetPrefix sets the logger output prefix
func (cl *CoLog) SetPrefix(prefix string) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.prefix = prefix
}
// SetMinLevel sets the minimum level that will be actually logged
func (cl *CoLog) SetMinLevel(l Level) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.minLevel = l
}
// SetDefaultLevel sets the level that will be used when no level is detected
func (cl *CoLog) SetDefaultLevel(l Level) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.defaultLevel = l
}
// ParseFields activates or deactivates field parsing in the message
func (cl *CoLog) ParseFields(active bool) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.parseFields = active
}
// SetHeaders sets custom headers as the input headers to be search for to determine the level
func (cl *CoLog) SetHeaders(headers HeaderMap) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.headers = headers
}
// AddHeader adds a custom header to the input headers to be search for to determine the level
func (cl *CoLog) AddHeader(header string, level Level) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.headers[header] = level
}
// SetFormatter sets the formatter to use
func (cl *CoLog) SetFormatter(f Formatter) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.customFmt = true
cl.formatter = f
}
// SetExtractor sets the formatter to use
func (cl *CoLog) SetExtractor(ex Extractor) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.extractor = ex
}
// FixedValue sets a key-value pair that will get automatically
// added to every log entry in this logger
func (cl *CoLog) FixedValue(key string, value interface{}) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.fixed[key] = value
}
// ClearFixedValues removes all previously set fields from the logger
func (cl *CoLog) ClearFixedValues() {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.fixed = make(Fields)
}
// Flags returns the output flags for the formatter if any
func (cl *CoLog) Flags() int {
cl.mu.Lock()
defer cl.mu.Unlock()
if cl.formatter == nil {
return 0
}
return cl.formatter.Flags()
}
// SetFlags sets the output flags for the formatter if any
func (cl *CoLog) SetFlags(flags int) {
cl.mu.Lock()
defer cl.mu.Unlock()
if cl.formatter == nil {
return
}
cl.formatter.SetFlags(flags)
}
// SetOutput is analog to log.SetOutput sets the output destination.
func (cl *CoLog) SetOutput(w io.Writer) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.out = w
// if we have a color formatter, notify if new output supports color
if _, ok := cl.formatter.(ColorFormatter); ok {
cl.formatter.(ColorFormatter).ColorSupported(cl.colorSupported())
}
}
// NewLogger returns a colog-enabled logger
func (cl *CoLog) NewLogger() *log.Logger {
cl.mu.Lock()
defer cl.mu.Unlock()
return log.New(cl, "", 0)
}
// Write implements io.Writer interface to that the standard logger uses.
func (cl *CoLog) Write(p []byte) (n int, err error) {
cl.mu.Lock()
defer func() {
cl.mu.Unlock()
if r := recover(); r != nil {
err = fmt.Errorf("error: colog: recovered panic %v\n", r)
fmt.Fprintln(os.Stderr, err.Error())
}
}()
e := cl.parse(p)
cl.extractFields(e)
cl.fireHooks(e)
if e.Level != unknown && e.Level < cl.minLevel {
return 0, nil
}
if e.Level == unknown && cl.defaultLevel < cl.minLevel {
return 0, nil
}
if cl.formatter == nil {
err = errors.New("error: colog: missing formatter")
fmt.Fprintln(os.Stderr, err.Error())
return 0, err
}
fp, err := cl.formatter.Format(e)
if err != nil {
fmt.Fprintf(os.Stderr, "error: colog: failed to format entry: %v\n", err)
return 0, err
}
n, err = cl.out.Write(fp)
if err != nil {
return n, err
}
return len(p), nil
}
func (cl *CoLog) parse(p []byte) *Entry {
e := &Entry{
Time: time.Now(),
Host: cl.host,
Prefix: cl.prefix,
Fields: make(Fields),
Message: bytes.TrimRight(p, "\n"),
}
// Apply fixed fields
for k, v := range cl.fixed {
e.Fields[k] = v
}
cl.applyLevel(e)
// this is a bit expensive, check is anyone might actually need it
if len(cl.hooks) != 0 || cl.formatter.Flags()&(log.Lshortfile|log.Llongfile) != 0 {
e.File, e.Line = getFileLine(5)
}
return e
}
func (cl *CoLog) applyLevel(e *Entry) {
for k, v := range cl.headers {
header := []byte(k)
if bytes.HasPrefix(e.Message, header) {
e.Level = v // apply level
e.Message = bytes.TrimPrefix(e.Message, header) // remove header from message
return
}
}
e.Level = cl.defaultLevel
return
}
// figure if output supports color
func (cl *CoLog) colorSupported() bool {
// ColorSupporters can decide themselves
if ce, ok := cl.out.(ColorSupporter); ok {
return ce.ColorSupported()
}
// Windows users need ColorSupporter outputs
if runtime.GOOS == "windows" {
return false
}
// Check for Fd() method
output, ok := cl.out.(interface {
Fd() uintptr
})
// If no file descriptor it's not a TTY
if !ok {
return false
}
return isTerminal(int(output.Fd()))
}
func (cl *CoLog) extractFields(e *Entry) {
if cl.parseFields && cl.extractor != nil {
err := cl.extractor.Extract(e)
if err != nil {
fmt.Fprintf(os.Stderr, "error: colog: failed to extract fields: %v\n", err)
}
}
}
func (cl *CoLog) fireHooks(e *Entry) {
for k := range cl.hooks[e.Level] {
err := cl.hooks[e.Level][k].Fire(e)
if err != nil {
fmt.Fprintf(os.Stderr, "error: colog: failed to fire hook: %v\n", err)
}
}
}
// Standard logger functions
// AddHook adds a hook to be fired on every event with
// matching level being logged on the standard logger
func AddHook(hook Hook) {
std.AddHook(hook)
}
// SetHost sets the logger hostname assigned to the entries of the standard logger
func SetHost(host string) {
std.SetHost(host)
}
// SetPrefix sets the logger output prefix of the standard logger
func SetPrefix(prefix string) {
std.SetPrefix(prefix)
}
// SetMinLevel sets the minimum level that will be actually logged by the standard logger
func SetMinLevel(l Level) {
std.SetMinLevel(l)
}
// SetDefaultLevel sets the level that will be used when no level is detected for the standard logger
func SetDefaultLevel(l Level) {
std.SetDefaultLevel(l)
}
// ParseFields activates or deactivates field parsing in the message for the standard logger
func ParseFields(active bool) {
std.ParseFields(active)
}
// SetHeaders sets custom headers as the input headers to be search for to determine the level for the standard logger
func SetHeaders(headers HeaderMap) {
std.SetHeaders(headers)
}
// AddHeader adds a custom header to the input headers to be search for to determine the level for the standard logger
func AddHeader(header string, level Level) {
std.AddHeader(header, level)
}
// Flags returns the output flags for the standard log formatter if any
func Flags() int {
return std.Flags()
}
// SetFlags sets the output flags for the standard log formatter if any
func SetFlags(flags int) {
std.SetFlags(flags)
}
// SetOutput is analog to log.SetOutput sets the output destination for the standard logger
func SetOutput(w io.Writer) {
std.SetOutput(w)
}
// SetFormatter sets the formatter to use by the standard logger
func SetFormatter(f Formatter) {
std.SetFormatter(f)
}
// SetExtractor sets the extractor to use by the standard logger
func SetExtractor(ex Extractor) {
std.SetExtractor(ex)
}
// FixedValue sets a field-value pair that will get automatically
// added to every log entry in the standard logger
func FixedValue(key string, value interface{}) {
std.FixedValue(key, value)
}
// ClearFixedValues removes all previously set field-value in the standard logger
func ClearFixedValues() {
std.ClearFixedValues()
}
// ParseLevel parses a string into a type Level
func ParseLevel(level string) (Level, error) {
if lvl, ok := std.headers[level+": "]; ok {
return lvl, nil
}
return unknown, fmt.Errorf("could not parse level %s", level)
}