Files
Paul Lorenz ff8ae7cd45 Add common/logging package: AsyncHandler, level helpers, and chain composition. Fixes #3904
- adds a new common/logging package with custom slog.Level constants for
  Trace (-8), Fatal (12), and Panic (16) extending slog's four standard
  levels, plus LevelName and ParseLevel as the single source of truth for
  canonical lowercase wire names (warn and warning both accepted)
- adds AsyncOptions (QueueSize, BlockThreshold, SummaryInterval) with
  Validate, defaults of 4096 / Warn / 5s, and AddFlags / OptionsFromFlags
  bindings so the package can wire into cobra via spf13/pflag alone
- adds AsyncHandler, a bounded async slog.Handler that hands records to a
  single drain goroutine and onto a downstream handler under a shared
  mutex; records at or above the block threshold block (with a closeNotify
  escape so shutdown cannot deadlock), records below it drop when the
  queue is full and bump a per-level atomic counter
- the drain emits a drop-summary record on each SummaryInterval tick when
  any per-level counter is non-zero, and also counts downstream errors in
  a drain_errors counter that appears in the same summary line; downstream
  errors are also logged once to os.Stderr to avoid slog recursion
- Close signals shutdown and returns immediately; the drain final-flushes
  records that beat the close, emits a final summary if drops occurred,
  and closes drainDone for tests
- SyncEmit bypasses the queue and writes through the downstream handler
  synchronously under the same downstreamMu the drain uses, so fatal/panic
  records are durable before the process exits
- adds boundHandler, which prepends bound attrs to every record flowing
  through it before delegating to its parent; WithAttrs returns a new
  boundHandler whose parent is the receiver's parent (not the receiver
  itself), so a chain of slog.Logger.With calls produces sibling
  boundHandlers at the same chain depth rather than stacking
  wrapper-on-wrapper
- adds groupedHandler, which wraps record attrs in slog.Group(name, ...)
  before delegating; a subsequent WithAttrs creates a boundHandler whose
  parent is the groupedHandler, so the attrs land inside the group
- AsyncHandler.WithAttrs and WithGroup are the real chain entry points;
  empty attrs and empty group names return the receiver so no-op
  slog.Logger.With() and WithGroup("") allocate nothing
- covers the lot with -race tests: level round-trip and offset fallback,
  defaults validity and bad-value rejection, flag round-trip, async normal
  flow, drop-on-full with summary attrs, block at the threshold, Close
  idempotent + non-blocking + unblocks Handle, Handle racing Close never
  panics, SyncEmit synchronous and serialized with the drain, drain-error
  counting, the four worked examples from the design doc for the chain
  (with-then-group, group-then-with, nested groups, basic with-attrs),
  no-nesting on repeated WithAttrs, sibling-loggers-do-not-leak-attrs,
  WithGroup("") and WithAttrs(nil) as no-ops on all three handler types,
  and Enabled delegation through the chain
2026-06-12 19:34:17 -04:00

86 lines
2.5 KiB
Go

/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package logging is the async slog sink that ziti's logging refactor lands on.
// It owns the seven canonical log levels (trace through panic), the bounded
// AsyncHandler, and the SyncEmit path used for fatal/panic durability.
package logging
import (
"log/slog"
"strings"
"github.com/pkg/errors"
)
// Custom slog.Level values extending slog's four standard levels (Debug=-4,
// Info=0, Warn=4, Error=8) with Trace below Debug and Fatal and Panic above
// Error. Together these cover the seven canonical level names ziti carries on
// the agent IPC wire.
const (
LevelTrace slog.Level = -8
LevelFatal slog.Level = 12
LevelPanic slog.Level = 16
)
// LevelName returns the canonical lowercase name for a slog.Level on the wire.
// All seven canonical levels (trace, debug, info, warn, error, fatal, panic)
// map to their canonical names. Non-canonical values (e.g. slog.LevelDebug+1)
// fall back to a lowercased slog.Level.String(); slog renders those as
// "DEBUG+1" / "ERROR+4", so the wire never carries silent garbage.
func LevelName(l slog.Level) string {
switch l {
case LevelTrace:
return "trace"
case slog.LevelDebug:
return "debug"
case slog.LevelInfo:
return "info"
case slog.LevelWarn:
return "warn"
case slog.LevelError:
return "error"
case LevelFatal:
return "fatal"
case LevelPanic:
return "panic"
}
return strings.ToLower(l.String())
}
// ParseLevel converts a canonical level name (case-insensitive) into a
// slog.Level, returning an error for unrecognized names. Both "warn" and
// "warning" are accepted.
func ParseLevel(name string) (slog.Level, error) {
switch strings.ToLower(strings.TrimSpace(name)) {
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":
return LevelFatal, nil
case "panic":
return LevelPanic, nil
}
return 0, errors.Errorf("invalid log level %q", name)
}