eda033ff9b
Addresses errcheck, gosec, revive, staticcheck, and unused findings
across the codebase (unchecked error returns, unsafe file inclusion
warnings on operator/test-controlled paths, missing package comments,
unused parameters, deprecated API usage). Also fixes two suppression
comments that were silently no-ops due to wrong syntax (#nosec needs
a leading '#', nolint reasons need '//' not '--').
With the backlog clear, drop continue-on-error from the CI lint job
per the plan left in b4b4806.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
195 lines
4.9 KiB
Go
195 lines
4.9 KiB
Go
// Package logging provides structured logging backed by log/slog, with UTC
|
|
// RFC3339 timestamps and a level-gated API compatible with the previous
|
|
// *log.Logger-based implementation.
|
|
package logging
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Logger provides structured logging backed by log/slog, with UTC RFC3339
|
|
// timestamps and a level-gated API compatible with the previous
|
|
// *log.Logger-based implementation.
|
|
type Logger struct {
|
|
slogger *slog.Logger
|
|
level LogLevel
|
|
}
|
|
|
|
// LogLevel represents the logging level
|
|
type LogLevel int
|
|
|
|
// Logging levels, in increasing order of severity. DebugLevel is the most
|
|
// verbose and ErrorLevel the least.
|
|
const (
|
|
DebugLevel LogLevel = iota
|
|
InfoLevel
|
|
WarnLevel
|
|
ErrorLevel
|
|
)
|
|
|
|
// toSlogLevel maps our LogLevel to the equivalent slog.Level.
|
|
func (l LogLevel) toSlogLevel() slog.Level {
|
|
switch l {
|
|
case DebugLevel:
|
|
return slog.LevelDebug
|
|
case InfoLevel:
|
|
return slog.LevelInfo
|
|
case WarnLevel:
|
|
return slog.LevelWarn
|
|
case ErrorLevel:
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|
|
|
|
// replaceAttr normalizes the slog time attribute to a UTC RFC3339 timestamp
|
|
// so log output has a stable, predictable format regardless of handler.
|
|
func replaceAttr(_ []string, a slog.Attr) slog.Attr {
|
|
if a.Key == slog.TimeKey {
|
|
if t, ok := a.Value.Any().(time.Time); ok {
|
|
a.Value = slog.StringValue(t.UTC().Format(time.RFC3339))
|
|
}
|
|
}
|
|
return a
|
|
}
|
|
|
|
// newHandler builds a slog.Handler for the given format ("json" or "text",
|
|
// with "json" as the default), level, and output writer.
|
|
func newHandler(format string, level LogLevel, output io.Writer) slog.Handler {
|
|
opts := &slog.HandlerOptions{
|
|
Level: level.toSlogLevel(),
|
|
ReplaceAttr: replaceAttr,
|
|
}
|
|
|
|
switch format {
|
|
case "text":
|
|
return slog.NewTextHandler(output, opts)
|
|
default:
|
|
return slog.NewJSONHandler(output, opts)
|
|
}
|
|
}
|
|
|
|
// New creates a new logger using the text format with the given level and
|
|
// output writer.
|
|
func New(level LogLevel, output io.Writer) *Logger {
|
|
if output == nil {
|
|
output = os.Stdout
|
|
}
|
|
|
|
return &Logger{
|
|
slogger: slog.New(newHandler("text", level, output)),
|
|
level: level,
|
|
}
|
|
}
|
|
|
|
// NewFromConfig creates a logger from configuration using the json format,
|
|
// matching the documented default for logging.format.
|
|
func NewFromConfig(levelStr string, outputPath string) (*Logger, error) {
|
|
return NewFromOptions(levelStr, "json", outputPath)
|
|
}
|
|
|
|
// NewFromOptions creates a logger from configuration with an explicit
|
|
// format ("json", "text", or "" which defaults to "json"). outputPath may be
|
|
// "stdout", "stderr", "" (defaults to stdout), or a file path, which is
|
|
// opened for append (creating it if necessary).
|
|
func NewFromOptions(levelStr string, format string, outputPath string) (*Logger, error) {
|
|
level := parseLevel(levelStr)
|
|
|
|
var output io.Writer
|
|
switch outputPath {
|
|
case "stdout", "":
|
|
output = os.Stdout
|
|
case "stderr":
|
|
output = os.Stderr
|
|
default:
|
|
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) //nolint:gosec // outputPath is operator-configured (logging.output), not user-controlled input
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
output = file
|
|
}
|
|
|
|
return &Logger{
|
|
slogger: slog.New(newHandler(format, level, output)),
|
|
level: level,
|
|
}, nil
|
|
}
|
|
|
|
// Slog exposes the underlying *slog.Logger for structured call sites.
|
|
func (l *Logger) Slog() *slog.Logger {
|
|
return l.slogger
|
|
}
|
|
|
|
// Debug logs a debug message
|
|
func (l *Logger) Debug(msg string) {
|
|
l.slogger.Debug(msg)
|
|
}
|
|
|
|
// Debugf logs a formatted debug message
|
|
func (l *Logger) Debugf(format string, args ...interface{}) {
|
|
l.slogger.Debug(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Info logs an info message
|
|
func (l *Logger) Info(msg string) {
|
|
l.slogger.Info(msg)
|
|
}
|
|
|
|
// Infof logs a formatted info message
|
|
func (l *Logger) Infof(format string, args ...interface{}) {
|
|
l.slogger.Info(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Warn logs a warning message
|
|
func (l *Logger) Warn(msg string) {
|
|
l.slogger.Warn(msg)
|
|
}
|
|
|
|
// Warnf logs a formatted warning message
|
|
func (l *Logger) Warnf(format string, args ...interface{}) {
|
|
l.slogger.Warn(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Error logs an error message
|
|
func (l *Logger) Error(msg string) {
|
|
l.slogger.Error(msg)
|
|
}
|
|
|
|
// Errorf logs a formatted error message
|
|
func (l *Logger) Errorf(format string, args ...interface{}) {
|
|
l.slogger.Error(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Fatal logs a fatal message at error level and exits
|
|
func (l *Logger) Fatal(msg string) {
|
|
l.slogger.Error(msg)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Fatalf logs a formatted fatal message at error level and exits
|
|
func (l *Logger) Fatalf(format string, args ...interface{}) {
|
|
l.slogger.Error(fmt.Sprintf(format, args...))
|
|
os.Exit(1)
|
|
}
|
|
|
|
// parseLevel parses a log level string
|
|
func parseLevel(levelStr string) LogLevel {
|
|
switch levelStr {
|
|
case "debug":
|
|
return DebugLevel
|
|
case "info":
|
|
return InfoLevel
|
|
case "warn", "warning":
|
|
return WarnLevel
|
|
case "error":
|
|
return ErrorLevel
|
|
default:
|
|
return InfoLevel
|
|
}
|
|
}
|