feat(observability): slog-backed logging, Prometheus metrics, grpc health
- internal/logging now wraps log/slog; logging.format json/text finally works (json is the documented default). Same exported API. - New internal/metrics: /metrics on the configured metrics port with notification gauges by status/type, queue depth, and HTTP request count/duration labeled by mux route pattern; sampled from service stats so the service layer stays metrics-agnostic. - Standard grpc.health.v1 health service registered (k8s gRPC probes); gRPC MaxRecvMsgSize bounded to match the REST 1 MB body limit. - Dedicated health listener on health_check.port serving /health and /readyz (probes now work in grpc-only mode); metrics, health, and REST servers all shut down gracefully. - main wires retry backoff, CORS, readiness checks, and TLS from config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+82
-51
@@ -3,15 +3,17 @@ package logging
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logger provides structured logging with ISO 8601 timestamps
|
||||
// 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 {
|
||||
*log.Logger
|
||||
level LogLevel
|
||||
slogger *slog.Logger
|
||||
level LogLevel
|
||||
}
|
||||
|
||||
// LogLevel represents the logging level
|
||||
@@ -24,20 +26,73 @@ const (
|
||||
ErrorLevel
|
||||
)
|
||||
|
||||
// New creates a new logger with ISO 8601 timestamp format
|
||||
// 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{
|
||||
Logger: log.New(output, "", 0), // No flags, we'll format ourselves
|
||||
level: level,
|
||||
slogger: slog.New(newHandler("text", level, output)),
|
||||
level: level,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFromConfig creates a logger from configuration
|
||||
// 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
|
||||
@@ -54,85 +109,66 @@ func NewFromConfig(levelStr string, outputPath string) (*Logger, error) {
|
||||
output = file
|
||||
}
|
||||
|
||||
return New(level, output), nil
|
||||
return &Logger{
|
||||
slogger: slog.New(newHandler(format, level, output)),
|
||||
level: level,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// formatMessage formats a log message with ISO 8601 timestamp
|
||||
func (l *Logger) formatMessage(level string, msg string) string {
|
||||
timestamp := time.Now().UTC().Format(time.RFC3339)
|
||||
return timestamp + " [" + level + "] " + msg
|
||||
// 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) {
|
||||
if l.level <= DebugLevel {
|
||||
l.Logger.Println(l.formatMessage("DEBUG", msg))
|
||||
}
|
||||
l.slogger.Debug(msg)
|
||||
}
|
||||
|
||||
// Debugf logs a formatted debug message
|
||||
func (l *Logger) Debugf(format string, args ...interface{}) {
|
||||
if l.level <= DebugLevel {
|
||||
msg := sprintf(format, args...)
|
||||
l.Logger.Println(l.formatMessage("DEBUG", msg))
|
||||
}
|
||||
l.slogger.Debug(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Info logs an info message
|
||||
func (l *Logger) Info(msg string) {
|
||||
if l.level <= InfoLevel {
|
||||
l.Logger.Println(l.formatMessage("INFO", msg))
|
||||
}
|
||||
l.slogger.Info(msg)
|
||||
}
|
||||
|
||||
// Infof logs a formatted info message
|
||||
func (l *Logger) Infof(format string, args ...interface{}) {
|
||||
if l.level <= InfoLevel {
|
||||
msg := sprintf(format, args...)
|
||||
l.Logger.Println(l.formatMessage("INFO", msg))
|
||||
}
|
||||
l.slogger.Info(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Warn logs a warning message
|
||||
func (l *Logger) Warn(msg string) {
|
||||
if l.level <= WarnLevel {
|
||||
l.Logger.Println(l.formatMessage("WARN", msg))
|
||||
}
|
||||
l.slogger.Warn(msg)
|
||||
}
|
||||
|
||||
// Warnf logs a formatted warning message
|
||||
func (l *Logger) Warnf(format string, args ...interface{}) {
|
||||
if l.level <= WarnLevel {
|
||||
msg := sprintf(format, args...)
|
||||
l.Logger.Println(l.formatMessage("WARN", msg))
|
||||
}
|
||||
l.slogger.Warn(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Error logs an error message
|
||||
func (l *Logger) Error(msg string) {
|
||||
if l.level <= ErrorLevel {
|
||||
l.Logger.Println(l.formatMessage("ERROR", msg))
|
||||
}
|
||||
l.slogger.Error(msg)
|
||||
}
|
||||
|
||||
// Errorf logs a formatted error message
|
||||
func (l *Logger) Errorf(format string, args ...interface{}) {
|
||||
if l.level <= ErrorLevel {
|
||||
msg := sprintf(format, args...)
|
||||
l.Logger.Println(l.formatMessage("ERROR", msg))
|
||||
}
|
||||
l.slogger.Error(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Fatal logs a fatal message and exits
|
||||
// Fatal logs a fatal message at error level and exits
|
||||
func (l *Logger) Fatal(msg string) {
|
||||
l.Logger.Println(l.formatMessage("FATAL", msg))
|
||||
l.slogger.Error(msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted fatal message and exits
|
||||
// Fatalf logs a formatted fatal message at error level and exits
|
||||
func (l *Logger) Fatalf(format string, args ...interface{}) {
|
||||
msg := sprintf(format, args...)
|
||||
l.Logger.Println(l.formatMessage("FATAL", msg))
|
||||
l.slogger.Error(fmt.Sprintf(format, args...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -151,8 +187,3 @@ func parseLevel(levelStr string) LogLevel {
|
||||
return InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
// sprintf is a helper using fmt
|
||||
func sprintf(format string, args ...interface{}) string {
|
||||
return fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user