127 lines
3.6 KiB
Go
127 lines
3.6 KiB
Go
// Package logging provides the single Logrus output for both service roles.
|
|
// The slog handler keeps existing structured call sites and injected loggers,
|
|
// without maintaining a second output or a second level configuration.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func Configure(role string, debug bool, output io.Writer) {
|
|
backend := logrus.New()
|
|
backend.SetOutput(output)
|
|
backend.SetFormatter(&logrus.JSONFormatter{TimestampFormat: time.RFC3339Nano, DisableHTMLEscape: true})
|
|
if debug {
|
|
backend.SetLevel(logrus.DebugLevel)
|
|
} else {
|
|
backend.SetLevel(logrus.InfoLevel)
|
|
}
|
|
slog.SetDefault(slog.New(&handler{backend: backend, fields: logrus.Fields{"role": role}}))
|
|
}
|
|
|
|
type handler struct {
|
|
backend *logrus.Logger
|
|
fields logrus.Fields
|
|
group string
|
|
}
|
|
|
|
func level(l slog.Level) logrus.Level {
|
|
switch {
|
|
case l >= slog.LevelError:
|
|
return logrus.ErrorLevel
|
|
case l >= slog.LevelWarn:
|
|
return logrus.WarnLevel
|
|
case l >= slog.LevelInfo:
|
|
return logrus.InfoLevel
|
|
default:
|
|
return logrus.DebugLevel
|
|
}
|
|
}
|
|
func (h *handler) Enabled(_ context.Context, l slog.Level) bool {
|
|
return h.backend.IsLevelEnabled(level(l))
|
|
}
|
|
func (h *handler) Handle(_ context.Context, r slog.Record) error {
|
|
fields := h.copyFields()
|
|
r.Attrs(func(a slog.Attr) bool { h.add(fields, a); return true })
|
|
h.backend.WithFields(fields).WithTime(r.Time).Log(level(r.Level), cleanString(r.Message))
|
|
return nil
|
|
}
|
|
func (h *handler) copyFields() logrus.Fields {
|
|
fields := make(logrus.Fields, len(h.fields)+8)
|
|
for k, v := range h.fields {
|
|
fields[k] = v
|
|
}
|
|
return fields
|
|
}
|
|
func (h *handler) add(fields logrus.Fields, a slog.Attr) {
|
|
a.Value = a.Value.Resolve()
|
|
if a.Equal(slog.Attr{}) {
|
|
return
|
|
}
|
|
if a.Value.Kind() == slog.KindGroup {
|
|
group := h.group
|
|
if a.Key != "" {
|
|
group += a.Key + "."
|
|
}
|
|
nested := *h
|
|
nested.group = group
|
|
for _, child := range a.Value.Group() {
|
|
nested.add(fields, child)
|
|
}
|
|
return
|
|
}
|
|
fields[h.group+a.Key] = safeValue(a.Key, a.Value.Any())
|
|
}
|
|
func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
clone := *h
|
|
clone.fields = h.copyFields()
|
|
for _, a := range attrs {
|
|
clone.add(clone.fields, a)
|
|
}
|
|
return &clone
|
|
}
|
|
func (h *handler) WithGroup(name string) slog.Handler {
|
|
if name == "" {
|
|
return h
|
|
}
|
|
clone := *h
|
|
clone.group += name + "."
|
|
return &clone
|
|
}
|
|
|
|
// Body observes actual boundary bytes. Redaction happens only when Debug is on
|
|
// and never mutates the original data. Unsafe/malformed bodies produce an
|
|
// explicit diagnostic, not an unsanitized fallback or a successful parse claim.
|
|
func Body(ctx context.Context, message string, body []byte, attrs ...any) {
|
|
if !slog.Default().Enabled(ctx, slog.LevelDebug) {
|
|
return
|
|
}
|
|
attrs = append(append([]any{}, attrs...), "body_bytes", len(body), "body_sha256", fmt.Sprintf("%x", sha256.Sum256(body)))
|
|
logObservedBody(ctx, message, body, "json", RedactJSON, attrs...)
|
|
}
|
|
|
|
func TextBody(ctx context.Context, message string, body []byte, attrs ...any) {
|
|
if !slog.Default().Enabled(ctx, slog.LevelDebug) {
|
|
return
|
|
}
|
|
attrs = append(append([]any{}, attrs...), "body_bytes", len(body), "body_sha256", fmt.Sprintf("%x", sha256.Sum256(body)))
|
|
logObservedBody(ctx, message, body, "utf-8", RedactText, attrs...)
|
|
}
|
|
|
|
func logObservedBody(ctx context.Context, message string, body []byte, encoding string, redact func([]byte) (string, error), attrs ...any) {
|
|
safe, err := redact(body)
|
|
if err != nil {
|
|
attrs = append(attrs, "redaction_error", err.Error(), "body", "[OMITTED: unsafe or invalid JSON]")
|
|
} else {
|
|
attrs = append(attrs, "body", safe, "body_encoding", encoding)
|
|
}
|
|
slog.DebugContext(ctx, message, attrs...)
|
|
}
|