diff --git a/go.mod b/go.mod index 9e23b07..44b9afe 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23 require ( github.com/didip/tollbooth/v8 v8.0.1 - github.com/go-pkgz/lgr v0.11.1 + github.com/go-pkgz/lgr v0.12.0 github.com/go-pkgz/rest v1.20.2 github.com/go-pkgz/routegroup v1.3.1 github.com/go-pkgz/syncs v1.3.2 diff --git a/go.sum b/go.sum index 1797df2..fb8b11d 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/didip/tollbooth/v8 v8.0.1 h1:VAAapTo1t4Bn6bbpcHjuovwoa9u3JH++wgjbpWv+ github.com/didip/tollbooth/v8 v8.0.1/go.mod h1:oEd9l+ep373d7DmvKLc0a5gasPOev2mTewi6KPQBGJ4= github.com/go-pkgz/expirable-cache/v3 v3.0.0 h1:u3/gcu3sabLYiTCevoRKv+WzjIn5oo7P8XtiXBeRDLw= github.com/go-pkgz/expirable-cache/v3 v3.0.0/go.mod h1:2OQiDyEGQalYecLWmXprm3maPXeVb5/6/X7yRPYTzec= -github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM= -github.com/go-pkgz/lgr v0.11.1/go.mod h1:tgDF4RXQnBfIgJqjgkv0yOeTQ3F1yewWIZkpUhHnAkU= +github.com/go-pkgz/lgr v0.12.0 h1:uoSCLdiMocZDa+L66DavHG5UIkOJvWKOVqt6sNQllw0= +github.com/go-pkgz/lgr v0.12.0/go.mod h1:A4AxjOthFVFK6jRnVYMeusno5SeDAxcLVHd0kI/lN/Y= github.com/go-pkgz/rest v1.20.2 h1:6wYWo85H7xFU09FadVKKc5LKIfIpCStBXJj9F/P4COc= github.com/go-pkgz/rest v1.20.2/go.mod h1:NC2xNN/y1rIs0PY13FowKoH8rk9RhJNJ0tTbkBg8Yks= github.com/go-pkgz/routegroup v1.3.1 h1:XAVWskX8Iup6HoQD9zv+gJx4DOJC2DSkKBHCMeeW8/s= diff --git a/vendor/github.com/go-pkgz/lgr/.golangci.yml b/vendor/github.com/go-pkgz/lgr/.golangci.yml index c0e04d0..c90a028 100644 --- a/vendor/github.com/go-pkgz/lgr/.golangci.yml +++ b/vendor/github.com/go-pkgz/lgr/.golangci.yml @@ -1,8 +1,8 @@ linters-settings: govet: - check-shadowing: true + shadow: true golint: - min-confidence: 0 + min-confidence: 0.6 gocyclo: min-complexity: 15 maligned: @@ -24,44 +24,80 @@ linters-settings: disabled-checks: - wrapperFunc - hugeParam + - rangeValCopy linters: + disable-all: true enable: - - megacheck - revive - govet - unconvert - - megacheck - - gas - - gocyclo - - dupl - - misspell + - gosec - unparam - unused - typecheck - ineffassign - stylecheck - gochecknoinits - - exportloopref - gocritic - nakedret - gosimple - prealloc + fast: false - disable-all: true + run: - output: - format: tab - skip-dirs: - - vendor + concurrency: 4 issues: + exclude-dirs: + - vendor exclude-rules: - text: "should have a package comment, unless it's in another file for this package" linters: - golint + - text: "exitAfterDefer:" + linters: + - gocritic + - text: "whyNoLint: include an explanation for nolint directive" + linters: + - gocritic + - text: "go.mongodb.org/mongo-driver/bson/primitive.E" + linters: + - govet + - text: "weak cryptographic primitive" + linters: + - gosec + - text: "integer overflow conversion" + linters: + - gosec + - text: "should have a package comment" + linters: + - revive - text: "at least one file in a package should have a package comment" linters: - stylecheck + - text: "commentedOutCode: may want to remove commented-out code" + linters: + - gocritic + - text: "unnamedResult: consider giving a name to these results" + linters: + - gocritic + - text: "var-naming: don't use an underscore in package name" + linters: + - revive + - text: "should not use underscores in package names" + linters: + - stylecheck + - text: "struct literal uses unkeyed fields" + linters: + - govet + - linters: + - unparam + - unused + - revive + path: _test\.go$ + text: "unused-parameter" exclude-use-default: false + diff --git a/vendor/github.com/go-pkgz/lgr/CODEOWNERS b/vendor/github.com/go-pkgz/lgr/CODEOWNERS new file mode 100644 index 0000000..91c94cb --- /dev/null +++ b/vendor/github.com/go-pkgz/lgr/CODEOWNERS @@ -0,0 +1,5 @@ +# These owners will be the default owners for everything in the repo. +# Unless a later match takes precedence, @umputun will be requested for +# review when someone opens a pull request. + +* @umputun diff --git a/vendor/github.com/go-pkgz/lgr/README.md b/vendor/github.com/go-pkgz/lgr/README.md index f01f999..87494b8 100644 --- a/vendor/github.com/go-pkgz/lgr/README.md +++ b/vendor/github.com/go-pkgz/lgr/README.md @@ -37,15 +37,16 @@ _Without `lgr.Caller*` it will drop `{caller}` part_ - `lgr.Trace` - turn trace mode on to allow messages with "TRACE" abd "DEBUG" levels both (filtered otherwise) - `lgr.Out(io.Writer)` - sets the output writer, default `os.Stdout` - `lgr.Err(io.Writer)` - sets the error writer, default `os.Stderr` -- `lgr.CallerFile` - adds the caller file info -- `lgr.CallerFunc` - adds the caller function info -- `lgr.CallerPkg` - adds the caller package +- `lgr.CallerFile` - adds the caller file info (only affects lgr's native text format, not slog output) +- `lgr.CallerFunc` - adds the caller function info (only affects lgr's native text format, not slog output) +- `lgr.CallerPkg` - adds the caller package (only affects lgr's native text format, not slog output) - `lgr.LevelBraces` - wraps levels with "[" and "]" - `lgr.Msec` - adds milliseconds to timestamp - `lgr.Format` - sets a custom template, overwrite all other formatting modifiers. - `lgr.Secret(secret ...)` - sets list of the secrets to hide from the logging outputs. - `lgr.Map(mapper)` - sets mapper functions to change elements of the logging output based on levels. - `lgr.StackTraceOnError` - turns on stack trace for ERROR level. +- `lgr.SlogHandler(h slog.Handler)` - delegates logging to the provided slog handler. example: `l := lgr.New(lgr.Debug, lgr.Msec)` @@ -106,15 +107,87 @@ example with [fatih/color](https://github.com/fatih/color): ``` ### adaptors -`lgr` logger can be converted to `io.Writer` or `*log.Logger` +`lgr` logger can be converted to `io.Writer`, `*log.Logger`, or `slog.Handler` - `lgr.ToWriter(l lgr.L, level string) io.Writer` - makes io.Writer forwarding write ops to underlying `lgr.L` - `lgr.ToStdLogger(l lgr.L, level string) *log.Logger` - makes standard logger on top of `lgr.L` +- `lgr.ToSlogHandler(l lgr.L) slog.Handler` - converts lgr.L to a slog.Handler for use with slog _`level` parameter is optional, if defined (non-empty) will enforce the level._ - `lgr.SetupStdLogger(opts ...Option)` initializes std global logger (`log.std`) with lgr logger and given options. All standard methods like `log.Print`, `log.Println`, `log.Fatal` and so on will be forwarder to lgr. +- `lgr.SetupWithSlog(logger *slog.Logger)` sets up the global logger with a slog logger. + +### slog integration + +In addition to the standard logger interface, lgr provides seamless integration with Go's `log/slog` package: + +#### Using lgr with slog + +```go +// Create lgr logger +lgrLogger := lgr.New(lgr.Debug, lgr.Msec) + +// Convert to slog handler and create slog logger +handler := lgr.ToSlogHandler(lgrLogger) +logger := slog.New(handler) + +// Use standard slog API with lgr formatting +logger.Info("message", "key1", "value1") +// Output: 2023/09/15 10:34:56.789 INFO message key1="value1" +``` + +#### Using slog with lgr interface + +```go +// Create slog handler +jsonHandler := slog.NewJSONHandler(os.Stdout, nil) + +// Wrap it with lgr interface +logger := lgr.FromSlogHandler(jsonHandler) + +// Use lgr API with slog backend +logger.Logf("INFO message with %s", "structured data") +// Output: {"time":"2023-09-15T10:34:56.789Z","level":"INFO","msg":"message with structured data"} +``` + +#### Using slog directly in lgr + +```go +// Create a logger that uses slog directly +jsonHandler := slog.NewJSONHandler(os.Stdout, nil) +logger := lgr.New(lgr.SlogHandler(jsonHandler)) + +// Use lgr API with slog backend +logger.Logf("INFO message") +// Output: {"time":"2023-09-15T10:34:56.789Z","level":"INFO","msg":"message"} +``` + +#### JSON output with caller information + +To get caller information in JSON output when using slog handlers, create the handler with `AddSource: true`: + +```go +// Create JSON handler with source information (caller info) +jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, // This enables caller information in JSON output +}) + +// Use handler with lgr +logger := lgr.New(lgr.SlogHandler(jsonHandler)) + +logger.Logf("INFO message with caller info") +// Output will include source file, line and function in JSON +``` + +Note: The lgr caller options (`lgr.CallerFile`, `lgr.CallerFunc`, `lgr.CallerPkg`) only work with lgr's native text format +and don't affect JSON output from slog handlers. To include caller information in JSON logs: + +1. For slog JSON handlers: Create the handler with `AddSource: true` as shown above +2. For text-based logs: Use lgr's native caller options without slog integration + +This behavior is designed to respect each logging system's conventions for representing caller information. ### global logger diff --git a/vendor/github.com/go-pkgz/lgr/interface.go b/vendor/github.com/go-pkgz/lgr/interface.go index 46b961a..11ecd01 100644 --- a/vendor/github.com/go-pkgz/lgr/interface.go +++ b/vendor/github.com/go-pkgz/lgr/interface.go @@ -18,7 +18,7 @@ type Func func(format string, args ...interface{}) func (f Func) Logf(format string, args ...interface{}) { f(format, args...) } // NoOp logger -var NoOp = Func(func(format string, args ...interface{}) {}) +var NoOp = Func(func(format string, args ...interface{}) {}) //nolint:revive // Std logger sends to std default logger directly var Std = Func(func(format string, args ...interface{}) { stdlog.Printf(format, args...) }) @@ -30,7 +30,7 @@ func Printf(format string, args ...interface{}) { // Print simplifies replacement of std logger func Print(line string) { - def.logf(line) + def.logf(line) //nolint:govet } // Fatalf simplifies replacement of std logger diff --git a/vendor/github.com/go-pkgz/lgr/logger.go b/vendor/github.com/go-pkgz/lgr/logger.go index 3759437..00cc0d9 100644 --- a/vendor/github.com/go-pkgz/lgr/logger.go +++ b/vendor/github.com/go-pkgz/lgr/logger.go @@ -11,8 +11,10 @@ package lgr import ( "bytes" + "context" "fmt" "io" + "log/slog" "os" "path" "regexp" @@ -51,18 +53,19 @@ var ( // Logger provided simple logger with basic support of levels. Thread safe type Logger struct { // set with Option calls - stdout, stderr io.Writer // destination writes for out and err - sameStream bool // stdout and stderr are the same stream - dbg bool // allows reporting for DEBUG level - trace bool // allows reporting for TRACE and DEBUG levels - callerFile bool // reports caller file with line number, i.e. foo/bar.go:89 - callerFunc bool // reports caller function name, i.e. bar.myFunc - callerPkg bool // reports caller package name - levelBraces bool // encloses level with [], i.e. [INFO] - callerDepth int // how many stack frames to skip, relative to the real (reported) frame - format string // layout template - secrets [][]byte // sub-strings to secrets by matching - mapper Mapper // map (alter) output based on levels + stdout, stderr io.Writer // destination writes for out and err + sameStream bool // stdout and stderr are the same stream + dbg bool // allows reporting for DEBUG level + trace bool // allows reporting for TRACE and DEBUG levels + callerFile bool // reports caller file with line number, i.e. foo/bar.go:89 + callerFunc bool // reports caller function name, i.e. bar.myFunc + callerPkg bool // reports caller package name + levelBraces bool // encloses level with [], i.e. [INFO] + callerDepth int // how many stack frames to skip, relative to the real (reported) frame + format string // layout template + secrets [][]byte // sub-strings to secrets by matching + mapper Mapper // map (alter) output based on levels + slogHandler slog.Handler // optional slog handler to delegate logging // internal use now nowFn @@ -161,6 +164,25 @@ func (l *Logger) logf(format string, args ...interface{}) { return } + // if slog handler is set, use it + if l.slogHandler != nil { + // use NewRecord for consistency with adapter setup + // skip=0 because we don't need caller information from this context + record := slog.NewRecord(l.now(), stringToLevel(lv), msg, 0) + _ = l.slogHandler.Handle(context.Background(), record) + + // handle FATAL and PANIC levels as they have special behavior + if lv == "FATAL" || lv == "PANIC" { + if lv == "PANIC" { + // print panic stack trace + stack := getDump() + _, _ = l.stderr.Write([]byte(fmt.Sprintf("\n*** PANIC: %s\n\n%s", msg, stack))) + } + l.fatal() + } + return + } + var ci callerInfo if l.callerOn { // optimization to avoid expensive caller evaluation if caller info not in the template ci = l.reportCaller(l.callerDepth) diff --git a/vendor/github.com/go-pkgz/lgr/options.go b/vendor/github.com/go-pkgz/lgr/options.go index 16fadac..5225d91 100644 --- a/vendor/github.com/go-pkgz/lgr/options.go +++ b/vendor/github.com/go-pkgz/lgr/options.go @@ -2,6 +2,7 @@ package lgr import ( "io" + "log/slog" "strings" ) @@ -48,11 +49,13 @@ func Format(f string) Option { } // CallerFunc adds caller info with function name. Ignored if Format option used. +// Note: This option only affects lgr's native text format and is ignored when using SlogHandler. func CallerFunc(l *Logger) { l.callerFunc = true } // CallerPkg adds caller's package name. Ignored if Format option used. +// Note: This option only affects lgr's native text format and is ignored when using SlogHandler. func CallerPkg(l *Logger) { l.callerPkg = true } @@ -63,6 +66,7 @@ func LevelBraces(l *Logger) { } // CallerFile adds caller info with file, and line number. Ignored if Format option used. +// Note: This option only affects lgr's native text format and is ignored when using SlogHandler. func CallerFile(l *Logger) { l.callerFile = true } @@ -96,3 +100,34 @@ func Map(m Mapper) Option { func StackTraceOnError(l *Logger) { l.errorDump = true } + +// SlogHandler sets slog.Handler to delegate logging to. When using this option, +// the output format will be controlled by the slog.Handler provided, not by lgr's +// format options. +// +// IMPORTANT: When using lgr.SlogHandler: +// +// 1. To get caller information in JSON output, you must create the handler with +// slog.HandlerOptions{AddSource: true}. +// +// 2. The lgr caller info options (lgr.CallerFile, lgr.CallerFunc) do NOT affect +// JSON output from slog handlers. They only work with lgr's native text format. +// +// Example of correct setup for JSON with caller info: +// +// // create handler with AddSource enabled +// jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ +// AddSource: true, // This enables caller information in JSON output +// }) +// +// // use handler with lgr +// logger := lgr.New(lgr.SlogHandler(jsonHandler)) +// +// For text format with caller info, use lgr's native caller options: +// +// logger := lgr.New(lgr.CallerFile, lgr.CallerFunc) +func SlogHandler(h slog.Handler) Option { + return func(l *Logger) { + l.slogHandler = h + } +} diff --git a/vendor/github.com/go-pkgz/lgr/slog.go b/vendor/github.com/go-pkgz/lgr/slog.go new file mode 100644 index 0000000..0f93081 --- /dev/null +++ b/vendor/github.com/go-pkgz/lgr/slog.go @@ -0,0 +1,220 @@ +package lgr + +import ( + "context" + "fmt" + "log/slog" + "os" + "runtime" + "strings" + "time" +) + +// ToSlogHandler converts lgr.L to slog.Handler +func ToSlogHandler(l L) slog.Handler { + return &lgrSlogHandler{lgr: l} +} + +// FromSlogHandler creates lgr.L wrapper around slog.Handler +func FromSlogHandler(h slog.Handler) L { + return &slogLgrAdapter{handler: h} +} + +// SetupWithSlog sets up the global logger with a slog logger +func SetupWithSlog(logger *slog.Logger) { + Setup(SlogHandler(logger.Handler())) +} + +// lgrSlogHandler implements slog.Handler using lgr.L +type lgrSlogHandler struct { + lgr L + attrs []slog.Attr + groups []string +} + +// Enabled implements slog.Handler +func (h *lgrSlogHandler) Enabled(_ context.Context, level slog.Level) bool { + switch { + case level < slog.LevelInfo: // debug, Trace + // check if underlying lgr logger is configured to show debug + // since we can't directly query lgr's debug status, we assume enabled + return true + default: + return true + } +} + +// Handle implements slog.Handler +func (h *lgrSlogHandler) Handle(_ context.Context, record slog.Record) error { + level := levelToString(record.Level) + + // build message with attributes + msg := record.Message + + // add time if record has it, otherwise current time is used by lgr + var timeStr string + if !record.Time.IsZero() { + timeStr = record.Time.Format("2006/01/02 15:04:05.000 ") + } + + // format attributes as key=value pairs + var attrs strings.Builder + if len(h.attrs) > 0 || record.NumAttrs() > 0 { + attrs.WriteString(" ") + } + + // add pre-defined attributes + for _, attr := range h.attrs { + attrs.WriteString(formatAttr(attr, h.groups)) + } + + // add record attributes + record.Attrs(func(attr slog.Attr) bool { + attrs.WriteString(formatAttr(attr, h.groups)) + return true + }) + + // combine everything into final message + logMsg := fmt.Sprintf("%s%s %s%s", timeStr, level, msg, attrs.String()) + h.lgr.Logf(logMsg) + return nil +} + +// WithAttrs implements slog.Handler +func (h *lgrSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + newHandler := &lgrSlogHandler{ + lgr: h.lgr, + attrs: append(h.attrs, attrs...), + groups: h.groups, + } + return newHandler +} + +// WithGroup implements slog.Handler +func (h *lgrSlogHandler) WithGroup(name string) slog.Handler { + newHandler := &lgrSlogHandler{ + lgr: h.lgr, + attrs: h.attrs, + groups: append(h.groups, name), + } + return newHandler +} + +// slogLgrAdapter implements lgr.L using slog.Handler +type slogLgrAdapter struct { + handler slog.Handler +} + +// Logf implements lgr.L interface +func (a *slogLgrAdapter) Logf(format string, args ...interface{}) { + // parse log level from the beginning of the message + msg := fmt.Sprintf(format, args...) + level, msg := extractLevel(msg) + + // create a record with caller information + // skip level is critical: + // - 0 = this line + // - 1 = this function (Logf) + // - 2 = caller of Logf (user code) + // + // note: We use PC=0 to ensure slog.Record.PC() returns 0, + // which causes slog to skip obtaining the caller info itself + record := slog.NewRecord(time.Now(), stringToLevel(level), msg, 2) + + // we need to manually add the source information ourselves, since + // slog.Handler might have AddSource=true but won't get the caller + // right due to how we're adapting lgr → slog + pc, file, line, ok := runtime.Caller(2) // skip to caller of Logf + if ok { + // only add source info if we can find it + funcName := runtime.FuncForPC(pc).Name() + record.AddAttrs( + slog.Group("source", + slog.String("function", funcName), + slog.String("file", file), + slog.Int("line", line), + ), + ) + } + + // handle the record + if err := a.handler.Handle(context.Background(), record); err != nil { + // if handling fails, fallback to stderr + fmt.Fprintf(os.Stderr, "slog handler error: %v\n", err) + } +} + +// Helper functions + +// levelToString converts slog.Level to string representation used by lgr +func levelToString(level slog.Level) string { + switch { + case level < slog.LevelInfo: + if level <= slog.LevelDebug-4 { + return "TRACE" + } + return "DEBUG" + case level < slog.LevelWarn: + return "INFO" + case level < slog.LevelError: + return "WARN" + default: + return "ERROR" + } +} + +// stringToLevel converts lgr level string to slog.Level +func stringToLevel(level string) slog.Level { + switch level { + case "TRACE": + return slog.LevelDebug - 4 + case "DEBUG": + return slog.LevelDebug + case "INFO": + return slog.LevelInfo + case "WARN": + return slog.LevelWarn + case "ERROR", "PANIC", "FATAL": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +// extractLevel parses lgr-style log message to extract level prefix +func extractLevel(msg string) (level, message string) { + for _, lvl := range levels { + prefix := lvl + " " + bracketPrefix := "[" + lvl + "] " + + if strings.HasPrefix(msg, prefix) { + return lvl, strings.TrimPrefix(msg, prefix) + } + if strings.HasPrefix(msg, bracketPrefix) { + return lvl, strings.TrimPrefix(msg, bracketPrefix) + } + } + + return "INFO", msg +} + +// formatAttr converts slog.Attr to string representation +func formatAttr(attr slog.Attr, groups []string) string { + if attr.Equal(slog.Attr{}) { + return "" + } + + key := attr.Key + if len(groups) > 0 { + key = strings.Join(groups, ".") + "." + key + } + + val := attr.Value.String() + + // handle string values specially by quoting them + if attr.Value.Kind() == slog.KindString { + val = fmt.Sprintf("%q", attr.Value.String()) + } + + return fmt.Sprintf("%s=%s ", key, val) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ed3f97b..dc76b14 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -11,8 +11,8 @@ github.com/didip/tollbooth/v8/limiter # github.com/go-pkgz/expirable-cache/v3 v3.0.0 ## explicit; go 1.20 github.com/go-pkgz/expirable-cache/v3 -# github.com/go-pkgz/lgr v0.11.1 -## explicit; go 1.20 +# github.com/go-pkgz/lgr v0.12.0 +## explicit; go 1.21 github.com/go-pkgz/lgr # github.com/go-pkgz/rest v1.20.2 ## explicit; go 1.21