- Add package-level documentation with usage examples and architecture details - Document all public types, functions, and methods following godoc conventions - Remove unused logger.Error type and NewError function - Apply consistent documentation style across all packages Packages updated: - apitls: TLS certificate management with automatic renewal - config: Environment-based configuration system - config/depenv: Deployment environment handling - ekko: Enhanced Echo web framework wrapper - kafka: Kafka client wrapper with TLS support - logger: Structured logging with OpenTelemetry integration - tracing: OpenTelemetry distributed tracing setup - types: Shared data structures for NTP Pool project - xff/fastlyxff: Fastly CDN IP range management All tests pass after documentation changes.
213 lines
7.0 KiB
Go
213 lines
7.0 KiB
Go
// Package logger provides structured logging with OpenTelemetry trace integration.
|
|
//
|
|
// This package offers multiple logging configurations for different deployment scenarios:
|
|
// - Text logging to stderr with optional timestamp removal for systemd
|
|
// - OTLP (OpenTelemetry Protocol) logging for observability pipelines
|
|
// - Multi-logger setup that outputs to both text and OTLP simultaneously
|
|
// - Context-aware logging with trace ID correlation
|
|
//
|
|
// The package automatically detects systemd environments and adjusts timestamp handling
|
|
// accordingly. It supports debug level configuration via environment variables and
|
|
// provides compatibility bridges for legacy logging interfaces.
|
|
//
|
|
// Key features:
|
|
// - Automatic OpenTelemetry trace and span ID inclusion in log entries
|
|
// - Configurable log levels via DEBUG environment variable (with optional prefix)
|
|
// - Systemd-compatible output (no timestamps when INVOCATION_ID is present)
|
|
// - Thread-safe logger setup with sync.Once protection
|
|
// - Context propagation for request-scoped logging
|
|
//
|
|
// Environment variables:
|
|
// - DEBUG: Enable debug level logging (configurable prefix via ConfigPrefix)
|
|
// - INVOCATION_ID: Systemd detection for timestamp handling
|
|
package logger
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"log/slog"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
|
|
slogtraceid "github.com/remychantenay/slog-otel"
|
|
slogmulti "github.com/samber/slog-multi"
|
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
|
)
|
|
|
|
// ConfigPrefix allows customizing the environment variable prefix for configuration.
|
|
// When set, environment variables like DEBUG become {ConfigPrefix}_DEBUG.
|
|
// This enables multiple services to have independent logging configuration.
|
|
var ConfigPrefix = ""
|
|
|
|
var (
|
|
textLogger *slog.Logger
|
|
otlpLogger *slog.Logger
|
|
multiLogger *slog.Logger
|
|
)
|
|
|
|
var (
|
|
setupText sync.Once // this sets the default
|
|
setupOtlp sync.Once // this never sets the default
|
|
setupMulti sync.Once // this sets the default, and will always run after the others
|
|
mu sync.Mutex
|
|
)
|
|
|
|
func setupStdErrHandler() slog.Handler {
|
|
programLevel := new(slog.LevelVar) // Info by default
|
|
|
|
envVar := "DEBUG"
|
|
if len(ConfigPrefix) > 0 {
|
|
envVar = ConfigPrefix + "_" + envVar
|
|
}
|
|
|
|
if opt := os.Getenv(envVar); len(opt) > 0 {
|
|
if debug, _ := strconv.ParseBool(opt); debug {
|
|
programLevel.Set(slog.LevelDebug)
|
|
}
|
|
}
|
|
|
|
logOptions := &slog.HandlerOptions{Level: programLevel}
|
|
|
|
if len(os.Getenv("INVOCATION_ID")) > 0 {
|
|
// don't add timestamps when running under systemd
|
|
log.Default().SetFlags(0)
|
|
|
|
logOptions.ReplaceAttr = logRemoveTime
|
|
}
|
|
|
|
logHandler := slogtraceid.OtelHandler{
|
|
Next: slog.NewTextHandler(os.Stderr, logOptions),
|
|
}
|
|
|
|
return logHandler
|
|
}
|
|
|
|
func setupOtlpLogger() *slog.Logger {
|
|
setupOtlp.Do(func() {
|
|
otlpLogger = slog.New(
|
|
newLogFmtHandler(otelslog.NewHandler("common")),
|
|
)
|
|
})
|
|
return otlpLogger
|
|
}
|
|
|
|
// SetupMultiLogger creates a logger that outputs to both text (stderr) and OTLP simultaneously.
|
|
// This is useful for services that need both human-readable logs and structured observability data.
|
|
//
|
|
// The multi-logger combines:
|
|
// - Text handler: Stderr output with OpenTelemetry trace integration
|
|
// - OTLP handler: Structured logs sent via OpenTelemetry Protocol
|
|
//
|
|
// On first call, this logger becomes the default logger returned by Setup().
|
|
// The function is thread-safe and uses sync.Once to ensure single initialization.
|
|
func SetupMultiLogger() *slog.Logger {
|
|
setupMulti.Do(func() {
|
|
textHandler := Setup().Handler()
|
|
otlpHandler := setupOtlpLogger().Handler()
|
|
|
|
multiHandler := slogmulti.Fanout(
|
|
textHandler,
|
|
otlpHandler,
|
|
)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
multiLogger = slog.New(multiHandler)
|
|
slog.SetDefault(multiLogger)
|
|
})
|
|
|
|
return multiLogger
|
|
}
|
|
|
|
// SetupOLTP creates a logger that sends structured logs via OpenTelemetry Protocol.
|
|
// This logger is designed for observability pipelines and log aggregation systems.
|
|
//
|
|
// The OTLP logger formats log messages similarly to the text logger for better
|
|
// compatibility with Loki + Grafana, while still providing structured attributes.
|
|
// Log attributes are available both in the message format and as OTLP attributes.
|
|
//
|
|
// This logger does not become the default logger and must be used explicitly.
|
|
// It requires OpenTelemetry tracing configuration to be set up via the tracing package.
|
|
//
|
|
// See: https://github.com/grafana/loki/issues/14788 for formatting rationale.
|
|
func SetupOLTP() *slog.Logger {
|
|
return setupOtlpLogger()
|
|
}
|
|
|
|
// Setup creates and returns the standard text logger for the application.
|
|
// This is the primary logging function that most applications should use.
|
|
//
|
|
// Features:
|
|
// - Text formatting to stderr with human-readable output
|
|
// - Automatic OpenTelemetry trace_id and span_id inclusion when available
|
|
// - Systemd compatibility: omits timestamps when INVOCATION_ID environment variable is present
|
|
// - Debug level support via DEBUG environment variable (respects ConfigPrefix)
|
|
// - Thread-safe initialization with sync.Once
|
|
//
|
|
// On first call, this logger becomes the slog default logger. If SetupMultiLogger()
|
|
// has been called previously, Setup() returns the multi-logger instead of the text logger.
|
|
//
|
|
// The logger automatically detects execution context:
|
|
// - Systemd: Removes timestamps (systemd adds its own)
|
|
// - Debug mode: Enables debug level logging based on environment variables
|
|
// - OpenTelemetry: Includes trace correlation when tracing is active
|
|
func Setup() *slog.Logger {
|
|
setupText.Do(func() {
|
|
h := setupStdErrHandler()
|
|
textLogger = slog.New(h)
|
|
slog.SetDefault(textLogger)
|
|
})
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if multiLogger != nil {
|
|
return multiLogger
|
|
}
|
|
return textLogger
|
|
}
|
|
|
|
type loggerKey struct{}
|
|
|
|
// NewContext stores a logger in the context for request-scoped logging.
|
|
// This enables passing request-specific loggers (e.g., with request IDs,
|
|
// user context, or other correlation data) through the call stack.
|
|
//
|
|
// Use this to create context-aware logging where different parts of the
|
|
// application can access the same enriched logger instance.
|
|
//
|
|
// Example:
|
|
//
|
|
// logger := slog.With("request_id", requestID)
|
|
// ctx := logger.NewContext(ctx, logger)
|
|
// // Pass ctx to downstream functions
|
|
func NewContext(ctx context.Context, l *slog.Logger) context.Context {
|
|
return context.WithValue(ctx, loggerKey{}, l)
|
|
}
|
|
|
|
// FromContext retrieves a logger from the context.
|
|
// If no logger is stored in the context, it returns the default logger from Setup().
|
|
//
|
|
// This function provides a safe way to access context-scoped loggers without
|
|
// needing to check for nil values. It ensures that logging is always available,
|
|
// falling back to the application's standard logger configuration.
|
|
//
|
|
// Example:
|
|
//
|
|
// log := logger.FromContext(ctx)
|
|
// log.Info("processing request") // Uses context logger or default
|
|
func FromContext(ctx context.Context) *slog.Logger {
|
|
if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
|
|
return l
|
|
}
|
|
return Setup()
|
|
}
|
|
|
|
func logRemoveTime(groups []string, a slog.Attr) slog.Attr {
|
|
// Remove time
|
|
if a.Key == slog.TimeKey && len(groups) == 0 {
|
|
return slog.Attr{}
|
|
}
|
|
return a
|
|
}
|