mirror of
https://github.com/slackhq/nebula.git
synced 2026-05-11 14:42:02 -07:00
233 lines
7.6 KiB
Go
233 lines
7.6 KiB
Go
// Package logging wires the nebula runtime-reconfigurable slog handler used
|
|
// by nebula.Main and the nebula CLI binaries. Callers build a logger with
|
|
// NewLogger, then call ApplyConfig at startup and from a config reload
|
|
// callback to push logging.level, logging.format, and
|
|
// logging.disable_timestamp changes onto the logger without rebuilding it.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// Config is the subset of *config.C that ApplyConfig reads. Declaring it
|
|
// here keeps the logging package from depending on config directly, which
|
|
// would cycle through the shared test helpers (test.NewLogger imports
|
|
// logging, and config's tests import test). *config.C satisfies this
|
|
// interface structurally with no adapter.
|
|
type Config interface {
|
|
GetString(key, def string) string
|
|
GetBool(key string, def bool) bool
|
|
}
|
|
|
|
// LevelTrace is a custom slog level below Debug, used when logging.level is
|
|
// "trace". slog has no builtin trace level; the value is one step below
|
|
// slog.LevelDebug in slog's 4-point spacing.
|
|
const LevelTrace = slog.Level(-8)
|
|
|
|
// NewLogger returns a *slog.Logger whose level, format, and timestamp
|
|
// emission can be reconfigured at runtime via ApplyConfig and the SSH debug
|
|
// commands. The default configuration is info-level text output so log
|
|
// calls made before ApplyConfig runs still produce output. Timestamps
|
|
// follow slog's default RFC3339Nano format; set logging.disable_timestamp
|
|
// in config to suppress them.
|
|
//
|
|
// ApplyConfig and the SSH commands discover the reconfig surface via
|
|
// structural type-assertion on l.Handler(), so replacement implementations
|
|
// (tests, platform-specific sinks) need only implement the subset of
|
|
// {SetLevel(slog.Level), SetFormat(string) error, SetDisableTimestamp(bool)}
|
|
// they care about. Callers that pass a plain *slog.Logger without these
|
|
// methods get a silent no-op; reconfiguration is always opt-in.
|
|
func NewLogger(w io.Writer) *slog.Logger {
|
|
return slog.New(NewHandler(w))
|
|
}
|
|
|
|
// NewHandler builds the *Handler that NewLogger wraps. Exported for
|
|
// platform-specific sinks (notably cmd/nebula-service/logs_windows.go)
|
|
// that want to wrap the handler with extra behavior, such as tagging each
|
|
// record with its Event Log severity, while still benefiting from all the
|
|
// level / format / timestamp / WithAttrs machinery implemented here.
|
|
func NewHandler(w io.Writer) *Handler {
|
|
root := &handlerRoot{}
|
|
root.level.Set(slog.LevelInfo)
|
|
opts := &slog.HandlerOptions{Level: &root.level}
|
|
return &Handler{
|
|
root: root,
|
|
text: slog.NewTextHandler(w, opts),
|
|
json: slog.NewJSONHandler(w, opts),
|
|
}
|
|
}
|
|
|
|
// handlerRoot carries the reconfiguration state shared by every logger
|
|
// derived from a NewHandler call. All fields are consulted on the log
|
|
// path and updated lock-free.
|
|
type handlerRoot struct {
|
|
level slog.LevelVar
|
|
disableTimestamp atomic.Bool
|
|
// jsonMode picks which of the pre-derived inner handlers Handler.Handle
|
|
// dispatches to. Flipping it propagates instantly to every derived logger
|
|
// without rebuilding or chain-replaying anything.
|
|
jsonMode atomic.Bool
|
|
}
|
|
|
|
// Handler is the slog.Handler returned by NewHandler. It holds two
|
|
// pre-derived slog handlers -- one text, one json -- both built from the
|
|
// same accumulated WithAttrs/WithGroup state. Handle picks which one to
|
|
// dispatch to based on handlerRoot.jsonMode, so a SetFormat call takes
|
|
// effect immediately across the whole process without having to rebuild
|
|
// any derived loggers.
|
|
type Handler struct {
|
|
root *handlerRoot
|
|
text slog.Handler
|
|
json slog.Handler
|
|
}
|
|
|
|
func (h *Handler) Enabled(_ context.Context, l slog.Level) bool {
|
|
return h.root.level.Level() <= l
|
|
}
|
|
|
|
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
|
|
if h.root.disableTimestamp.Load() {
|
|
r.Time = time.Time{}
|
|
}
|
|
if h.root.jsonMode.Load() {
|
|
return h.json.Handle(ctx, r)
|
|
}
|
|
return h.text.Handle(ctx, r)
|
|
}
|
|
|
|
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
if len(attrs) == 0 {
|
|
return h
|
|
}
|
|
return &Handler{
|
|
root: h.root,
|
|
text: h.text.WithAttrs(attrs),
|
|
json: h.json.WithAttrs(attrs),
|
|
}
|
|
}
|
|
|
|
func (h *Handler) WithGroup(name string) slog.Handler {
|
|
if name == "" {
|
|
return h
|
|
}
|
|
return &Handler{
|
|
root: h.root,
|
|
text: h.text.WithGroup(name),
|
|
json: h.json.WithGroup(name),
|
|
}
|
|
}
|
|
|
|
// SetLevel updates the effective log level. Propagates to every derived
|
|
// logger via the shared LevelVar.
|
|
func (h *Handler) SetLevel(level slog.Level) { h.root.level.Set(level) }
|
|
|
|
// GetLevel reports the current log level.
|
|
func (h *Handler) GetLevel() slog.Level { return h.root.level.Level() }
|
|
|
|
// SetFormat flips the output format atomically. Valid formats are "text"
|
|
// and "json". Every derived logger sees the new format on its next Handle
|
|
// call; no rebuild or registration is required.
|
|
func (h *Handler) SetFormat(format string) error {
|
|
switch format {
|
|
case "text":
|
|
h.root.jsonMode.Store(false)
|
|
case "json":
|
|
h.root.jsonMode.Store(true)
|
|
default:
|
|
return fmt.Errorf("unknown log format `%s`. possible formats: %s", format, []string{"text", "json"})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetFormat reports the currently selected format name.
|
|
func (h *Handler) GetFormat() string {
|
|
if h.root.jsonMode.Load() {
|
|
return "json"
|
|
}
|
|
return "text"
|
|
}
|
|
|
|
// SetDisableTimestamp toggles whether Handle zeroes r.Time before
|
|
// dispatching (slog's builtin text/json handlers skip emitting the time
|
|
// attribute on a zero time).
|
|
func (h *Handler) SetDisableTimestamp(v bool) { h.root.disableTimestamp.Store(v) }
|
|
|
|
// ApplyConfig reads logging.level, logging.format, and (optionally)
|
|
// logging.disable_timestamp from c and applies them to l. The reconfig
|
|
// surface is discovered via structural type-assertion on l.Handler(), so
|
|
// foreign handlers silently opt out of whichever capabilities they do not
|
|
// implement.
|
|
//
|
|
// nebula.Main does NOT call this function on your behalf; callers that want
|
|
// config-driven log level / format / timestamp updates invoke it at
|
|
// startup and register it as a reload callback themselves. This keeps the
|
|
// library from mutating an embedder's logger without their say-so.
|
|
func ApplyConfig(l *slog.Logger, c Config) error {
|
|
h := l.Handler()
|
|
|
|
lvl, err := ParseLevel(strings.ToLower(c.GetString("logging.level", "info")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ls, ok := h.(interface{ SetLevel(slog.Level) }); ok {
|
|
ls.SetLevel(lvl)
|
|
}
|
|
|
|
format := strings.ToLower(c.GetString("logging.format", "text"))
|
|
if fs, ok := h.(interface{ SetFormat(string) error }); ok {
|
|
if err := fs.SetFormat(format); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if ts, ok := h.(interface{ SetDisableTimestamp(bool) }); ok {
|
|
ts.SetDisableTimestamp(c.GetBool("logging.disable_timestamp", false))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ParseLevel converts a config-string level name ("trace", "debug", "info",
|
|
// "warn"/"warning", "error", "fatal"/"panic") to a slog.Level. "fatal" and
|
|
// "panic" are accepted for backwards compatibility with pre-slog configs
|
|
// and both map to slog.LevelError.
|
|
func ParseLevel(s string) (slog.Level, error) {
|
|
switch s {
|
|
case "trace":
|
|
return LevelTrace, nil
|
|
case "debug":
|
|
return slog.LevelDebug, nil
|
|
case "info":
|
|
return slog.LevelInfo, nil
|
|
case "warn", "warning":
|
|
return slog.LevelWarn, nil
|
|
case "error":
|
|
return slog.LevelError, nil
|
|
case "fatal", "panic":
|
|
return slog.LevelError, nil
|
|
default:
|
|
return 0, fmt.Errorf("not a valid logging level: %q", s)
|
|
}
|
|
}
|
|
|
|
// LevelName returns a human-readable name for a slog.Level matching the
|
|
// strings accepted by ParseLevel.
|
|
func LevelName(l slog.Level) string {
|
|
switch {
|
|
case l <= LevelTrace:
|
|
return "trace"
|
|
case l <= slog.LevelDebug:
|
|
return "debug"
|
|
case l <= slog.LevelInfo:
|
|
return "info"
|
|
case l <= slog.LevelWarn:
|
|
return "warn"
|
|
default:
|
|
return "error"
|
|
}
|
|
}
|