Logging

Structured logs of what the engine did and when, through log/slog.

Unlike WithMeter and WithTracer, logging needs no bridge module: WithLogger takes a standard library *slog.Logger directly, so it works with the core mamori module alone.

w, err := mamori.Watch[Config](ctx,
	mamori.WithLogger(slog.Default()),
)

mamori logs nothing until you pass this option. The zero configuration is a discard logger, so linking mamori into an application never writes a line to that application’s stderr on its own - a library that did would be making a decision that belongs to the application.

Every event mamori can log:

MessageLevelWhen
resolve failedWarnA one-shot Load, or a Watch’s initial resolve, could not reach the provider or the provider returned an error.
watch errorWarnA runtime watch or poll delivered a non-not-found error for a field.
value is staleWarnIn addition to watch error, once the field has gone unrefreshed longer than the WithStale threshold.
resolve recoveredInfoA field that previously carried an error resolved cleanly (or fell back to a tolerated absence); logged only when there was an error to recover from, so a healthy refresh stays quiet.
candidate rejected by validation; continuing to serve the previous configErrorA reconciled candidate failed struct validation and was discarded.
change rejected by PreApply; continuing to serve the previous configWarnA PreApply hook rejected a candidate.
config change appliedInfoA reconciled snapshot was accepted; logged once per flush, with the number of changed fields.
field updatedDebugOne record per field included in an applied change, so the detail is available without making the Info line unbounded.
change event dropped, dispatch queue full; the OnChange handler is not keeping upWarnThe bounded OnChange dispatch queue was full and the oldest event was dropped.
provider has no native watch, pollingDebugA ref’s provider does not implement WatchableProvider, so mamori falls back to polling it.

Two guarantees hold across every one of these records.

A resolved value never appears. Records carry the field path, the provider scheme, the ref (with sensitive query options like ?token= redacted the same way Status redacts them), the version, and the error - never Value.Bytes or a decoded field. A configuration log is exactly the artifact most likely to be shipped off the host to a collector, so this is tested, not just documented.

The handler runs on the reconciler goroutine, the same constraint OnError carries: a handler that blocks (writing synchronously to a remote collector, for instance) stalls reconciliation for as long as it runs. Buffer it if it needs to do I/O.

WithLogger and OnError are independent and can both be set; an error reaches both, and neither suppresses the other.

See also

  • OpenTelemetry and Prometheus count the same events. A log tells you what happened to one field; a counter tells you how often it happens across all of them, which is what you alert on.
  • Observability answers what is true right now, rather than what happened over time.