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  }