Files
Paul Lorenz 0aefe599c9 Install slog and route agent log-level callbacks through common/logging. Fixes #3910
- adds BuildPrettyHandler and BuildHandlerForFormat in common/logging:
  pretty output wraps the hand-rolled logging.PrettyHandler (a direct
  port of pfxlog's; df/dl was dropped after review found level-label
  gaps, so there is no github.com/michaelquigley/df dependency) in the
  AsyncHandler chain; the format-aware builder picks pretty / json /
  text by --log-formatter so default look matches pre-slog
- adds BuildTextHandler so --log-formatter=text emits logrus-TextFormatter-
  style key=value output (level=info msg=...) via a slog TextHandler rather
  than the colored pretty handler, restoring the pre-slog meaning of text
- adds logging.Fatal: a slog-world fatal (slog provides none) that emits
  at LevelFatal durably via SyncEmit, then exits, so hard-exit paths do
  not lose the record to the async queue; converts the controller and
  router startup hard-exit sites from Error+os.Exit / Error+panic to it,
  dropping the router's startup panic
- rewires agentlog.DefaultLogLevelCallbacks onto common/logging:
  SetLogLevel drives logging.SetGlobalLevel (lockstep slog + logrus),
  SetChannelLogLevel and ClearChannelLogLevel drive SetNamedLevel /
  ClearNamedLevel; per-channel overrides become slog-only per design
- adds agentToSlog mapping across the seven canonical levels with an
  Info fallback for forward-compat
- ziti/run Options.PreRun and ziti/tunnel rootPreRun build the slog
  handler chain via logging.BuildHandlerForFormat and call Install;
  --verbose seeds the initial level instead of mutating logrus
  directly; AsyncOptions flags exposed via logging.AddFlags on each
  persistent flag set
- hardens the run command's logging flags: PreRun reads --verbose /
  --log-formatter across the command chain so they are honored at either
  the alias-parent (ziti controller run) or child position, and the
  ziti controller / ziti router alias parents skip their legacy
  pfxlog/logrus PersistentPreRun setup for the run subcommand (which
  installs the slog chain itself), keeping it for sibling subcommands
- adds Phase 7 acceptance tests in common/agentlog: TestInstallInvariant
  covers Out=io.Discard, noop formatter, ReportCaller, and the
  lockstep level mirror after Install; TestEndToEnd_AgentSetLogLevel
  walks the agent set-log-level path end to end across bridged-logrus
  and direct-slog routes; TestPerChannelOverride_AppliesToSlogOnly_NotPfxlog
  confirms the design's slog-only channel semantics
- adds Fatal/Panic durability subprocess tests in common/logging that
  fork the test binary, Install the production handler chain, then
  call logrus.Fatal / logrus.Panic and assert the records reach stderr
  before exit/panic; proves the bridge's SyncEmit path flushes before
  os.Exit
- adds doc/logging.md developer note covering how to write a slog
  line, channel-naming convention, the no-Warn/Error-in-hot-paths
  rule, the operator surface, the migration checklist, AsyncOptions
  tunables, and what's deliberately deferred
- adds doc/design/slog-conversion-plan.md with the code-grounded
  per-package channel inventory, the sdk-golang embedder-injection
  pattern, conversion order with deep analysis for the first four
  chunks, and cross-repo coordination notes
2026-06-15 15:34:31 -04:00

125 lines
4.6 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 agentlog
import (
"context"
"log/slog"
"testing"
"github.com/openziti/ziti/v2/common/agent"
"github.com/openziti/ziti/v2/common/logging"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
// TestAgentToSlogMapping covers the seven canonical levels by name so a
// future renumbering of either enum is caught here, not silently downstream.
func TestAgentToSlogMapping(t *testing.T) {
cases := []struct {
agentLvl agent.LogLevel
slogLvl slog.Level
}{
{agent.PanicLevel, logging.LevelPanic},
{agent.FatalLevel, logging.LevelFatal},
{agent.ErrorLevel, slog.LevelError},
{agent.WarnLevel, slog.LevelWarn},
{agent.InfoLevel, slog.LevelInfo},
{agent.DebugLevel, slog.LevelDebug},
{agent.TraceLevel, logging.LevelTrace},
}
for _, c := range cases {
t.Run(c.agentLvl.String(), func(t *testing.T) {
require.Equal(t, c.slogLvl, agentToSlog(c.agentLvl))
})
}
}
// TestAgentToSlogUnknownFallsBackToInfo proves an out-of-range LogLevel does
// not silence logging; it falls back to Info so log output is preserved.
func TestAgentToSlogUnknownFallsBackToInfo(t *testing.T) {
require.Equal(t, slog.LevelInfo, agentToSlog(agent.LogLevel(99)))
}
// TestDefaultCallbacks_SetLogLevel proves SetLogLevel drives the slog global
// AND logrus.SetLevel via the package-level lockstep helper. This is the path
// `ziti agent set-log-level info` takes.
func TestDefaultCallbacks_SetLogLevel(t *testing.T) {
prevLogrus := logrus.StandardLogger().Level
t.Cleanup(func() { logrus.StandardLogger().SetLevel(prevLogrus) })
configureForTest(t)
cbs := DefaultLogLevelCallbacks()
cbs.SetLogLevel(agent.DebugLevel)
require.Equal(t, slog.LevelDebug, logging.GlobalLevel())
require.Equal(t, logrus.DebugLevel, logrus.StandardLogger().Level)
cbs.SetLogLevel(agent.WarnLevel)
require.Equal(t, slog.LevelWarn, logging.GlobalLevel())
require.Equal(t, logrus.WarnLevel, logrus.StandardLogger().Level)
}
// TestDefaultCallbacks_SetChannelLogLevel proves the channel callback drives
// the named-logger override; the slog Logger returned by logging.For honors
// the override on its next Enabled check.
func TestDefaultCallbacks_SetChannelLogLevel(t *testing.T) {
configureForTest(t)
cbs := DefaultLogLevelCallbacks()
// Global Warn; channel "router.link" overridden to Debug.
cbs.SetLogLevel(agent.WarnLevel)
cbs.SetChannelLogLevel("router.link", agent.DebugLevel)
logger := logging.For("router.link")
require.True(t, logger.Enabled(context.Background(), slog.LevelDebug), "override must lift the channel above the global level")
// Another channel without an override stays at global Warn.
other := logging.For("router.xgress")
require.False(t, other.Enabled(context.Background(), slog.LevelDebug), "non-overridden channels must follow the global level")
}
// TestDefaultCallbacks_ClearChannelLogLevel proves Clear restores the channel
// to the global level live; the channel logger's next Enabled check uses the
// global, even if it was below the previously-overridden level.
func TestDefaultCallbacks_ClearChannelLogLevel(t *testing.T) {
configureForTest(t)
cbs := DefaultLogLevelCallbacks()
cbs.SetLogLevel(agent.WarnLevel)
cbs.SetChannelLogLevel("router.link", agent.DebugLevel)
cbs.ClearChannelLogLevel("router.link")
logger := logging.For("router.link")
require.False(t, logger.Enabled(context.Background(), slog.LevelDebug), "Clear must drop the channel back to the global level")
}
// configureForTest wires a no-op default Registry so the package-level
// logging functions don't panic. The Registry's root is a discard handler
// because tests here only assert level state, not record dispatch.
func configureForTest(t *testing.T) {
t.Helper()
logging.Configure(discardHandler{})
}
type discardHandler struct{}
func (discardHandler) Enabled(context.Context, slog.Level) bool { return true }
func (discardHandler) Handle(context.Context, slog.Record) error { return nil }
func (h discardHandler) WithAttrs([]slog.Attr) slog.Handler { return h }
func (h discardHandler) WithGroup(string) slog.Handler { return h }