-
Notifications
You must be signed in to change notification settings - Fork 0
/
logger.go
735 lines (655 loc) · 21.5 KB
/
logger.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
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
// Copyright 2017-2023 Fortio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Fortio's log is simple logger built on top of go's default one with
additional opinionated levels similar to glog but simpler to use and configure.
See [Config] object for options like whether to include line number and file name of caller or not etc
So far it's a "global" logger as in you just use the functions in the package directly (e.g log.S())
and the configuration is global for the process.
*/
package log // import "fortio.org/log"
import (
"bytes"
"flag"
"fmt"
"io"
"log"
"math"
"os"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"fortio.org/log/goroutine"
"fortio.org/struct2env"
)
// Level is the level of logging (0 Debug -> 6 Fatal).
type Level int8
// Log levels. Go can't have variable and function of the same name so we keep
// medium length (Dbg,Info,Warn,Err,Crit,Fatal) names for the functions.
const (
Debug Level = iota
Verbose
Info
Warning
Error
Critical
Fatal
NoLevel
// Prefix for all config from environment,
// e.g NoTimestamp becomes LOGGER_NO_TIMESTAMP.
EnvPrefix = "LOGGER_"
)
//nolint:revive // we keep "Config" for the variable itself.
type LogConfig struct {
LogPrefix string // "Prefix to log lines before logged messages
LogFileAndLine bool // Logs filename and line number of callers to log.
FatalPanics bool // If true, log.Fatalf will panic (stack trace) instead of just exit 1
FatalExit func(int) `env:"-"` // Function to call upon log.Fatalf. e.g. os.Exit.
JSON bool // If true, log in structured JSON format instead of text (but see ConsoleColor).
NoTimestamp bool // If true, don't log timestamp in json.
ConsoleColor bool // If true and we detect console output (not redirected), use text+color mode.
// Force color mode even if logger output is not console (useful for CI that recognize ansi colors).
// SetColorMode() must be called if this or ConsoleColor are changed.
ForceColor bool
// If true, log the goroutine ID (gid) in json.
GoroutineID bool
// If true, single combined log for LogAndCall
CombineRequestAndResponse bool
// String version of the log level, used for setting from environment.
Level string
// If true, ignore SetDefaultsForClientTools() calls even if set. Allows full line/file debug and basically
// imply configuration from the environment variables.
IgnoreCliMode bool
}
// DefaultConfig() returns the default initial configuration for the logger, best suited
// for servers. It will log caller file and line number, use a prefix to split line info
// from the message and panic (+exit) on Fatal.
// It's JSON structured by default, unless console is detected.
// Use SetDefaultsForClientTools for CLIs.
func DefaultConfig() *LogConfig {
return &LogConfig{
LogPrefix: "> ",
LogFileAndLine: true,
FatalPanics: true,
FatalExit: os.Exit,
JSON: true,
ConsoleColor: true,
GoroutineID: true,
CombineRequestAndResponse: true,
}
}
var (
Config = DefaultConfig()
// Used for dynamic flag setting as strings and validation.
LevelToStrA = []string{
"Debug",
"Verbose",
"Info",
"Warning",
"Error",
"Critical",
"Fatal",
}
levelToStrM map[string]Level
levelInternal int32
// Used for JSON logging.
LevelToJSON = []string{
// matching https://github.com/grafana/grafana/blob/main/docs/sources/explore/logs-integration.md
// adding the "" around to save processing when generating json. using short names to save some bytes.
"\"dbug\"",
"\"trace\"",
"\"info\"",
"\"warn\"",
"\"err\"",
"\"crit\"",
"\"fatal\"",
"\"info\"", // For Printf / NoLevel JSON output
}
// Reverse mapping of level string used in JSON to Level. Used by https://github.com/fortio/logc
// to interpret and colorize pre existing JSON logs.
JSONStringLevelToLevel map[string]Level
)
// SetDefaultsForClientTools changes the default value of LogPrefix and LogFileAndLine
// to make output without caller and prefix, a default more suitable for command line tools (like dnsping).
// Needs to be called before flag.Parse(). Caller could also use log.Printf instead of changing this
// if not wanting to use levels. Also makes log.Fatalf just exit instead of panic.
// Will be ignored if the environment config has been set to ignore this.
func SetDefaultsForClientTools() {
if Config.IgnoreCliMode {
Infof("Ignoring SetDefaultsForClientTools() call due to LOGGER_IGNORE_CLI_MODE environment config")
return
}
Config.LogPrefix = " "
Config.LogFileAndLine = false
Config.FatalPanics = false
Config.ConsoleColor = true
Config.JSON = false
Config.GoroutineID = false
Config.CombineRequestAndResponse = false
SetColorMode()
}
// JSONEntry is the logical format of the JSON [Config.JSON] output mode.
// While that serialization of is custom in order to be cheap, it maps to the following
// structure.
type JSONEntry struct {
TS float64 // In seconds since epoch (unix micros resolution), see TimeToTS().
R int64 // Goroutine ID (if enabled)
Level string
File string
Line int
Msg string
// + additional optional fields
// See https://go.dev/play/p/oPK5vyUH2tf for a possibility (using https://github.com/devnw/ajson )
// or https://go.dev/play/p/H0RPmuc3dzv (using github.com/mitchellh/mapstructure)
}
// Time() converts a LogEntry.TS to time.Time.
// The returned time is set UTC to avoid TZ mismatch.
// Inverse of TimeToTS().
func (l *JSONEntry) Time() time.Time {
sec := int64(l.TS)
return time.Unix(
sec, // float seconds -> int Seconds
int64(math.Round(1e6*(l.TS-float64(sec)))*1000), // reminder -> Nanoseconds
)
}
func intToLevel(i int) Level {
if i < 0 || i >= len(LevelToStrA) {
return -1
}
return Level(i) //nolint:gosec // we just checked above.
}
//nolint:gochecknoinits // needed
func init() {
if !isValid(os.Stderr) { // wasm in browser case for instance
SetOutput(os.Stdout) // this could also be invalid too, but... we tried.
}
setLevel(Info) // starting value
levelToStrM = make(map[string]Level, 2*len(LevelToStrA))
JSONStringLevelToLevel = make(map[string]Level, len(LevelToJSON)-1) // -1 to not reverse info to NoLevel
for l, name := range LevelToStrA {
// Allow both -loglevel Verbose and -loglevel verbose ...
lvl := intToLevel(l)
levelToStrM[name] = lvl
levelToStrM[strings.ToLower(name)] = lvl
}
for l, name := range LevelToJSON[0 : Fatal+1] { // Skip NoLevel
// strip the quotes around
JSONStringLevelToLevel[name[1:len(name)-1]] = intToLevel(l)
}
log.SetFlags(log.Ltime)
configFromEnv()
SetColorMode()
jWriter.buf.Grow(2048)
}
func configFromEnv() {
prev := Config.Level
struct2env.SetFromEnv(EnvPrefix, Config)
if Config.Level != "" && Config.Level != prev {
lvl, err := ValidateLevel(Config.Level)
if err != nil {
Errf("Invalid log level from environment %q: %v", Config.Level, err)
return
}
SetLogLevelQuiet(lvl)
Infof("Log level set from environment %s%s to %s", EnvPrefix, "LEVEL", lvl.String())
}
Config.Level = GetLogLevel().String()
}
func setLevel(lvl Level) {
atomic.StoreInt32(&levelInternal, int32(lvl))
}
// String returns the string representation of the level.
func (l Level) String() string {
return LevelToStrA[l]
}
// ValidateLevel returns error if the level string is not valid.
func ValidateLevel(str string) (Level, error) {
var lvl Level
var ok bool
if lvl, ok = levelToStrM[str]; !ok {
return -1, fmt.Errorf("should be one of %v", LevelToStrA)
}
return lvl, nil
}
// LoggerStaticFlagSetup call to setup a static flag under the passed name or
// `-loglevel` by default, to set the log level.
// Use https://pkg.go.dev/fortio.org/dflag/dynloglevel#LoggerFlagSetup for a dynamic flag instead.
func LoggerStaticFlagSetup(names ...string) {
if len(names) == 0 {
names = []string{"loglevel"}
}
for _, name := range names {
flag.Var(&flagV, name, fmt.Sprintf("log `level`, one of %v", LevelToStrA))
}
}
// --- Start of code/types needed string to level custom flag validation section ---
type flagValidation struct {
ours bool
}
var flagV = flagValidation{true}
func (f *flagValidation) String() string {
// Need to tell if it's our value or the zeroValue the flag package creates
// to decide whether to print (default ...) or not.
if !f.ours {
return ""
}
return GetLogLevel().String()
}
func (f *flagValidation) Set(inp string) error {
v := strings.ToLower(strings.TrimSpace(inp))
lvl, err := ValidateLevel(v)
if err != nil {
return err
}
SetLogLevel(lvl)
return nil
}
// --- End of code/types needed string to level custom flag validation section ---
// Sets level from string (called by dflags).
// Use https://pkg.go.dev/fortio.org/dflag/dynloglevel#LoggerFlagSetup to set up
// `-loglevel` as a dynamic flag (or an example of how this function is used).
func SetLogLevelStr(str string) error {
var lvl Level
var err error
if lvl, err = ValidateLevel(str); err != nil {
return err
}
SetLogLevel(lvl)
return err // nil
}
// SetLogLevel sets the log level and returns the previous one.
func SetLogLevel(lvl Level) Level {
return setLogLevel(lvl, true)
}
// SetLogLevelQuiet sets the log level and returns the previous one but does
// not log the change of level itself.
func SetLogLevelQuiet(lvl Level) Level {
return setLogLevel(lvl, false)
}
// setLogLevel sets the log level and returns the previous one.
// if logChange is true the level change is logged.
func setLogLevel(lvl Level, logChange bool) Level {
prev := GetLogLevel()
if lvl < Debug {
logUnconditionalf(Config.LogFileAndLine, Error, "SetLogLevel called with level %d lower than Debug!", lvl)
return -1
}
if lvl > Critical {
logUnconditionalf(Config.LogFileAndLine, Error, "SetLogLevel called with level %d higher than Critical!", lvl)
return -1
}
if lvl != prev {
if logChange && Log(Info) {
logUnconditionalf(Config.LogFileAndLine, Info, "Log level is now %d %s (was %d %s)", lvl, lvl.String(), prev, prev.String())
}
setLevel(lvl)
jWriter.mutex.Lock()
Config.Level = lvl.String()
jWriter.mutex.Unlock()
}
return prev
}
// EnvHelp shows the current config as environment variables.
//
// LOGGER_LOG_PREFIX, LOGGER_LOG_FILE_AND_LINE, LOGGER_FATAL_PANICS,
// LOGGER_JSON, LOGGER_NO_TIMESTAMP, LOGGER_CONSOLE_COLOR, LOGGER_CONSOLE_COLOR
// LOGGER_FORCE_COLOR, LOGGER_GOROUTINE_ID, LOGGER_COMBINE_REQUEST_AND_RESPONSE,
// LOGGER_LEVEL.
func EnvHelp(w io.Writer) {
res, _ := struct2env.StructToEnvVars(Config)
str := struct2env.ToShellWithPrefix(EnvPrefix, res, true)
fmt.Fprintln(w, "# Logger environment variables:")
fmt.Fprint(w, str)
}
// GetLogLevel returns the currently configured LogLevel.
func GetLogLevel() Level {
return intToLevel(int(atomic.LoadInt32(&levelInternal)))
}
// Log returns true if a given level is currently logged.
func Log(lvl Level) bool {
return int32(lvl) >= atomic.LoadInt32(&levelInternal)
}
// LevelByName returns the LogLevel by its name.
func LevelByName(str string) Level {
return levelToStrM[str]
}
// Logf logs with format at the given level.
// 2 level of calls so it's always same depth for extracting caller file/line.
// Note that log.Logf(Fatal, "...") will not panic or exit, only log.Fatalf() does.
func Logf(lvl Level, format string, rest ...interface{}) {
logPrintf(lvl, format, rest...)
}
// Used when doing our own logging writing, in JSON/structured mode (and some color variants as well, misnomer).
// Also reusing that lock to update global Config.Level.
var (
jWriter = jsonWriter{w: os.Stderr, tsBuf: make([]byte, 0, 32)}
)
type jsonWriter struct {
w io.Writer
mutex sync.Mutex
buf bytes.Buffer
tsBuf []byte
}
func jsonWrite(msg string) {
jsonWriteBytes([]byte(msg))
}
func jsonWriteBytes(msg []byte) {
jWriter.mutex.Lock()
_, _ = jWriter.w.Write(msg) // if we get errors while logging... can't quite ... log errors
jWriter.mutex.Unlock()
}
// Converts a time.Time to a float64 timestamp (seconds since epoch at microsecond resolution).
// This is what is used in JSONEntry.TS.
func TimeToTS(t time.Time) float64 {
// note that nanos like 1688763601.199999400 become 1688763601.1999996 in float64 (!)
// so we use UnixMicro to hide this problem which also means we don't give the nearest
// microseconds but it gets truncated instead ( https://go.dev/play/p/rzojmE2odlg )
usec := t.UnixMicro()
tfloat := float64(usec) / 1e6
return tfloat
}
// timeToTStr is copying the string-ification code from jsonTimestamp(),
// it is used by tests to individually test what jsonTimestamp does.
func timeToTStr(t time.Time) string {
return fmt.Sprintf("%.6f", TimeToTS(t))
}
func jsonTimestamp() string {
if Config.NoTimestamp {
return ""
}
// Change timeToTStr if changing this.
return fmt.Sprintf("\"ts\":%.6f,", TimeToTS(time.Now()))
}
// Returns the json GoRoutineID if enabled.
func jsonGID() string {
if !Config.GoroutineID {
return ""
}
return fmt.Sprintf("\"r\":%d,", goroutine.ID())
}
func logPrintf(lvl Level, format string, rest ...interface{}) {
if !Log(lvl) {
return
}
if Config.JSON && !Config.LogFileAndLine && !Color && !Config.NoTimestamp && !Config.GoroutineID && len(rest) == 0 {
logSimpleJSON(lvl, format)
return
}
logUnconditionalf(Config.LogFileAndLine, lvl, format, rest...)
}
func logSimpleJSON(lvl Level, msg string) {
jWriter.mutex.Lock()
jWriter.buf.Reset()
jWriter.buf.WriteString("{\"ts\":")
t := TimeToTS(time.Now())
jWriter.tsBuf = jWriter.tsBuf[:0] // reset the slice
jWriter.tsBuf = strconv.AppendFloat(jWriter.tsBuf, t, 'f', 6, 64)
jWriter.buf.Write(jWriter.tsBuf)
fmt.Fprintf(&jWriter.buf, ",\"level\":%s,\"msg\":%q}\n",
LevelToJSON[lvl],
msg)
_, _ = jWriter.w.Write(jWriter.buf.Bytes())
jWriter.mutex.Unlock()
}
func logUnconditionalf(logFileAndLine bool, lvl Level, format string, rest ...interface{}) {
prefix := Config.LogPrefix
if prefix == "" {
prefix = " "
}
lvl1Char := ""
if lvl == NoLevel {
prefix = ""
}
if logFileAndLine { //nolint:nestif
_, file, line, _ := runtime.Caller(3)
file = file[strings.LastIndex(file, "/")+1:]
switch {
case Color:
jsonWrite(fmt.Sprintf("%s%s%s %s:%d%s%s%s%s\n",
colorTimestamp(), colorGID(), ColorLevelToStr(lvl),
file, line, prefix, LevelToColor[lvl], fmt.Sprintf(format, rest...), Colors.Reset))
case Config.JSON:
jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"file\":%q,\"line\":%d,\"msg\":%q}\n",
jsonTimestamp(), LevelToJSON[lvl], jsonGID(), file, line, fmt.Sprintf(format, rest...)))
default:
if lvl != NoLevel {
lvl1Char = "[" + LevelToStrA[lvl][0:1] + "]"
}
log.Print(lvl1Char, " ", file, ":", line, prefix, fmt.Sprintf(format, rest...))
}
} else {
switch {
case Color:
jsonWrite(fmt.Sprintf("%s%s%s%s%s%s%s\n",
colorTimestamp(), colorGID(), ColorLevelToStr(lvl), prefix, LevelToColor[lvl],
fmt.Sprintf(format, rest...), Colors.Reset))
case Config.JSON:
if len(rest) != 0 {
format = fmt.Sprintf(format, rest...)
}
jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"msg\":%q}\n",
jsonTimestamp(), LevelToJSON[lvl], jsonGID(), format))
default:
if lvl != NoLevel {
lvl1Char = "[" + LevelToStrA[lvl][0:1] + "]"
}
log.Print(lvl1Char, prefix, fmt.Sprintf(format, rest...))
}
}
}
// Printf forwards to the underlying go logger to print (with only timestamp prefixing).
func Printf(format string, rest ...interface{}) {
logUnconditionalf(false, NoLevel, format, rest...)
}
// SetOutput sets the output to a different writer (forwards to system logger).
func SetOutput(w io.Writer) {
jWriter.w = w
log.SetOutput(w)
SetColorMode() // Colors.Reset color mode boolean
}
// SetFlags forwards flags to the system logger.
func SetFlags(f int) {
log.SetFlags(f)
}
// -- would be nice to be able to create those in a loop instead of copypasta:
// Debugf logs if Debug level is on.
func Debugf(format string, rest ...interface{}) {
logPrintf(Debug, format, rest...)
}
// LogVf logs if Verbose level is on.
func LogVf(format string, rest ...interface{}) { //nolint:revive
logPrintf(Verbose, format, rest...)
}
// Infof logs if Info level is on.
func Infof(format string, rest ...interface{}) {
logPrintf(Info, format, rest...)
}
// Warnf logs if Warning level is on.
func Warnf(format string, rest ...interface{}) {
logPrintf(Warning, format, rest...)
}
// Errf logs if Warning level is on.
func Errf(format string, rest ...interface{}) {
logPrintf(Error, format, rest...)
}
// Critf logs if Warning level is on.
func Critf(format string, rest ...interface{}) {
logPrintf(Critical, format, rest...)
}
// Fatalf logs if Warning level is on and panics or exits.
func Fatalf(format string, rest ...interface{}) {
logPrintf(Fatal, format, rest...)
if Config.FatalPanics {
panic("aborting...")
}
Config.FatalExit(1)
}
// FErrF logs a fatal error and returns 1.
// meant for cli main functions written like:
//
// func main() { os.Exit(Main()) }
//
// and in Main() they can do:
//
// if err != nil {
// return log.FErrf("error: %v", err)
// }
//
// so they can be tested with testscript.
// See https://github.com/fortio/delta/ for an example.
func FErrf(format string, rest ...interface{}) int {
logPrintf(Fatal, format, rest...)
return 1
}
// LogDebug shortcut for fortio.Log(fortio.Debug).
func LogDebug() bool { //nolint:revive
return Log(Debug)
}
// LogVerbose shortcut for fortio.Log(fortio.Verbose).
func LogVerbose() bool { //nolint:revive
return Log(Verbose)
}
// LoggerI defines a log.Logger like interface to pass to packages
// for simple logging. See [Logger()]. See also [NewStdLogger()] for
// intercepting with same type / when an interface can't be used.
type LoggerI interface {
Printf(format string, rest ...interface{})
}
type loggerShm struct{}
func (l *loggerShm) Printf(format string, rest ...interface{}) {
logPrintf(Info, format, rest...)
}
// Logger returns a LoggerI (standard logger compatible) that can be used for simple logging.
func Logger() LoggerI {
logger := loggerShm{}
return &logger
}
// Somewhat slog compatible/style logger
type KeyVal struct {
Key string
StrValue string
Value fmt.Stringer
Cached bool
}
// String() is the slog compatible name for Str. Ends up calling Any() anyway.
func String(key, value string) KeyVal {
return Any(key, value)
}
func Str(key, value string) KeyVal {
return Any(key, value)
}
// Few more slog style short cuts.
func Int(key string, value int) KeyVal {
return Any(key, value)
}
func Int64(key string, value int64) KeyVal {
return Any(key, value)
}
func Float64(key string, value float64) KeyVal {
return Any(key, value)
}
func Bool(key string, value bool) KeyVal {
return Any(key, value)
}
func Rune(key string, value rune) KeyVal {
// Special case otherwise rune is printed as int32 number
return Any(key, string(value)) // similar to "%c".
}
func (v *KeyVal) StringValue() string {
if !v.Cached {
v.StrValue = v.Value.String()
v.Cached = true
}
return v.StrValue
}
type ValueTypes interface{ any }
type ValueType[T ValueTypes] struct {
Val T
}
// Our original name, now switched to slog style Any.
func Attr[T ValueTypes](key string, value T) KeyVal {
return Any(key, value)
}
func Any[T ValueTypes](key string, value T) KeyVal {
return KeyVal{
Key: key,
Value: ValueType[T]{Val: value},
}
}
// S logs a message of the given level with additional attributes.
func S(lvl Level, msg string, attrs ...KeyVal) {
s(lvl, Config.LogFileAndLine, Config.JSON, msg, attrs...)
}
func s(lvl Level, logFileAndLine bool, json bool, msg string, attrs ...KeyVal) {
if !Log(lvl) {
return
}
if Config.JSON && !Config.LogFileAndLine && !Color && !Config.NoTimestamp && !Config.GoroutineID && len(attrs) == 0 {
logSimpleJSON(lvl, msg)
return
}
buf := strings.Builder{}
var format string
switch {
case Color:
format = Colors.Reset + ", " + Colors.Blue + "%s" + Colors.Reset + "=" + LevelToColor[lvl] + "%v"
case json:
format = ",%q:%s"
default:
format = ", %s=%s"
}
for _, attr := range attrs {
buf.WriteString(fmt.Sprintf(format, attr.Key, attr.StringValue()))
}
// TODO share code with log.logUnconditionalf yet without extra locks or allocations/buffers?
prefix := Config.LogPrefix
if prefix == "" {
prefix = " "
}
lvl1Char := ""
if lvl == NoLevel {
prefix = ""
} else {
lvl1Char = "[" + LevelToStrA[lvl][0:1] + "]"
}
if logFileAndLine {
_, file, line, _ := runtime.Caller(2)
file = file[strings.LastIndex(file, "/")+1:]
switch {
case Color:
jsonWrite(fmt.Sprintf("%s%s%s %s:%d%s%s%s%s%s\n",
colorTimestamp(), colorGID(), ColorLevelToStr(lvl),
file, line, prefix, LevelToColor[lvl], msg, buf.String(), Colors.Reset))
case json:
jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"file\":%q,\"line\":%d,\"msg\":%q%s}\n",
jsonTimestamp(), LevelToJSON[lvl], jsonGID(), file, line, msg, buf.String()))
default:
log.Print(lvl1Char, " ", file, ":", line, prefix, msg, buf.String())
}
} else {
switch {
case Color:
jsonWrite(fmt.Sprintf("%s%s%s%s%s%s%s%s\n",
colorTimestamp(), colorGID(), ColorLevelToStr(lvl), prefix, LevelToColor[lvl], msg, buf.String(), Colors.Reset))
case json:
jsonWrite(fmt.Sprintf("{%s\"level\":%s,\"msg\":%q%s}\n",
jsonTimestamp(), LevelToJSON[lvl], msg, buf.String()))
default:
log.Print(lvl1Char, prefix, msg, buf.String())
}
}
}