Files
a850fc9c24 feat(logging): add Trace level to Logger (#608)
* feat(logging): add Trace level to Logger

Add a TraceLevel constant (value -1, more verbose than Debug) and
Trace/Tracef methods to the Logger interface. Trace is intended for
per-event/per-request logging that is too noisy for Debug.

- TraceLevel is exposed alongside Debug/Info/Error in the Level enum.
- LogrusLogger forwards Trace/Tracef to logrus's native Trace level.
- NewDefaultLoggerWithLevel lets callers pick a precise level (the
  existing NewDefaultLogger(w, debug, ...) wrapper is preserved for
  backward compatibility).
- ParseLevel(string) and Level.String() are added for CLI parsing
  and structured logging.
- Package-level Trace/Tracef helpers mirror the existing Debug/Info
  helpers and delegate to FromContext.
- The generated mock is updated; the local mockLogger in
  pkg/authn/licence/licence_test.go now implements Trace/Tracef.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* feat(logging): add ZapLogger adapter with Trace support

Add a zap-backed implementation of the Logger interface, mirroring the
existing LogrusLogger but built on go.uber.org/zap. Trace records are
emitted at a custom zapcore.Level (-2, below DebugLevel) so cores that
opt out of Trace — typically OTel exporter bridges wrapped with
MinLevelCore — silently drop them.

Public surface:
- ZapLogger struct, NewZap(sugar), NopZap() — drop-in Logger backed by
  *zap.SugaredLogger.
- (*ZapLogger).Zap() exposes the underlying *zap.Logger for libraries
  that require it directly (etcd WAL, gRPC instrumentation, ...).
- ToZapLevel(Level) and EncodeLevelWithTrace(base) help build pipelines
  that handle the custom Trace level correctly.
- MinLevelCore wraps a zapcore.Core to enforce a minimum level — meant
  to keep Trace records off the OTel bridge while letting them flow to
  the console core.

This backport unblocks services (ledger-v3) that already maintain a
local copy of this adapter; centralising it here avoids divergence
and keeps the Trace semantics consistent across the platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* chore(logging): regenerate mock to match mockgen output

The hand-edited mock placed Trace/Tracef before Writer instead of in the
order mockgen produces. Running `go generate ./...` in CI flagged the
diff via the "Dirty" check. Regenerated via mockgen to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* test(logging): cover Trace level, ParseLevel, and MinLevelCore

Add unit tests for the new public surface so codecov picks up the patch
coverage:

- ParseLevel: accepts trace|debug|info|error case-insensitively, rejects
  unknown strings.
- Level.String: round-trips the supported levels and falls back to a
  readable form for unknown values.
- ZapLogger: Trace records are emitted when the configured level is
  TraceLevel and silently dropped at DebugLevel; Enabled returns the
  expected truth table across all four levels; WithFields propagates.
- NopZap: never emits and reports Enabled=false.
- MinLevelCore: the canonical pipeline (console accepts Trace, OTel core
  wrapped with MinLevelCore at DebugLevel) routes Trace to the console
  only and lets Debug through both cores.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-02 12:30:32 +00:00

102 lines
2.6 KiB
Go

package logging
import (
"context"
"fmt"
"io"
"strings"
)
// Level represents a logging severity level. Lower values are more verbose.
type Level int
const (
TraceLevel Level = iota - 1 // -1, more verbose than Debug; reserved for per-event/per-request logs.
DebugLevel // 0
InfoLevel // 1
ErrorLevel // 2
)
// ParseLevel parses a textual level (case-insensitive) into a Level.
func ParseLevel(s string) (Level, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "trace":
return TraceLevel, nil
case "debug":
return DebugLevel, nil
case "info":
return InfoLevel, nil
case "error":
return ErrorLevel, nil
default:
return 0, fmt.Errorf("unknown log level: %q (expected trace|debug|info|error)", s)
}
}
// String returns the textual representation of a Level.
func (l Level) String() string {
switch l {
case TraceLevel:
return "trace"
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case ErrorLevel:
return "error"
default:
return fmt.Sprintf("level(%d)", int(l))
}
}
//go:generate mockgen -source logging.go -destination logging_generated.go -package logging . Logger
type Logger interface {
Tracef(fmt string, args ...any)
Debugf(fmt string, args ...any)
Infof(fmt string, args ...any)
Errorf(fmt string, args ...any)
Trace(args ...any)
Debug(args ...any)
Info(args ...any)
Error(args ...any)
WithFields(map[string]any) Logger
WithField(key string, value any) Logger
WithContext(ctx context.Context) Logger
Writer() io.Writer
// Enabled reports whether the logger will emit logs at the given level.
// Use this to guard expensive log calls and avoid unnecessary allocations:
//
// if logger.Enabled(logging.DebugLevel) {
// logger.WithFields(map[string]any{...}).Debugf("...")
// }
Enabled(level Level) bool
}
func Tracef(format string, args ...any) {
FromContext(context.TODO()).Tracef(format, args...)
}
func Debugf(format string, args ...any) {
FromContext(context.TODO()).Debugf(format, args...)
}
func Infof(format string, args ...any) {
FromContext(context.TODO()).Infof(format, args...)
}
func Errorf(format string, args ...any) {
FromContext(context.TODO()).Errorf(format, args...)
}
func Trace(args ...any) {
FromContext(context.TODO()).Trace(args...)
}
func Debug(args ...any) {
FromContext(context.TODO()).Debug(args...)
}
func Info(args ...any) {
FromContext(context.TODO()).Info(args...)
}
func Error(args ...any) {
FromContext(context.TODO()).Error(args...)
}
func WithFields(fields map[string]any) Logger {
return FromContext(context.TODO()).WithFields(fields)
}