fortio.org/log@v1.12.2/logger.go (about) 1 // Copyright 2017-2023 Fortio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 /* 16 Fortio's log is simple logger built on top of go's default one with 17 additional opinionated levels similar to glog but simpler to use and configure. 18 19 See [Config] object for options like whether to include line number and file name of caller or not etc 20 21 So far it's a "global" logger as in you just use the functions in the package directly (e.g log.S()) 22 and the configuration is global for the process. 23 */ 24 package log // import "fortio.org/log" 25 26 import ( 27 "bytes" 28 "encoding/json" 29 "flag" 30 "fmt" 31 "io" 32 "log" 33 "math" 34 "os" 35 "runtime" 36 "strconv" 37 "strings" 38 "sync" 39 "sync/atomic" 40 "time" 41 42 "fortio.org/log/goroutine" 43 "fortio.org/struct2env" 44 ) 45 46 // Level is the level of logging (0 Debug -> 6 Fatal). 47 type Level int32 48 49 // Log levels. Go can't have variable and function of the same name so we keep 50 // medium length (Dbg,Info,Warn,Err,Crit,Fatal) names for the functions. 51 const ( 52 Debug Level = iota 53 Verbose 54 Info 55 Warning 56 Error 57 Critical 58 Fatal 59 NoLevel 60 // Prefix for all config from environment, 61 // e.g NoTimestamp becomes LOGGER_NO_TIMESTAMP. 62 EnvPrefix = "LOGGER_" 63 ) 64 65 //nolint:revive // we keep "Config" for the variable itself. 66 type LogConfig struct { 67 LogPrefix string // "Prefix to log lines before logged messages 68 LogFileAndLine bool // Logs filename and line number of callers to log. 69 FatalPanics bool // If true, log.Fatalf will panic (stack trace) instead of just exit 1 70 FatalExit func(int) `env:"-"` // Function to call upon log.Fatalf. e.g. os.Exit. 71 JSON bool // If true, log in structured JSON format instead of text (but see ConsoleColor). 72 NoTimestamp bool // If true, don't log timestamp in json. 73 ConsoleColor bool // If true and we detect console output (not redirected), use text+color mode. 74 // Force color mode even if logger output is not console (useful for CI that recognize ansi colors). 75 // SetColorMode() must be called if this or ConsoleColor are changed. 76 ForceColor bool 77 // If true, log the goroutine ID (gid) in json. 78 GoroutineID bool 79 // If true, single combined log for LogAndCall 80 CombineRequestAndResponse bool 81 // String version of the log level, used for setting from environment. 82 Level string 83 } 84 85 // DefaultConfig() returns the default initial configuration for the logger, best suited 86 // for servers. It will log caller file and line number, use a prefix to split line info 87 // from the message and panic (+exit) on Fatal. 88 // It's JSON structured by default, unless console is detected. 89 // Use SetDefaultsForClientTools for CLIs. 90 func DefaultConfig() *LogConfig { 91 return &LogConfig{ 92 LogPrefix: "> ", 93 LogFileAndLine: true, 94 FatalPanics: true, 95 FatalExit: os.Exit, 96 JSON: true, 97 ConsoleColor: true, 98 GoroutineID: true, 99 CombineRequestAndResponse: true, 100 } 101 } 102 103 var ( 104 Config = DefaultConfig() 105 // Used for dynamic flag setting as strings and validation. 106 LevelToStrA = []string{ 107 "Debug", 108 "Verbose", 109 "Info", 110 "Warning", 111 "Error", 112 "Critical", 113 "Fatal", 114 } 115 levelToStrM map[string]Level 116 levelInternal int32 117 // Used for JSON logging. 118 LevelToJSON = []string{ 119 // matching https://github.com/grafana/grafana/blob/main/docs/sources/explore/logs-integration.md 120 // adding the "" around to save processing when generating json. using short names to save some bytes. 121 "\"dbug\"", 122 "\"trace\"", 123 "\"info\"", 124 "\"warn\"", 125 "\"err\"", 126 "\"crit\"", 127 "\"fatal\"", 128 "\"info\"", // For Printf / NoLevel JSON output 129 } 130 // Reverse mapping of level string used in JSON to Level. Used by https://github.com/fortio/logc 131 // to interpret and colorize pre existing JSON logs. 132 JSONStringLevelToLevel map[string]Level 133 ) 134 135 // SetDefaultsForClientTools changes the default value of LogPrefix and LogFileAndLine 136 // to make output without caller and prefix, a default more suitable for command line tools (like dnsping). 137 // Needs to be called before flag.Parse(). Caller could also use log.Printf instead of changing this 138 // if not wanting to use levels. Also makes log.Fatalf just exit instead of panic. 139 func SetDefaultsForClientTools() { 140 Config.LogPrefix = " " 141 Config.LogFileAndLine = false 142 Config.FatalPanics = false 143 Config.ConsoleColor = true 144 Config.JSON = false 145 Config.GoroutineID = false 146 Config.CombineRequestAndResponse = false 147 SetColorMode() 148 } 149 150 // JSONEntry is the logical format of the JSON [Config.JSON] output mode. 151 // While that serialization of is custom in order to be cheap, it maps to the following 152 // structure. 153 type JSONEntry struct { 154 TS float64 // In seconds since epoch (unix micros resolution), see TimeToTS(). 155 R int64 // Goroutine ID (if enabled) 156 Level string 157 File string 158 Line int 159 Msg string 160 // + additional optional fields 161 // See https://go.dev/play/p/oPK5vyUH2tf for a possibility (using https://github.com/devnw/ajson ) 162 // or https://go.dev/play/p/H0RPmuc3dzv (using github.com/mitchellh/mapstructure) 163 } 164 165 // Time() converts a LogEntry.TS to time.Time. 166 // The returned time is set UTC to avoid TZ mismatch. 167 // Inverse of TimeToTS(). 168 func (l *JSONEntry) Time() time.Time { 169 sec := int64(l.TS) 170 return time.Unix( 171 sec, // float seconds -> int Seconds 172 int64(math.Round(1e6*(l.TS-float64(sec)))*1000), // reminder -> Nanoseconds 173 ) 174 } 175 176 //nolint:gochecknoinits // needed 177 func init() { 178 setLevel(Info) // starting value 179 levelToStrM = make(map[string]Level, 2*len(LevelToStrA)) 180 JSONStringLevelToLevel = make(map[string]Level, len(LevelToJSON)-1) // -1 to not reverse info to NoLevel 181 for l, name := range LevelToStrA { 182 // Allow both -loglevel Verbose and -loglevel verbose ... 183 levelToStrM[name] = Level(l) 184 levelToStrM[strings.ToLower(name)] = Level(l) 185 } 186 for l, name := range LevelToJSON[0 : Fatal+1] { // Skip NoLevel 187 // strip the quotes around 188 JSONStringLevelToLevel[name[1:len(name)-1]] = Level(l) 189 } 190 log.SetFlags(log.Ltime) 191 configFromEnv() 192 SetColorMode() 193 jWriter.buf.Grow(2048) 194 } 195 196 func configFromEnv() { 197 prev := Config.Level 198 struct2env.SetFromEnv(EnvPrefix, Config) 199 if Config.Level != "" && Config.Level != prev { 200 lvl, err := ValidateLevel(Config.Level) 201 if err != nil { 202 Errf("Invalid log level from environment %q: %v", Config.Level, err) 203 return 204 } 205 SetLogLevelQuiet(lvl) 206 Infof("Log level set from environment %s%s to %s", EnvPrefix, "LEVEL", lvl.String()) 207 } 208 Config.Level = GetLogLevel().String() 209 } 210 211 func setLevel(lvl Level) { 212 atomic.StoreInt32(&levelInternal, int32(lvl)) 213 } 214 215 // String returns the string representation of the level. 216 func (l Level) String() string { 217 return LevelToStrA[l] 218 } 219 220 // ValidateLevel returns error if the level string is not valid. 221 func ValidateLevel(str string) (Level, error) { 222 var lvl Level 223 var ok bool 224 if lvl, ok = levelToStrM[str]; !ok { 225 return -1, fmt.Errorf("should be one of %v", LevelToStrA) 226 } 227 return lvl, nil 228 } 229 230 // LoggerStaticFlagSetup call to setup a static flag under the passed name or 231 // `-loglevel` by default, to set the log level. 232 // Use https://pkg.go.dev/fortio.org/dflag/dynloglevel#LoggerFlagSetup for a dynamic flag instead. 233 func LoggerStaticFlagSetup(names ...string) { 234 if len(names) == 0 { 235 names = []string{"loglevel"} 236 } 237 for _, name := range names { 238 flag.Var(&flagV, name, fmt.Sprintf("log `level`, one of %v", LevelToStrA)) 239 } 240 } 241 242 // --- Start of code/types needed string to level custom flag validation section --- 243 244 type flagValidation struct { 245 ours bool 246 } 247 248 var flagV = flagValidation{true} 249 250 func (f *flagValidation) String() string { 251 // Need to tell if it's our value or the zeroValue the flag package creates 252 // to decide whether to print (default ...) or not. 253 if !f.ours { 254 return "" 255 } 256 return GetLogLevel().String() 257 } 258 259 func (f *flagValidation) Set(inp string) error { 260 v := strings.ToLower(strings.TrimSpace(inp)) 261 lvl, err := ValidateLevel(v) 262 if err != nil { 263 return err 264 } 265 SetLogLevel(lvl) 266 return nil 267 } 268 269 // --- End of code/types needed string to level custom flag validation section --- 270 271 // Sets level from string (called by dflags). 272 // Use https://pkg.go.dev/fortio.org/dflag/dynloglevel#LoggerFlagSetup to set up 273 // `-loglevel` as a dynamic flag (or an example of how this function is used). 274 func SetLogLevelStr(str string) error { 275 var lvl Level 276 var err error 277 if lvl, err = ValidateLevel(str); err != nil { 278 return err 279 } 280 SetLogLevel(lvl) 281 return err // nil 282 } 283 284 // SetLogLevel sets the log level and returns the previous one. 285 func SetLogLevel(lvl Level) Level { 286 return setLogLevel(lvl, true) 287 } 288 289 // SetLogLevelQuiet sets the log level and returns the previous one but does 290 // not log the change of level itself. 291 func SetLogLevelQuiet(lvl Level) Level { 292 return setLogLevel(lvl, false) 293 } 294 295 // setLogLevel sets the log level and returns the previous one. 296 // if logChange is true the level change is logged. 297 func setLogLevel(lvl Level, logChange bool) Level { 298 prev := GetLogLevel() 299 if lvl < Debug { 300 logUnconditionalf(Config.LogFileAndLine, Error, "SetLogLevel called with level %d lower than Debug!", lvl) 301 return -1 302 } 303 if lvl > Critical { 304 logUnconditionalf(Config.LogFileAndLine, Error, "SetLogLevel called with level %d higher than Critical!", lvl) 305 return -1 306 } 307 if lvl != prev { 308 if logChange && Log(Info) { 309 logUnconditionalf(Config.LogFileAndLine, Info, "Log level is now %d %s (was %d %s)", lvl, lvl.String(), prev, prev.String()) 310 } 311 setLevel(lvl) 312 Config.Level = lvl.String() 313 } 314 return prev 315 } 316 317 // EnvHelp shows the current config as environment variables. 318 // 319 // LOGGER_LOG_PREFIX, LOGGER_LOG_FILE_AND_LINE, LOGGER_FATAL_PANICS, 320 // LOGGER_JSON, LOGGER_NO_TIMESTAMP, LOGGER_CONSOLE_COLOR, LOGGER_CONSOLE_COLOR 321 // LOGGER_FORCE_COLOR, LOGGER_GOROUTINE_ID, LOGGER_COMBINE_REQUEST_AND_RESPONSE, 322 // LOGGER_LEVEL. 323 func EnvHelp(w io.Writer) { 324 res, _ := struct2env.StructToEnvVars(Config) 325 str := struct2env.ToShellWithPrefix(EnvPrefix, res, true) 326 fmt.Fprintln(w, "# Logger environment variables:") 327 fmt.Fprint(w, str) 328 } 329 330 // GetLogLevel returns the currently configured LogLevel. 331 func GetLogLevel() Level { 332 return Level(atomic.LoadInt32(&levelInternal)) 333 } 334 335 // Log returns true if a given level is currently logged. 336 func Log(lvl Level) bool { 337 return int32(lvl) >= atomic.LoadInt32(&levelInternal) 338 } 339 340 // LevelByName returns the LogLevel by its name. 341 func LevelByName(str string) Level { 342 return levelToStrM[str] 343 } 344 345 // Logf logs with format at the given level. 346 // 2 level of calls so it's always same depth for extracting caller file/line. 347 // Note that log.Logf(Fatal, "...") will not panic or exit, only log.Fatalf() does. 348 func Logf(lvl Level, format string, rest ...interface{}) { 349 logPrintf(lvl, format, rest...) 350 } 351 352 // Used when doing our own logging writing, in JSON/structured mode. 353 var ( 354 jWriter = jsonWriter{w: os.Stderr, tsBuf: make([]byte, 0, 32)} 355 ) 356 357 type jsonWriter struct { 358 w io.Writer 359 mutex sync.Mutex 360 buf bytes.Buffer 361 tsBuf []byte 362 } 363 364 func jsonWrite(msg string) { 365 jsonWriteBytes([]byte(msg)) 366 } 367 368 func jsonWriteBytes(msg []byte) { 369 jWriter.mutex.Lock() 370 _, _ = jWriter.w.Write(msg) // if we get errors while logging... can't quite ... log errors 371 jWriter.mutex.Unlock() 372 } 373 374 // Converts a time.Time to a float64 timestamp (seconds since epoch at microsecond resolution). 375 // This is what is used in JSONEntry.TS. 376 func TimeToTS(t time.Time) float64 { 377 // note that nanos like 1688763601.199999400 become 1688763601.1999996 in float64 (!) 378 // so we use UnixMicro to hide this problem which also means we don't give the nearest 379 // microseconds but it gets truncated instead ( https://go.dev/play/p/rzojmE2odlg ) 380 usec := t.UnixMicro() 381 tfloat := float64(usec) / 1e6 382 return tfloat 383 } 384 385 // timeToTStr is copying the string-ification code from jsonTimestamp(), 386 // it is used by tests to individually test what jsonTimestamp does. 387 func timeToTStr(t time.Time) string { 388 return fmt.Sprintf("%.6f", TimeToTS(t)) 389 } 390 391 func jsonTimestamp() string { 392 if Config.NoTimestamp { 393 return "" 394 } 395 // Change timeToTStr if changing this. 396 return fmt.Sprintf("\"ts\":%.6f,", TimeToTS(time.Now())) 397 } 398 399 // Returns the json GoRoutineID if enabled. 400 func jsonGID() string { 401 if !Config.GoroutineID { 402 return "" 403 } 404 return fmt.Sprintf("\"r\":%d,", goroutine.ID()) 405 } 406 407 func logPrintf(lvl Level, format string, rest ...interface{}) { 408 if !Log(lvl) { 409 return 410 } 411 if Config.JSON && !Config.LogFileAndLine && !Color && !Config.NoTimestamp && !Config.GoroutineID && len(rest) == 0 { 412 logSimpleJSON(lvl, format) 413 return 414 } 415 logUnconditionalf(Config.LogFileAndLine, lvl, format, rest...) 416 } 417 418 func logSimpleJSON(lvl Level, msg string) { 419 jWriter.mutex.Lock() 420 jWriter.buf.Reset() 421 jWriter.buf.WriteString("{\"ts\":") 422 t := TimeToTS(time.Now()) 423 jWriter.tsBuf = jWriter.tsBuf[:0] // reset the slice 424 jWriter.tsBuf = strconv.AppendFloat(jWriter.tsBuf, t, 'f', 6, 64) 425 jWriter.buf.Write(jWriter.tsBuf) 426 fmt.Fprintf(&jWriter.buf, ",\"level\":%s,\"msg\":%q}\n", 427 LevelToJSON[lvl], 428 msg) 429 _, _ = jWriter.w.Write(jWriter.buf.Bytes()) 430 jWriter.mutex.Unlock() 431 } 432 433 func logUnconditionalf(logFileAndLine bool, lvl Level, format string, rest ...interface{}) { 434 prefix := Config.LogPrefix 435 if prefix == "" { 436 prefix = " " 437 } 438 lvl1Char := "" 439 if lvl == NoLevel { 440 prefix = "" 441 } 442 if logFileAndLine { //nolint:nestif 443 _, file, line, _ := runtime.Caller(3) 444 file = file[strings.LastIndex(file, "/")+1:] 445 switch { 446 case Color: 447 jsonWrite(fmt.Sprintf("%s%s%s %s:%d%s%s%s%s\n", 448 colorTimestamp(), colorGID(), ColorLevelToStr(lvl), 449 file, line, prefix, LevelToColor[lvl], fmt.Sprintf(format, rest...), Colors.Reset)) 450 case Config.JSON: 451 jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"file\":%q,\"line\":%d,\"msg\":%q}\n", 452 jsonTimestamp(), LevelToJSON[lvl], jsonGID(), file, line, fmt.Sprintf(format, rest...))) 453 default: 454 if lvl != NoLevel { 455 lvl1Char = "[" + LevelToStrA[lvl][0:1] + "]" 456 } 457 log.Print(lvl1Char, " ", file, ":", line, prefix, fmt.Sprintf(format, rest...)) 458 } 459 } else { 460 switch { 461 case Color: 462 jsonWrite(fmt.Sprintf("%s%s%s%s%s%s%s\n", 463 colorTimestamp(), colorGID(), ColorLevelToStr(lvl), prefix, LevelToColor[lvl], 464 fmt.Sprintf(format, rest...), Colors.Reset)) 465 case Config.JSON: 466 if len(rest) != 0 { 467 format = fmt.Sprintf(format, rest...) 468 } 469 jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"msg\":%q}\n", 470 jsonTimestamp(), LevelToJSON[lvl], jsonGID(), format)) 471 default: 472 if lvl != NoLevel { 473 lvl1Char = "[" + LevelToStrA[lvl][0:1] + "]" 474 } 475 log.Print(lvl1Char, prefix, fmt.Sprintf(format, rest...)) 476 } 477 } 478 } 479 480 // Printf forwards to the underlying go logger to print (with only timestamp prefixing). 481 func Printf(format string, rest ...interface{}) { 482 logUnconditionalf(false, NoLevel, format, rest...) 483 } 484 485 // SetOutput sets the output to a different writer (forwards to system logger). 486 func SetOutput(w io.Writer) { 487 jWriter.w = w 488 log.SetOutput(w) 489 SetColorMode() // Colors.Reset color mode boolean 490 } 491 492 // SetFlags forwards flags to the system logger. 493 func SetFlags(f int) { 494 log.SetFlags(f) 495 } 496 497 // -- would be nice to be able to create those in a loop instead of copypasta: 498 499 // Debugf logs if Debug level is on. 500 func Debugf(format string, rest ...interface{}) { 501 logPrintf(Debug, format, rest...) 502 } 503 504 // LogVf logs if Verbose level is on. 505 func LogVf(format string, rest ...interface{}) { //nolint:revive 506 logPrintf(Verbose, format, rest...) 507 } 508 509 // Infof logs if Info level is on. 510 func Infof(format string, rest ...interface{}) { 511 logPrintf(Info, format, rest...) 512 } 513 514 // Warnf logs if Warning level is on. 515 func Warnf(format string, rest ...interface{}) { 516 logPrintf(Warning, format, rest...) 517 } 518 519 // Errf logs if Warning level is on. 520 func Errf(format string, rest ...interface{}) { 521 logPrintf(Error, format, rest...) 522 } 523 524 // Critf logs if Warning level is on. 525 func Critf(format string, rest ...interface{}) { 526 logPrintf(Critical, format, rest...) 527 } 528 529 // Fatalf logs if Warning level is on and panics or exits. 530 func Fatalf(format string, rest ...interface{}) { 531 logPrintf(Fatal, format, rest...) 532 if Config.FatalPanics { 533 panic("aborting...") 534 } 535 Config.FatalExit(1) 536 } 537 538 // FErrF logs a fatal error and returns 1. 539 // meant for cli main functions written like: 540 // 541 // func main() { os.Exit(Main()) } 542 // 543 // and in Main() they can do: 544 // 545 // if err != nil { 546 // return log.FErrf("error: %v", err) 547 // } 548 // 549 // so they can be tested with testscript. 550 // See https://github.com/fortio/delta/ for an example. 551 func FErrf(format string, rest ...interface{}) int { 552 logPrintf(Fatal, format, rest...) 553 return 1 554 } 555 556 // LogDebug shortcut for fortio.Log(fortio.Debug). 557 func LogDebug() bool { //nolint:revive 558 return Log(Debug) 559 } 560 561 // LogVerbose shortcut for fortio.Log(fortio.Verbose). 562 func LogVerbose() bool { //nolint:revive 563 return Log(Verbose) 564 } 565 566 // LoggerI defines a log.Logger like interface to pass to packages 567 // for simple logging. See [Logger()]. See also [NewStdLogger()] for 568 // intercepting with same type / when an interface can't be used. 569 type LoggerI interface { 570 Printf(format string, rest ...interface{}) 571 } 572 573 type loggerShm struct{} 574 575 func (l *loggerShm) Printf(format string, rest ...interface{}) { 576 logPrintf(Info, format, rest...) 577 } 578 579 // Logger returns a LoggerI (standard logger compatible) that can be used for simple logging. 580 func Logger() LoggerI { 581 logger := loggerShm{} 582 return &logger 583 } 584 585 // Somewhat slog compatible/style logger 586 587 type KeyVal struct { 588 Key string 589 StrValue string 590 Value fmt.Stringer 591 Cached bool 592 } 593 594 // String() is the slog compatible name for Str. Ends up calling Any() anyway. 595 func String(key, value string) KeyVal { 596 return Any(key, value) 597 } 598 599 func Str(key, value string) KeyVal { 600 return Any(key, value) 601 } 602 603 // Few more slog style short cuts. 604 func Int(key string, value int) KeyVal { 605 return Any(key, value) 606 } 607 608 func Int64(key string, value int64) KeyVal { 609 return Any(key, value) 610 } 611 612 func Float64(key string, value float64) KeyVal { 613 return Any(key, value) 614 } 615 616 func Bool(key string, value bool) KeyVal { 617 return Any(key, value) 618 } 619 620 func (v *KeyVal) StringValue() string { 621 if !v.Cached { 622 v.StrValue = v.Value.String() 623 v.Cached = true 624 } 625 return v.StrValue 626 } 627 628 type ValueTypes interface{ any } 629 630 type ValueType[T ValueTypes] struct { 631 Val T 632 } 633 634 func toJSON(v any) string { 635 bytes, err := json.Marshal(v) 636 if err != nil { 637 return strconv.Quote(fmt.Sprintf("ERR marshaling %v: %v", v, err)) 638 } 639 str := string(bytes) 640 // We now handle errors before calling toJSON: if there is a marshaller we use it 641 // otherwise we use the string from .Error() 642 return str 643 } 644 645 func (v ValueType[T]) String() string { 646 // if the type is numeric, use Sprint(v.val) otherwise use Sprintf("%q", v.Val) to quote it. 647 switch s := any(v.Val).(type) { 648 case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, 649 float32, float64: 650 return fmt.Sprint(s) 651 case string: 652 return fmt.Sprintf("%q", s) 653 case error: 654 // Sadly structured errors like nettwork error don't have the reason in 655 // the exposed struct/JSON - ie on gets 656 // {"Op":"read","Net":"tcp","Source":{"IP":"127.0.0.1","Port":60067,"Zone":""}, 657 // "Addr":{"IP":"127.0.0.1","Port":3000,"Zone":""},"Err":{}} 658 // instead of 659 // read tcp 127.0.0.1:60067->127.0.0.1:3000: i/o timeout 660 // Noticed in https://github.com/fortio/fortio/issues/913 661 _, hasMarshaller := s.(json.Marshaler) 662 if hasMarshaller { 663 return toJSON(v.Val) 664 } else { 665 return fmt.Sprintf("%q", s.Error()) 666 } 667 /* It's all handled by json fallback now even though slightly more expensive at runtime, it's a lot simpler */ 668 default: 669 return toJSON(v.Val) // was fmt.Sprintf("%q", fmt.Sprint(v.Val)) 670 } 671 } 672 673 // Our original name, now switched to slog style Any. 674 func Attr[T ValueTypes](key string, value T) KeyVal { 675 return Any(key, value) 676 } 677 678 func Any[T ValueTypes](key string, value T) KeyVal { 679 return KeyVal{ 680 Key: key, 681 Value: ValueType[T]{Val: value}, 682 } 683 } 684 685 // S logs a message of the given level with additional attributes. 686 func S(lvl Level, msg string, attrs ...KeyVal) { 687 s(lvl, Config.LogFileAndLine, Config.JSON, msg, attrs...) 688 } 689 690 func s(lvl Level, logFileAndLine bool, json bool, msg string, attrs ...KeyVal) { 691 if !Log(lvl) { 692 return 693 } 694 if Config.JSON && !Config.LogFileAndLine && !Color && !Config.NoTimestamp && !Config.GoroutineID && len(attrs) == 0 { 695 logSimpleJSON(lvl, msg) 696 return 697 } 698 buf := strings.Builder{} 699 var format string 700 switch { 701 case Color: 702 format = Colors.Reset + ", " + Colors.Blue + "%s" + Colors.Reset + "=" + LevelToColor[lvl] + "%v" 703 case json: 704 format = ",%q:%s" 705 default: 706 format = ", %s=%s" 707 } 708 for _, attr := range attrs { 709 buf.WriteString(fmt.Sprintf(format, attr.Key, attr.StringValue())) 710 } 711 // TODO share code with log.logUnconditionalf yet without extra locks or allocations/buffers? 712 prefix := Config.LogPrefix 713 if prefix == "" { 714 prefix = " " 715 } 716 lvl1Char := "" 717 if lvl == NoLevel { 718 prefix = "" 719 } else { 720 lvl1Char = "[" + LevelToStrA[lvl][0:1] + "]" 721 } 722 if logFileAndLine { 723 _, file, line, _ := runtime.Caller(2) 724 file = file[strings.LastIndex(file, "/")+1:] 725 switch { 726 case Color: 727 jsonWrite(fmt.Sprintf("%s%s%s %s:%d%s%s%s%s%s\n", 728 colorTimestamp(), colorGID(), ColorLevelToStr(lvl), 729 file, line, prefix, LevelToColor[lvl], msg, buf.String(), Colors.Reset)) 730 case json: 731 jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"file\":%q,\"line\":%d,\"msg\":%q%s}\n", 732 jsonTimestamp(), LevelToJSON[lvl], jsonGID(), file, line, msg, buf.String())) 733 default: 734 log.Print(lvl1Char, " ", file, ":", line, prefix, msg, buf.String()) 735 } 736 } else { 737 switch { 738 case Color: 739 jsonWrite(fmt.Sprintf("%s%s%s%s%s%s%s%s\n", 740 colorTimestamp(), colorGID(), ColorLevelToStr(lvl), prefix, LevelToColor[lvl], msg, buf.String(), Colors.Reset)) 741 case json: 742 jsonWrite(fmt.Sprintf("{%s\"level\":%s,\"msg\":%q%s}\n", 743 jsonTimestamp(), LevelToJSON[lvl], msg, buf.String())) 744 default: 745 log.Print(lvl1Char, prefix, msg, buf.String()) 746 } 747 } 748 }