mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 00:35:41 +00:00
ff8ae7cd45
- 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
81 lines
2.4 KiB
Go
81 lines
2.4 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
|
|
|
|
import (
|
|
"log/slog"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestLevelNameRoundTrip(t *testing.T) {
|
|
for _, lvl := range []slog.Level{
|
|
LevelTrace,
|
|
slog.LevelDebug,
|
|
slog.LevelInfo,
|
|
slog.LevelWarn,
|
|
slog.LevelError,
|
|
LevelFatal,
|
|
LevelPanic,
|
|
} {
|
|
name := LevelName(lvl)
|
|
parsed, err := ParseLevel(name)
|
|
require.NoError(t, err, "round-trip should succeed for %v", lvl)
|
|
require.Equal(t, lvl, parsed)
|
|
}
|
|
}
|
|
|
|
func TestLevelNameCanonical(t *testing.T) {
|
|
require.Equal(t, "trace", LevelName(LevelTrace))
|
|
require.Equal(t, "debug", LevelName(slog.LevelDebug))
|
|
require.Equal(t, "info", LevelName(slog.LevelInfo))
|
|
require.Equal(t, "warn", LevelName(slog.LevelWarn))
|
|
require.Equal(t, "error", LevelName(slog.LevelError))
|
|
require.Equal(t, "fatal", LevelName(LevelFatal))
|
|
require.Equal(t, "panic", LevelName(LevelPanic))
|
|
}
|
|
|
|
// TestLevelNameOffsetFallback proves a non-canonical level value (e.g.
|
|
// slog.LevelDebug+1) emits valid lowercase output via slog.Level.String,
|
|
// rather than collapsing to an empty string or panicking. slog renders
|
|
// offsets relative to the nearest standard level, so the exact string depends
|
|
// on slog's own conventions; we just need it to be non-empty and lowercase.
|
|
func TestLevelNameOffsetFallback(t *testing.T) {
|
|
require.Equal(t, "debug+1", LevelName(slog.LevelDebug+1))
|
|
require.Equal(t, "error+1", LevelName(slog.LevelError+1))
|
|
}
|
|
|
|
func TestParseLevelAcceptsCaseAndWarningAlias(t *testing.T) {
|
|
lvl, err := ParseLevel("DEBUG")
|
|
require.NoError(t, err)
|
|
require.Equal(t, slog.LevelDebug, lvl)
|
|
|
|
lvl, err = ParseLevel("warning")
|
|
require.NoError(t, err)
|
|
require.Equal(t, slog.LevelWarn, lvl)
|
|
|
|
lvl, err = ParseLevel(" Trace ")
|
|
require.NoError(t, err)
|
|
require.Equal(t, LevelTrace, lvl)
|
|
}
|
|
|
|
func TestParseLevelRejectsUnknown(t *testing.T) {
|
|
_, err := ParseLevel("bogus")
|
|
require.Error(t, err)
|
|
}
|