Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
130 lines
3.8 KiB
Go
130 lines
3.8 KiB
Go
// Package logging configures structured logging for the service.
|
|
//
|
|
// It wraps log/slog rather than a third-party logger so that the output is
|
|
// plain JSON or plain text with no vendor in the dependency graph, and so that
|
|
// the level and format can be chosen from the environment the Helm chart sets.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Logger is the logging interface the application depends on. Declaring it here
|
|
// keeps handlers and middleware testable with a recorder, and keeps them from
|
|
// importing slog directly.
|
|
type Logger interface {
|
|
Debug(msg string, args ...any)
|
|
Info(msg string, args ...any)
|
|
Warn(msg string, args ...any)
|
|
Error(msg string, args ...any)
|
|
With(args ...any) Logger
|
|
}
|
|
|
|
// Options configures a logger.
|
|
type Options struct {
|
|
// Level is debug, info, warn or error.
|
|
Level string
|
|
// Format is json or text.
|
|
Format string
|
|
// Service is the value of the service.name attribute.
|
|
Service string
|
|
// Environment is the deployment name.
|
|
Environment string
|
|
// Version is the build version, reported in logs.
|
|
Version string
|
|
// Commit is the source revision, reported in logs.
|
|
Commit string
|
|
// BuildDate is the build timestamp, reported in logs.
|
|
BuildDate string
|
|
// Writer overrides the destination, for tests.
|
|
Writer io.Writer
|
|
}
|
|
|
|
// New builds a logger.
|
|
func New(opts Options) Logger {
|
|
writer := opts.Writer
|
|
if writer == nil {
|
|
writer = os.Stdout
|
|
}
|
|
|
|
handlerOpts := &slog.HandlerOptions{Level: parseLevel(opts.Level)}
|
|
|
|
var handler slog.Handler
|
|
if strings.EqualFold(opts.Format, "text") {
|
|
handler = slog.NewTextHandler(writer, handlerOpts)
|
|
} else {
|
|
handler = slog.NewJSONHandler(writer, handlerOpts)
|
|
}
|
|
|
|
logger := slog.New(handler).With(
|
|
slog.String("service", orDefault(opts.Service, "Go Bananas!!")),
|
|
slog.String("environment", opts.Environment),
|
|
slog.String("version", opts.Version),
|
|
slog.String("commit", orDefault(opts.Commit, "unknown")),
|
|
slog.String("built", orDefault(opts.BuildDate, "unknown")),
|
|
)
|
|
return &slogLogger{logger: logger}
|
|
}
|
|
|
|
// parseLevel maps a configuration string onto a slog level, defaulting to info
|
|
// so that a typo never silences the service.
|
|
func parseLevel(raw string) slog.Level {
|
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "warn", "warning":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|
|
|
|
// orDefault returns value when it is not empty, otherwise fallback.
|
|
func orDefault(value, fallback string) string {
|
|
if strings.TrimSpace(value) == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
// slogLogger adapts *slog.Logger to the Logger interface.
|
|
type slogLogger struct {
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func (l *slogLogger) Debug(msg string, args ...any) { l.logger.Debug(msg, args...) }
|
|
func (l *slogLogger) Info(msg string, args ...any) { l.logger.Info(msg, args...) }
|
|
func (l *slogLogger) Warn(msg string, args ...any) { l.logger.Warn(msg, args...) }
|
|
func (l *slogLogger) Error(msg string, args ...any) { l.logger.Error(msg, args...) }
|
|
|
|
func (l *slogLogger) With(args ...any) Logger {
|
|
return &slogLogger{logger: l.logger.With(args...)}
|
|
}
|
|
|
|
// Discard returns a logger that drops everything. It exists so that tests and
|
|
// one-off commands do not have to thread a logger through just to satisfy the
|
|
// dependency.
|
|
func Discard() Logger {
|
|
return &slogLogger{logger: slog.New(slog.NewJSONHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError + 8}))}
|
|
}
|
|
|
|
// FromContext returns the logger stored on a context, or a discarding logger.
|
|
func FromContext(ctx context.Context) Logger {
|
|
if logger, ok := ctx.Value(loggerKey{}).(Logger); ok {
|
|
return logger
|
|
}
|
|
return Discard()
|
|
}
|
|
|
|
type loggerKey struct{}
|
|
|
|
// WithContext stores a logger on a context.
|
|
func WithContext(ctx context.Context, logger Logger) context.Context {
|
|
return context.WithValue(ctx, loggerKey{}, logger)
|
|
}
|