Log what a provider does¶
Providers take an optional *slog.Logger. Unset, output is discarded.
Wiring one up¶
Through the registry, where there is no settings struct:
factory, err := forge.Lookup(sourceType)
provider, err := factory(ctx, ep, cfg, forge.WithLogger(logger))
Or directly, where there is:
Both converge on the same normalisation, so there is one code path and one place redaction is applied.
Unset means discard, not default¶
This module never writes to slog.Default. A library that does has taken a
position on your output stream, and you may not find out until it appears in
production logs you did not configure.
Because a discarding logger reports itself disabled, guarded call sites cost nothing:
if logger.Enabled(ctx, slog.LevelDebug) {
logger.Debug("page fetched", "page", n, "items", len(items))
}
What gets logged, and at what level¶
| Level | Used for |
|---|---|
Debug |
Tracing: which credential source won, capability probe hits and misses, pagination page and item counts, early termination, sanitisation firing |
Warn |
Degraded but proceeding: stale auth.env/auth.keychain, a credential source that errored while a later one succeeded, a capped enumeration |
Error |
Reserved. See below |
This module does not log an error it also returns. Reporting one fault twice
makes logs harder to read and takes a decision that belongs to the caller. The
single exception is a recovered panic (forge.ErrProviderPanic), which signals a
defect in a dependency's response handling that an operator should see even if
the caller swallows the error.
The conformance harness checks this, if you let it. Supply
NewProviderWithLogger alongside NewProvider and it builds your provider with
a capturing logger, drives the failing lookups your fixtures describe, and
reports any ERROR-level record:
newProvider := func(logger *slog.Logger) forge.Provider {
p, err := myforge.NewReleaseProvider(ctx, myforge.Settings{Logger: logger, /* … */})
require.NoError(t, err)
return p
}
forgetest.RunProviderConformance(t, forgetest.ConformanceConfig{
NewProvider: func() forge.Provider { return newProvider(nil) },
NewProviderWithLogger: newProvider,
// …
})
Leave it nil and the check is skipped, so the rule is unverified rather than
assumed satisfied. forgetest.CaptureLogger is exported for your own tests too:
it wraps exactly as NormaliseLogger does, so what you assert on is what a
consumer would see, redaction included.
Credentials never reach the logger¶
Two mechanisms, and the order matters.
The guarantee: no credential value is passed to a logger at all. What is logged is which source produced one and whether one was found:
The backstop: whatever logger you supply is wrapped so every attribute
value, including bound attributes, nested groups, errors and Stringers, passes
through redact before reaching your
handler.
The backstop is not the guarantee because redaction recognises well-known token
shapes (ghp_, sk-, AKIA, Slack xox…) and long opaque runs. A self-hosted
GitLab or Gitea token may match none of them. Relying on redaction alone would be
relying on a pattern list to have anticipated your forge.
You can use the wrapper on its own:
It is idempotent (re-wrapping returns the same handler rather than nesting) and never panics, both fuzz-verified.
Sending it to OpenTelemetry¶
observability already produces
an slog.Handler, so the two compose with no coupling in either direction:
provider, err := logs.NewProvider(ctx, settings)
logger := slog.New(logs.Handler(provider, "forge"))
forgeProvider, err := factory(ctx, ep, cfg, forge.WithLogger(logger))
This module depends on neither OpenTelemetry nor observability. The root
package's dependency guard forbids an instrumented client stack, so log/slog
from the standard library is both the cheapest option and the only one that keeps
that guard passing.