cf_logs

package module
v0.0.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

caerus-framework-logs

CI codecov License

Caerus Framework — logs component.

A log/slog-based logging component that plugs into the caerus-framework core as the first bootstrap stage, so logging is available to every other component's Init. Adds caller reporting (logrus-style) and optional full stack tracebacks on errors — log/slog plus traceback.

Features

  • log/slog handlers: structured text or JSON output.
  • Caller reporting: attach the calling file/line to every record (WithReportCaller), like logrus ReportCaller.
  • Stack tracebacks: attach a formatted stack traceback to records at or above a configurable level (WithStackTraces + WithStackLevel), with the handler's own frames and slog/runtime internals filtered out.
  • Dynamic level: process-global SetLevel, plus per-component SetLevelFor / ResetLevel so a noisy peer can be traced without flooding the process.
  • Runtime reconfiguration: Reconfigure rebuilds the logger (format, writer, caller reporting, stack traces) and pushes the rebuilt logger to every OnReconfigure / OnReconfigureFor subscriber, so components keep a live logger without polling.
  • Full CaerusComponent lifecycle; no os.Exit, no panics.

Cooperative redaction

Logs prints secrets as [redacted]. Configuration declares which fields are secrets (secret:"redact" on the config struct). This is cooperative: fmt.Sprintf, error strings, and slog.Info("cfg", cfg) on a raw struct still leak. There is no process-wide ReplaceAttr on slog.Default().

Concern What to do Default
Passwords, API keys, DSN userinfo secret:"redact" + cf_logs.RedactedString / cf_configuration.LogArgs Print [redacted]; presence via SecretSet("password", v)password_set=true
HTTP query, body, cookies Stay off in RequestLog (http module) Off
Client IP ClientIP(addr, mode) with full / partial / omit Caller chooses. Pass the address you already trust (RemoteAddr after your proxy policy). Do not pass X-Forwarded-For into this helper — it does not decide whether a header is forged.
log.Info("reload", "password", cf_logs.RedactedString(cfg.Password), "host", cfg.Host)
log.Info("reload", cf_logs.SecretSet("password", cfg.Password), "host", cfg.Host)
log.Info("reload", cf_configuration.LogArgs(cfg)...) // honors secret tags; overlay/Get unchanged

ReplaceAttrSecretKeys("password") is an opt-in handler hook for keys you list. It does not walk structs.

RedactURLUserinfo strips a URL password for error strings. Prefer not wrapping pgx/url.Parse errors that interpolate the raw DSN.

Wiring

Two wiring shapes are supported. Prefer the golden path: seed logs through cf.FrameworkOptions.Logs so core registers the component and binds its config source. Use bare AddComponent only for one-off binaries or tests.

Golden path (FrameworkOptions.Logs)

cf.New always builds logs as the first bootstrap stage. Point it at the logs configuration source (default file config/logs.json, env LOGS_) with the seed’s ConfigSource field:

fw := cf.New(&cf.FrameworkOptions{
	Logs: &cf.LogsSettings{
		Format:       "json",
		Level:        "info",
		ConfigSource: "logs", // Source.Name; Owner is cf_logs.ComponentName
		// Optional forensics (same fields as LogConfig; *bool omit = default):
		// ReportCaller: ptr(true), StackTraces: ptr(true), StackLevel: "error",
	},
	Observability: &cf.ObservabilitySettings{Bind: ":9090", ConfigSource: "observability"},
	Components: []cf.CaerusComponent{
		// chassis + app class …
	},
})
if err := fw.RunWithSignals(context.Background()); err != nil {
	log.Fatal(err)
}

Import _ "github.com/caerus-framework/caerus-framework-logs" (or any cf_logs symbol) so the core factory registers. Peers subscribe in Init with OnReconfigureFor(c.Name(), …) and list cf_logs.ComponentName in GetDependencies:

func (c *CFPostgres) GetDependencies() []string {
	return []string{cf_logs.ComponentName}
}

func (c *CFPostgres) Init(ctx context.Context, fw *cf.CaerusFramework) error {
	if !c.loggerSet {
		if logs, ok := cf.Get[*cf_logs.Logs](fw); ok {
			c.logsSub = logs.OnReconfigureFor(c.Name(), func(l *slog.Logger) { c.log = l })
		}
	}
	c.log.Info("initializing postgresql component")
	return nil
}

func (c *CFPostgres) Shutdown(ctx context.Context) error {
	if c.logsSub != nil {
		c.logsSub.Unsubscribe()
		c.logsSub = nil
	}
	// …
	return nil
}
Simple path (AddComponent)

For a minimal binary that builds logs by hand:

fw := cf.New()
logsComp := cf_logs.New(
	cf_logs.WithWriter(os.Stdout),
	cf_logs.WithFormat(cf_logs.FormatJSON),
	cf_logs.WithLevel(slog.LevelInfo),
	cf_logs.WithReportCaller(true),
	cf_logs.WithStackTraces(true), // traceback on slog.LevelError and above
	cf_logs.WithConfigSource("logs"),
)
_ = fw.AddComponent(logsComp)
// … register the rest, then Run / RunWithSignals
Configuration

Options are construction-time cf_logs.Options:

Option Default Purpose
WithLevel(slog.Level) slog.LevelInfo Process-global minimum (also SetLevel; overrides via SetLevelFor).
WithFormat(Format) FormatText FormatText or FormatJSON.
WithWriter(io.Writer) os.Stdout Output destination.
WithReportCaller(bool) false Add source (file:line) to every record.
WithStackTraces(bool) false Attach a stack traceback to records at/above the stack level.
WithStackLevel(slog.Level) slog.LevelError Threshold for stack tracebacks.
WithConfigSource(string) "" Bind a Source[LogConfig] (owner cf_logs.ComponentName); OnConfigReload applies its value live via ApplyConfig.

cf.LogsSettings (golden seed) mirrors LogConfig: Format, Level, ReportCaller / StackTraces (*bool), StackLevel (string), and ConfigSource. File/env reload still wins after Init.

Level and format names are parseable for config-driven setup via ParseFormat (json/text) and ParseLevel (debug/info/warn|warning/error). Both are case-insensitive; invalid values fail parse (format) or fall back to LevelInfo with an error (level). stack_level uses the same level names.

Config-driven (LogConfig / WithConfigSource)

As a core component, logs takes its option values from the configuration component: register a cf_configuration.Source[cf_logs.LogConfig] owned by cf_logs.ComponentName (config/logs.json in the demoapp). The logs component is notified once at Init with the source's value and again on every change (ApplyConfig): format and explicit caller/stack-trace flags rebuild the logger via Reconfigure; omitted report_caller / stack_traces keep the current values (*bool — omit ≠ false). Level goes through SetLevel so SetLevelFor(component, …) overrides keep working. Invalid values are logged and skipped (last-good kept).

{ "format": "json", "level": "info", "report_caller": true, "stack_traces": false, "stack_level": "error" }

stack_level is the threshold for tracebacks when stack_traces is on (same names as level; empty keeps the current threshold, default error).

Runtime reconfiguration

Logs.Reconfigure(opts ...Option) rebuilds the logger from the given handler-affecting options (WithFormat, WithWriter, WithReportCaller, WithStackTraces, WithStackLevel) and delivers the new logger to every subscriber. WithLevel is not applied by Reconfigure — the level is managed exclusively via SetLevel, and a rebuild preserves the current runtime level.

logs.Reconfigure(cf_logs.WithFormat(cf_logs.FormatJSON), cf_logs.WithReportCaller(true))

Framework components register with OnReconfigureFor(name, fn) (pass Name(), including WithName aliases) and receive a level-filtered logger immediately, then again on every Reconfigure. App code that wants the process-global logger can use OnReconfigure(fn) or Logger(). The returned *cf_logs.Subscription must be Unsubscribed on Shutdown:

sub := logs.OnReconfigureFor(c.Name(), func(l *slog.Logger) { c.log = l })
defer sub.Unsubscribe()
Per-component levels
logs.SetLevel(slog.LevelInfo)                 // process default
logs.SetLevelFor("vpq", slog.LevelDebug)      // only vpq (and WithName aliases)
logs.ResetLevel("vpq")                        // follow global again
_ = logs.LevelFor("vpq")                      // effective minimum for that name

SetLevel / SetLevelFor deliberately do not notify subscribers: the logger pointer is unchanged, and each holder's Leveler observes the change immediately. A component override may be noisier or quieter than the global level. Logs.Shutdown drops all remaining subscribers, so no deliveries happen during teardown.

Reloadable map (same names as level):

{
  "format": "json",
  "level": "info",
  "component_levels": { "interest": "debug" }
}

Keys are component Name() values (WithName("interest")"interest", not the default "vpq"). Applying a map replaces overrides: a name missing from the new map follows the process-global level again. Omit the field to keep last-good; "component_levels": {} clears all overrides. Invalid entries log Error and skip that key.

Component contract

Implements caerusframework.CaerusComponent:

  • Name()"logs" (cf_logs.ComponentName)
  • GetInitOrderStage()caerusframework.LogsStage (first bootstrap stage)
  • Init → no-op (logger already built at construction; peers subscribe later)
  • Shutdown → clears OnReconfigure / OnReconfigureFor subscribers so they stop receiving rebuilt loggers during teardown (the writer is still the caller's concern)

Does not implement MetricsProvider. Bootstrap logs cannot import caerus-framework-observability (cycle). When both are registered, observability’s private logsMetricsCollector scrapes this component and emits logs_info (format, global level, report_caller, stack traces, stack level) plus one logs_component_level sample per SetLevelFor override on every /metrics scrape.

Docs

License

Apache License 2.0 — see LICENSE.

Documentation

Index

Constants

View Source
const ComponentName = "logs"

ComponentName is the framework component name for the logs component. It is the identifier other components use in GetDependencies to require logging.

View Source
const RedactedPlaceholder = "[redacted]"

RedactedPlaceholder is what cooperative secret helpers print instead of a credential. Configuration’s `secret:"redact"` tag and RedactedString both resolve to this string. It is not a fingerprint or a hash.

Variables

This section is empty.

Functions

func ClientIP added in v0.0.7

func ClientIP(addr string, mode IPMode) string

ClientIP formats an already-chosen client identity for a log record. Pass the address the app trusts (for example r.RemoteAddr after your own proxy policy). Do not pass X-Forwarded-For here: this helper does not decide whether a header is forged.

func ParseLevel

func ParseLevel(name string) (slog.Level, error)

ParseLevel converts a canonical level name ("debug", "info", "warn" or "error") into a slog.Level. It returns an error for unknown names.

func RedactURLUserinfo added in v0.0.7

func RedactURLUserinfo(raw string) string

RedactURLUserinfo returns a URL string with the password (and only the password) in userinfo replaced by [redacted]. Username stays. If raw is not a URL or has no userinfo, it is returned unchanged. If parsing fails on a string that looks like a URL with userinfo, the function returns a generic placeholder so a bad DSN cannot leak through %w.

func ReplaceAttrSecretKeys added in v0.0.7

func ReplaceAttrSecretKeys(keys ...string) func(groups []string, a slog.Attr) slog.Attr

ReplaceAttrSecretKeys returns a slog HandlerOptions.ReplaceAttr function that rewrites matching attribute keys to [redacted] when the value is a non-empty string. Opt in on a handler you own; it does not wrap slog.Default and does not walk structs. Prefer RedactedString / secret tags.

func SecretSet added in v0.0.7

func SecretSet(key, v string) slog.Attr

SecretSet is a presence-only attr: key_set=true when v is non-empty, without printing v. Use on reload summaries (“a password exists”) instead of logging the password field.

Types

type Format

type Format int

Format selects the slog handler output format.

const (
	// FormatText emits human-readable key/value lines (slog's TextHandler).
	FormatText Format = iota
	// FormatJSON emits structured JSON lines (slog's JSONHandler).
	FormatJSON
)

func ParseFormat

func ParseFormat(name string) (Format, error)

ParseFormat converts a format name ("json", "text") into a Format. Matching is case-insensitive (like ParseLevel); unknown names return an error.

func (Format) String

func (f Format) String() string

String returns the canonical name of the format.

type IPMode added in v0.0.7

type IPMode string

IPMode selects how ClientIP formats an address for logs.

const (
	// IPFull logs the address as given (after stripping a :port if present).
	IPFull IPMode = "full"
	// IPPartial keeps IPv4 /24 (a.b.c.0) and IPv6 /48. Hostnames are omitted.
	IPPartial IPMode = "partial"
	// IPOmit logs nothing (empty string).
	IPOmit IPMode = "omit"
)

func ParseIPMode added in v0.0.7

func ParseIPMode(name string) (IPMode, error)

ParseIPMode maps full|partial|omit (case-insensitive). Unknown names error.

type LogConfig

type LogConfig struct {
	// Format is "text" or "json". Empty keeps the current format.
	Format string `json:"format,omitempty" yaml:"format,omitempty" env:"FORMAT" flag:"log-format"`
	// Level is "debug", "info", "warn" or "error". Empty keeps the current
	// process-global level.
	Level string `json:"level,omitempty" yaml:"level,omitempty" env:"LEVEL" flag:"log-level"`
	// ReportCaller records the source file:line of every log call. Nil keeps
	// the current setting; explicit true/false overrides.
	ReportCaller *bool `json:"report_caller,omitempty" yaml:"report_caller,omitempty" env:"REPORT_CALLER" flag:"report-caller"`
	// StackTraces attaches a stack traceback to records at or above the stack
	// level (default error). Nil keeps the current setting; explicit true/false
	// overrides.
	StackTraces *bool `json:"stack_traces,omitempty" yaml:"stack_traces,omitempty" env:"STACK_TRACES" flag:"stack-traces"`
	// StackLevel is the threshold for stack tracebacks ("debug", "info", "warn",
	// "error"). Empty keeps the current threshold (default error). Only takes
	// effect when stack traces are enabled.
	StackLevel string `json:"stack_level,omitempty" yaml:"stack_level,omitempty" env:"STACK_LEVEL" flag:"stack-level"`
	// ComponentLevels maps component Name() → level name. Applied via SetLevelFor
	// on load/reload. Keys not listed are ResetLevel'd so a removed map entry
	// follows the process-global level again. Nil/omitted keeps current overrides
	// (API SetLevelFor from code is not wiped). An empty map {} clears all
	// config-owned overrides.
	ComponentLevels map[string]string `json:"component_levels,omitempty" yaml:"component_levels,omitempty"`
}

LogConfig is the file/env/flag-drivable logging configuration loaded through the configuration component as the "logs" source. The logs component cannot read the configuration component directly (import cycle), so the framework delivers the freshly loaded value through OnConfigReload. Empty / nil fields keep the current value (bool switches are *bool so omit ≠ explicit false).

type Logs

type Logs struct {
	// contains filtered or unexported fields
}

Logs is the caerus-framework-logs component. It wraps a *slog.Logger and is registered with the framework as the "logs" component so that every other component can retrieve it via cf.Get[*cf_logs.Logs] or depend on it by name.

The logger is built at construction and can be rebuilt at runtime with Reconfigure. SetLevel changes the process-global minimum; SetLevelFor sets a per-component override used by OnReconfigureFor subscribers. Level changes do not rebuild the logger.

func New

func New(opts ...Option) *Logs

New creates a logs component. Configure it with options; defaults are text format, Info level, os.Stdout, caller reporting off, stack tracebacks off.

func (*Logs) ApplyConfig

func (l *Logs) ApplyConfig(cfg LogConfig)

ApplyConfig applies a LogConfig to the running component. Non-empty Format and non-nil ReportCaller/StackTraces (and non-empty StackLevel) rebuild the logger (delivering the new logger to every OnReconfigure / OnReconfigureFor subscriber); omitted bool fields keep the current forensic settings. Level is applied through SetLevel so per-component overrides (SetLevelFor) keep working. Invalid format/level/stack_level values are logged and skipped (last-good).

func (*Logs) CoreConfigSource

func (l *Logs) CoreConfigSource() ([]cf.ConfigSourceValue, error)

CoreConfigSource implements cf.CoreConfigSource. It declares the logs component's own configuration source; the logs module cannot import the configuration module (the configuration module imports logs), so the framework discovers it among registered components during argv absorption and registers the declaration on the component's behalf.

The source is owned by the component: default file config/<name>.json, env prefix LOGS_, owner cf_logs. An argv redeclaration wins: the --<name> file-path flag ParseFlags registers overrides where the file is read from, and the loaded value reaches the component through OnConfigReload (see WithConfigSource). No source is declared when WithConfigSource was not given.

func (*Logs) Format

func (l *Logs) Format() Format

Format returns the configured output format (text or JSON).

func (*Logs) GetInitOrderStage

func (l *Logs) GetInitOrderStage() cf.Stage

GetInitOrderStage implements cf.CaerusComponent. Logging is the very first bootstrap stage, so it is available to every other component's Init.

func (*Logs) Init

func (l *Logs) Init(ctx context.Context, fw *cf.CaerusFramework) error

Init implements cf.CaerusComponent. It is a no-op: the logger is fully configured at construction time.

func (*Logs) Level

func (l *Logs) Level() slog.Level

Level returns the process-global minimum log level (SetLevel).

func (*Logs) LevelFor

func (l *Logs) LevelFor(name string) slog.Level

LevelFor returns the effective minimum level for name: the SetLevelFor override when present, otherwise the process-global level.

func (*Logs) Logger

func (l *Logs) Logger() *slog.Logger

Logger returns the process-global slog.Logger (filtered by SetLevel). Prefer OnReconfigureFor from framework components so they honor SetLevelFor.

func (*Logs) LoggerFor

func (l *Logs) LoggerFor(name string) *slog.Logger

LoggerFor returns a logger filtered by the named component's level override when set, otherwise by the process-global SetLevel. Each call allocates a new wrapper; do not use it on a hot path. Framework components should subscribe once with OnReconfigureFor(Name(), …) and cache that pointer instead — it is rebuilt only on Reconfigure.

func (*Logs) Name

func (l *Logs) Name() string

Name implements cf.CaerusComponent.

func (*Logs) OnConfigReload

func (l *Logs) OnConfigReload(source string, cfg any)

OnConfigReload implements cf.ConfigReloader. It applies the freshly loaded LogConfig for the source named by WithConfigSource (see ApplyConfig). The configuration component delivers the value directly because the logs module cannot import it. A wrong payload type is logged and ignored (last-good).

func (*Logs) OnReconfigure

func (l *Logs) OnReconfigure(fn func(*slog.Logger)) *Subscription

OnReconfigure registers fn to receive the process-global logger immediately and again every time Reconfigure rebuilds it. Prefer OnReconfigureFor from framework components so SetLevelFor can isolate verbosity. SetLevel changes are deliberately not delivered, since the logger pointer is unchanged.

func (*Logs) OnReconfigureFor

func (l *Logs) OnReconfigureFor(name string, fn func(*slog.Logger)) *Subscription

OnReconfigureFor is like OnReconfigure but the delivered logger honors SetLevelFor(name) when set, otherwise the process-global SetLevel. Pass the component's Name() (including WithName aliases). An empty name behaves like OnReconfigure.

func (*Logs) Overrides

func (l *Logs) Overrides() map[string]slog.Level

Overrides returns a snapshot of per-component level overrides.

func (*Logs) Reconfigure

func (l *Logs) Reconfigure(opts ...Option)

Reconfigure rebuilds the logger from the given construction options and delivers the new logger to every OnReconfigure / OnReconfigureFor subscriber. It applies the handler-affecting options — WithFormat, WithWriter, WithReportCaller, WithStackTraces, WithStackLevel. WithLevel is not applied here: the global level is managed exclusively through SetLevel, and rebuilding preserves the current runtime level and per-component overrides. Subscribers are notified outside the internal lock.

func (*Logs) ReportCaller

func (l *Logs) ReportCaller() bool

ReportCaller returns whether the logger includes caller information.

func (*Logs) ResetLevel

func (l *Logs) ResetLevel(name string)

ResetLevel drops the per-component override for name so it follows SetLevel again. No-op when name is empty or has no override.

func (*Logs) SetLevel

func (l *Logs) SetLevel(level slog.Level)

SetLevel changes the process-global minimum log level at runtime. Components subscribed with OnReconfigureFor keep any SetLevelFor override; others and Logger() observe the new global immediately. SetLevel does not rebuild the logger, so reconfiguration subscribers are not notified.

func (*Logs) SetLevelFor

func (l *Logs) SetLevelFor(name string, level slog.Level)

SetLevelFor sets a per-component minimum log level. name should be the component's Name() (including WithName aliases). The override applies to LoggerFor and OnReconfigureFor subscribers for that name. It does not notify subscribers (the logger pointer is unchanged).

func (*Logs) Shutdown

func (l *Logs) Shutdown(ctx context.Context) error

Shutdown implements cf.CaerusComponent. The writer is the caller's concern; there is nothing to release. Pending reconfiguration subscribers are dropped so they stop receiving deliveries during teardown.

func (*Logs) StackLevel

func (l *Logs) StackLevel() slog.Level

StackLevel returns the level at which stack traces are emitted.

func (*Logs) StackTraces

func (l *Logs) StackTraces() bool

StackTraces returns whether the logger emits stack traces.

type Option

type Option func(*options)

Option configures the logs component at construction time.

func WithConfigSource

func WithConfigSource(name string) Option

WithConfigSource names the configuration source (caerus-framework- configuration) whose LogConfig is applied to the component. The logs module cannot read the configuration component directly (import cycle), so the framework delivers the freshly loaded value through OnConfigReload. The component self-registers the source during argv absorption (default file config/<name>.json, env prefix LOGS_, owner cf_logs); an argv --<name> file-path override wins, and the app may also register its own Source[LogConfig] for a custom default. Until the source loads, construction-time defaults apply.

func WithFormat

func WithFormat(format Format) Option

WithFormat selects the output format, text or JSON (default FormatText).

func WithLevel

func WithLevel(level slog.Level) Option

WithLevel sets the process-global minimum level that is emitted (default slog.LevelInfo). The level can still be changed at runtime with Logs.SetLevel. Reconfigure does not apply WithLevel; the global level is always managed via SetLevel. Per-component overrides use SetLevelFor.

func WithReportCaller

func WithReportCaller(enabled bool) Option

WithReportCaller enables the source (file:line) of the log call to be recorded on every record, like logrus's ReportCaller (default false).

func WithStackLevel

func WithStackLevel(level slog.Level) Option

WithStackLevel sets the threshold at or above which stack tracebacks are attached (default slog.LevelError). It only takes effect when stack traces are enabled.

func WithStackTraces

func WithStackTraces(enabled bool) Option

WithStackTraces attaches a formatted stack traceback to every record at or above the stack level (default false).

func WithWriter

func WithWriter(w io.Writer) Option

WithWriter sets the output destination (default os.Stdout).

type RedactedString added in v0.0.7

type RedactedString string

RedactedString is a secret that may be passed to slog. It implements slog.LogValuer so the cleartext never appears in a record: empty stays empty; any other value becomes [redacted].

This is cooperative. fmt.Sprintf, error strings, and slog.Any on a raw struct still leak. Mark the field, wrap the value, or call configuration’s LogArgs — do not expect a process-wide ReplaceAttr to catch everything.

func (RedactedString) LogValue added in v0.0.7

func (s RedactedString) LogValue() slog.Value

LogValue implements slog.LogValuer.

type Subscription

type Subscription struct {
	// contains filtered or unexported fields
}

Subscription is the handle returned by OnReconfigure. Unsubscribe removes the registered callback so it stops receiving rebuilt loggers. It is idempotent.

func (*Subscription) Unsubscribe

func (s *Subscription) Unsubscribe()

Unsubscribe stops the registered callback from receiving further deliveries.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL