logger

package module
v0.2.0 Latest Latest
Warning

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

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

README

dagstack-logger-go

Go binding for dagstack/logger-spec — OTel-compatible structured logging with named loggers, context propagation, scoped sink overrides, and dagstack JSON-lines wire format.

Status: v0.2.0 (Phase 1). Phase 1 sinks (Console, File, InMemory) are implemented, plus the Phase 1 redaction-config public API. OTLPSink, processor chain, and self-metrics arrive in Phase 2.

Installation

go get go.dagstack.dev/logger

(The Go vanity URL go.dagstack.dev/logger resolves to github.com/dagstack/logger-go.)

Usage

package main

import (
    "context"
    "errors"
    "os"

    "go.dagstack.dev/logger"
)

func main() {
    logger.Configure(
        logger.WithRootLevel("INFO"),
        logger.WithSinks(logger.NewConsoleSink(logger.ConsoleAuto, os.Stderr, 1)),
        logger.WithResourceAttributes(map[string]any{
            "service.name":    "pilot-app",
            "service.version": "0.1.0",
        }),
    )

    log := logger.Get("dagstack.rag.retriever", "1.4.2")
    log.Info("query received", logger.Attrs{"user.id": 42})

    if err := doWork(); err != nil {
        log.Exception(err, logger.Attrs{"request.id": "req-abc"})
    }

    log.Close()
}

func doWork() error {
    return errors.New("simulated failure")
}
Context propagation
ctx, span := tracer.Start(ctx, "operation")
defer span.End()

log := logger.Get("dagstack.rag")
log.InfoCtx(ctx, "in span") // trace_id, span_id auto-injected from ctx
Scoped sink overrides
mem := logger.NewInMemorySink(100, 1)
scoped := log.WithSinks(mem)
scoped.Info("captured only here")

records := mem.Records()
// records[0].Body == "captured only here"

For lexically bounded scope:

err := log.ScopeSinks(ctx, []logger.Sink{mem}, func(ctx context.Context) error {
    return runAgentPipeline(ctx)
})

Design choices specific to Go

  • context.Context first. Logger.InfoCtx(ctx, ...) reads OTel trace state from ctx; Logger.Info(...) is provided for migration but skips trace propagation.
  • PascalCase exports. Logger.Info, Logger.WithSinks, Configure(...). Functional-options pattern (WithRootLevel, WithSinks, WithPerLoggerLevels, WithResourceAttributes).
  • Errors as values. Logger.Flush(timeout) returns *FlushResult, error; sink Close() returns error. No panics from the public API.
  • go.opentelemetry.io/otel/trace for context propagation — no parallel context implementation.
  • Variadic-typed attrs. Attributes are passed as Attrs (a map[string]any alias) plus optional positional slog.Attr-like helpers in v0.2.

Roadmap

  • v0.1.0 — Phase 1 sinks (Console, File, InMemory), severity emits, child loggers, scoped overrides, OTel context propagation, dagstack JSON-lines wire format, default redaction.
  • v0.2.0 (current) — Phase 1 redaction-config public API (RedactionConfig + WithRedactionConfig), Reset(), WithAutoInjectTraceContext cross-binding parity flag, ConsoleSink TTY detection via golang.org/x/term, RFC 8785 UTF-16 key sort. See CHANGELOG.md for the full list.
  • v0.3.0 — processor chain (redaction extra patterns, samplers), slog.Attr-style variadic attrs, LoggerCtx helpers.
  • v0.4.0OTLPSink (HTTP+protobuf), self-metrics via OTel Metrics SDK, runtime reconfigure.

Specification

Normative decisions live in dagstack/logger-spec ADR-0001 v1.2.

Local development

git clone git@github.com:dagstack/logger-go.git
cd logger-go
make test           # go test -race ./...
make vet            # go vet
make coverage       # coverage report
make check          # full validation pass

License

Apache-2.0 — see LICENSE.

Documentation

Overview

Package logger is the Go binding for dagstack/logger-spec — OTel-compatible structured logging with named loggers, automatic context propagation, scoped sink overrides, and a dagstack JSON-lines wire format.

Spec: https://github.com/dagstack/logger-spec (ADR-0001 v1.2). Reference implementation: https://github.com/dagstack/logger-python.

Status: v0.2.0 (Phase 1). Phase 1 sinks (Console, File, InMemory) are implemented, plus the Phase 1 redaction-config public API. OTLPSink, processor chain, and self-metrics arrive in Phase 2.

Typical usage:

logger.Configure(
    logger.WithRootLevel("INFO"),
    logger.WithSinks(logger.NewConsoleSink(logger.ConsoleAuto, os.Stderr, 1)),
    logger.WithResourceAttributes(map[string]any{
        "service.name": "pilot-app",
    }),
)
log := logger.Get("dagstack.rag.retriever", "1.4.2")
log.Info("query received", logger.Attrs{"user.id": 42})

Context propagation reads OTel trace state from the supplied context.Context. Use the *Ctx variants of the severity methods to enable trace_id/span_id auto-injection:

log.InfoCtx(ctx, "in span", nil)

Scoped sink overrides per spec §6:

mem := logger.NewInMemorySink(100, 1)
scoped := log.WithSinks(mem)
scoped.Info("captured only here", nil)

For lexically bounded scope, use ScopeSinks (callback-style; the spec's "Go ctx + defer" idiom is also exposed via WithSinks + manual restoration):

err := log.ScopeSinks(ctx, []logger.Sink{mem}, func(ctx context.Context) error {
    return runAgentPipeline(ctx)
})

Phase 1 does not support runtime watch — OnReconfigure registers a Subscription with Active=false, InactiveReason is set to a diagnostic string, and the callback never fires.

Index

Constants

View Source
const (
	SeverityTextTrace = "TRACE"
	SeverityTextDebug = "DEBUG"
	SeverityTextInfo  = "INFO"
	SeverityTextWarn  = "WARN"
	SeverityTextError = "ERROR"
	SeverityTextFatal = "FATAL"
)

Canonical severity_text strings — exactly the 6 OTel-recommended values per spec §2. Backends filter by exact match.

View Source
const RedactedPlaceholder = "***"

RedactedPlaceholder is the string used to replace secret-suffix attribute values per spec ADR-0001 §10.1 — a literal "***".

Variables

CanonicalSeverityTexts is the ordered set of the 6 canonical severity_text strings. Bindings must not emit any other value for severity_text.

View Source
var DefaultBaggageKeys = []string{"tenant.id", "request.id", "user.id"}

DefaultBaggageKeys is the allow-list of W3C Baggage entries auto-injected into LogRecord attributes per spec ADR-0001 §3.4. Other baggage entries are skipped to avoid leaking arbitrary cross-service context into logs.

Phase 1 delivers the trace-context portion only; baggage extraction is gated on availability of the OTel baggage API and can be enabled in a future minor release without breaking the surface.

View Source
var DefaultSecretSuffixes = []string{
	"_key",
	"_secret",
	"_token",
	"_password",
	"_passphrase",
	"_credentials",
}

DefaultSecretSuffixes is the canonical set of suffix patterns matched case-insensitively against attribute keys. Per spec §10.1 / §10.4 v1.1 this is an opinionated 6-element subset of `config-spec/_meta/secret_patterns.yaml`. The list is fixed at v1.1 to preserve API stability; richer matchers ship via the Phase 2 processor pipeline (§10.3).

A key whose lowercased form ends with any of these suffixes is treated as secret and its value is replaced with RedactedPlaceholder before serialization.

Functions

func ActiveTraceContext

func ActiveTraceContext(ctx context.Context) (traceID, spanID []byte, traceFlags uint8)

ActiveTraceContext extracts the active OTel SpanContext from ctx and returns its (TraceID, SpanID, TraceFlags) triple ready for LogRecord fields. When no valid span context is present, returns (nil, nil, 0).

Per spec §3.4 — the binding MUST use the OTel Context API as the source of trace state, not its own context implementation.

func BuildEffectiveSuffixes added in v0.2.0

func BuildEffectiveSuffixes(cfg RedactionConfig) []string

BuildEffectiveSuffixes produces the post-Configure suffix list applied during emit. When ReplaceDefaults=false, the result is the union of DefaultSecretSuffixes and ExtraSuffixes (deduplicated, lowercased). When ReplaceDefaults=true, only ExtraSuffixes are returned (also deduplicated and lowercased). Returns a non-nil empty slice when the resulting set is empty (replace-all-off mode); callers MUST treat that as "no suffix-based masking" — never fall back to DefaultSecretSuffixes silently, otherwise replace mode loses its intent.

BuildEffectiveSuffixes does NOT validate cfg.ExtraSuffixes — callers passing untrusted input MUST run ValidateRedactionConfig first, or use WithRedactionConfig which validates at option-construction time.

func CanonicalJSONMarshal

func CanonicalJSONMarshal(v any) ([]byte, error)

CanonicalJSONMarshal serializes a value into a canonical JSON byte slice per RFC 8785 subset (config-spec §9.1.1).

Rules:

  • Sorted object keys (lexicographic UTF-16 code-unit order per RFC 8785 §3.2.3 — matches the cross-binding canonical sort applied by dagstack-logger Python and @dagstack/logger TypeScript).
  • No whitespace except inside strings.
  • Integers without a decimal point ("1"); floats use shortest round-trip.
  • "-0.0" → "0.0" (RFC 8785 §3.2.2.3).
  • NaN / ±Infinity → error.
  • Non-string map keys → error.
  • UTF-8 strings pass through unescaped (HTML-safe escaping disabled).

The implementation mirrors logger-python's canonical_json.canonical_json_dumps to guarantee byte-identical output across bindings.

func CanonicalJSONMarshalString

func CanonicalJSONMarshalString(v any) (string, error)

CanonicalJSONMarshalString is a convenience wrapper that returns the result as a string.

func Configure

func Configure(opts ...ConfigureOption)

Configure applies the bootstrap options to the global logger state.

The root logger is updated atomically: min-severity, sinks, per-logger level overrides, and the Resource attribute set. Unspecified groups preserve their previous values when called more than once (so a partial reconfigure stays safe).

Invalid severity strings are rejected at option-construction time — WithRootLevel and WithPerLoggerLevels panic if the string does not resolve to a canonical level (TRACE / DEBUG / INFO / WARN / ERROR / FATAL or a 1–24 integer). Callers typically resolve options once at startup, where a panic is acceptable (recover-on-startup is a tested pattern). Configure itself does not validate further once the options have been built.

func DecodeSpanID

func DecodeSpanID(hexStr string) ([]byte, error)

DecodeSpanID decodes a 16-character hex string into an 8-byte span_id. Returns nil when hexStr is empty.

func DecodeTraceID

func DecodeTraceID(hexStr string) ([]byte, error)

DecodeTraceID decodes a 32-character hex string into a 16-byte trace_id. Returns nil when hexStr is empty.

func EncodeSpanID

func EncodeSpanID(spanID []byte) (string, error)

EncodeSpanID encodes an 8-byte span_id as a 16-character lowercase hex string. Returns an empty string when spanID is nil.

func EncodeTraceID

func EncodeTraceID(traceID []byte) (string, error)

EncodeTraceID encodes a 16-byte trace_id as a 32-character lowercase hex string. Per spec ADR-0001 §1 — wire-format encoding for dagstack JSON-lines and OTel JSON. Returns an empty string when traceID is nil.

func IsCanonicalSeverityText

func IsCanonicalSeverityText(text string) bool

IsCanonicalSeverityText reports whether text is one of the 6 canonical OTel-recommended strings.

func IsSecretKey

func IsSecretKey(key string, suffixes []string) bool

IsSecretKey reports whether key matches any of the suffix patterns, case-insensitively. The suffixes parameter allows callers to override the default set; pass nil to use DefaultSecretSuffixes.

func IsValidSeverityNumber

func IsValidSeverityNumber(severityNumber int) bool

IsValidSeverityNumber reports whether severityNumber is in the valid [1, 24] range.

func Reset added in v0.2.0

func Reset()

Reset clears the global Logger registry — restores root defaults.

For test isolation: call between tests that mutate logger state via SetSinks / SetMinSeverity / SetResource or via Configure. After Reset, a subsequent Get(name) creates a fresh node with no inherited overrides.

SAFETY: this is a coarse instrument — it invalidates ALL logger handles still held elsewhere. Production code MUST NOT call this; it is reserved for test fixtures and the binding's own teardown.

func SeverityTextFor

func SeverityTextFor(severityNumber int) (string, error)

SeverityTextFor maps a severity_number in [1, 24] to its canonical severity_text string. Returns an error if severity_number is out of range.

func ToDagstackJSONL

func ToDagstackJSONL(record *LogRecord) (string, error)

ToDagstackJSONL serializes a LogRecord as a single canonical JSON line (no trailing newline). Each sink is responsible for adding the LF separator between records.

func ToDagstackJSONLDict

func ToDagstackJSONLDict(record *LogRecord) (map[string]any, error)

ToDagstackJSONLDict converts a LogRecord into a Go map ready for canonical JSON serialization. Per spec ADR-0001 §1 — dagstack JSON-lines wire format uses snake_case field names, lowercase hex trace_id/span_id, and integer nanoseconds for timestamps.

Empty / zero fields are omitted from the result for cleaner diagnostics:

  • observed_time_unix_nano omitted when zero
  • attributes omitted when empty
  • instrumentation_scope omitted when nil
  • resource omitted when nil or empty
  • trace_id / span_id omitted when nil
  • trace_flags omitted when zero

func ValidateRedactionConfig added in v0.2.0

func ValidateRedactionConfig(cfg RedactionConfig) error

ValidateRedactionConfig returns an error when cfg.ExtraSuffixes contains an entry that is empty, whitespace-bearing, or not lowercase ASCII. Per spec §10.4 the binding MUST reject these at Configure time.

Types

type Attrs

type Attrs = map[string]any

Attrs is a convenience alias for the per-record attribute map. Per spec ADR-0001 §1, attributes is a Map<string, Value> where Value is a recursive sum type: string | int | float | bool | nil | map[string]Value | []Value.

In Go we use any (interface{}) as the value type to avoid an unwieldy recursive type alias; runtime checks in the wire emitter validate that values are JSON-serializable.

func RedactAttributes

func RedactAttributes(attrs Attrs, suffixes []string) Attrs

RedactAttributes returns a new attribute map where values of keys matching the secret suffixes are replaced with RedactedPlaceholder. The redaction is recursive both for nested map[string]any values and for []any slices whose items are map[string]any (per spec §10.2). A secret key buried inside a list of maps (for example, an event-stream payload) is masked even though the list key itself is not secret.

The original attrs map is not mutated. Pass nil for suffixes to use DefaultSecretSuffixes.

type ConfigureOption

type ConfigureOption func(*configureState)

ConfigureOption mutates the bootstrap state applied by Configure. Use the `With*` constructors below to build options; the option set is open for extension without breaking source compatibility.

func WithAutoInjectTraceContext added in v0.2.0

func WithAutoInjectTraceContext(enabled bool) ConfigureOption

WithAutoInjectTraceContext is the cross-binding parity flag declared in spec ADR-0001 v1.2 §3.4.2. The Go binding's idiomatic API surface is the explicit-ctx mode (Get(name).InfoCtx(ctx, ...) etc.) per §3.4.1; the auto-inject mode is **declared unsupported** in this binding to honour Go's no-implicit-context invariant.

Calling this option with `enabled=false` is a no-op and is provided so cross-binding configurations (Python / TypeScript) that explicitly set the flag to `false` can keep the Go-side configure call symmetric.

Calling with `enabled=true` panics at option-construction time per §3.4.1 — a binding without an ambient-context primitive MUST surface a configuration error rather than silently no-op. Use the *Ctx severity methods (InfoCtx / ErrorCtx / etc.) for explicit context propagation.

func WithPerLoggerLevels

func WithPerLoggerLevels(levels map[string]any) ConfigureOption

WithPerLoggerLevels overrides min-severity for the listed logger names. Useful for silencing noisy upstreams (e.g., {"net/http": "WARN"}).

func WithRedactionConfig added in v0.2.0

func WithRedactionConfig(cfg RedactionConfig) ConfigureOption

WithRedactionConfig registers a Phase 1 redaction policy (spec §10.4). The configured suffix list applies to every emit through the root logger; child loggers inherit unless their own redaction is set later.

Validation runs at option-construction time — invalid suffixes (empty, whitespace, non-lowercase-ASCII) panic so the misconfiguration surfaces at startup, never at the first emit. This matches the Configure philosophy already used by WithRootLevel / WithPerLoggerLevels.

Default behaviour (no WithRedactionConfig call) keeps the 6-element DefaultSecretSuffixes set.

func WithResourceAttributes

func WithResourceAttributes(attrs Attrs) ConfigureOption

WithResourceAttributes installs process/service-level attributes on the root logger Resource (per spec §4.2). Inherited by all loggers.

func WithRootLevel

func WithRootLevel(level any) ConfigureOption

WithRootLevel sets the default minimum severity threshold for the root logger. The level argument may be a string (case-insensitive name like "INFO" or "warn") or an integer in the [1, 24] range.

func WithSinks

func WithSinks(sinks ...Sink) ConfigureOption

WithSinks attaches the provided sinks to the root logger. Children inherit unless they declare their own sinks.

type ConsoleMode

type ConsoleMode int

ConsoleMode selects the output style of ConsoleSink per spec ADR-0001 §7.2.

const (
	// ConsoleAuto selects pretty when the stream is a TTY, json otherwise.
	ConsoleAuto ConsoleMode = iota
	// ConsoleJSON forces canonical JSON-lines output.
	ConsoleJSON
	// ConsolePretty forces single-line colored output (ANSI escape codes).
	ConsolePretty
)

func (ConsoleMode) String

func (m ConsoleMode) String() string

String returns the textual mode name (used in Sink.ID).

type ConsoleSink

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

ConsoleSink writes LogRecords to stdout/stderr as JSON-lines or pretty colored text. Per spec §7.2 it is a Phase 1 MVP sink; the default mode auto-detects TTY for the destination stream.

func NewConsoleSink

func NewConsoleSink(mode ConsoleMode, stream io.Writer, minSeverity int) *ConsoleSink

NewConsoleSink constructs a ConsoleSink writing to stream in the given mode. When stream is nil, os.Stderr is used.

minSeverity sets the early-drop threshold: records with severity_number below this value are skipped (use 1 to accept everything).

func (*ConsoleSink) Close

func (c *ConsoleSink) Close() error

Close marks the sink as closed and flushes the stream once. Subsequent Emit calls become no-ops. Close does not close stdout/stderr — those are shared with the process — only the close marker is set.

func (*ConsoleSink) Emit

func (c *ConsoleSink) Emit(record *LogRecord)

Emit writes record to the underlying stream. Errors are absorbed; sink failures must not propagate to the caller of Logger.Info.

func (*ConsoleSink) Flush

func (c *ConsoleSink) Flush(_ float64) error

Flush is a no-op for synchronous console writes; the stream is flushed after every Emit. timeoutSeconds is ignored.

func (*ConsoleSink) ID

func (c *ConsoleSink) ID() string

ID returns the URI-style sink identifier ("console:json", "console:pretty", "console:auto").

func (*ConsoleSink) SupportsSeverity

func (c *ConsoleSink) SupportsSeverity(severityNumber int) bool

SupportsSeverity reports whether severityNumber meets the configured minimum severity threshold.

type FileSink

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

FileSink writes LogRecords to a local file as canonical JSON-lines, with optional size-based rotation per spec ADR-0001 §7.2.

SECURITY NOTE: the path argument is resolved with filepath.Abs and opened as-is — there is no allow-list, sanitization, or path-traversal check, and the open follows symlinks. The host MUST treat path as a trusted configuration value and never accept it directly from end-user input or a plugin manifest. If the application supports plugin-supplied logging configuration, enforce an allow-list of writable directories before constructing the sink, and consider symlink-resistant resolution (openat2 RESOLVE_NO_SYMLINKS or a manual walk-up + lstat check) upstream of the FileSink.

Phase 1 implementation uses a hand-rolled rotator (rather than the standard library logging package) — Go's log/slog and log packages do not expose a rotating file handler, and a clean implementation here keeps the dependency footprint minimal.

Rotation rules:

  • When maxBytes > 0, the file is rotated after a write that would push its size beyond maxBytes.
  • Rotation moves the current file to "<path>.1", existing ".N" files shift to ".N+1"; the file at index keep is removed.
  • When maxBytes <= 0, rotation is disabled and the file grows unbounded.

func NewFileSink

func NewFileSink(path string, maxBytes int64, keep int, minSeverity int) (*FileSink, error)

NewFileSink opens (creating if necessary) the file at path for append-only writes. Returns an error if the parent directory does not exist or the file cannot be opened.

maxBytes <= 0 disables rotation; keep is the number of archived files to retain (e.g., keep=2 keeps .1 and .2).

func (*FileSink) Close

func (s *FileSink) Close() error

Close closes the underlying file handle. Idempotent.

func (*FileSink) Emit

func (s *FileSink) Emit(record *LogRecord)

Emit writes record as a JSON line. Errors are absorbed; sink failures must not propagate to the caller of Logger.Info.

func (*FileSink) Flush

func (s *FileSink) Flush(_ float64) error

Flush attempts to sync the underlying file handle. timeoutSeconds is ignored — Phase 1 writes are synchronous.

func (*FileSink) ID

func (s *FileSink) ID() string

ID returns the URI-style sink identifier.

func (*FileSink) SupportsSeverity

func (s *FileSink) SupportsSeverity(severityNumber int) bool

SupportsSeverity reports whether severityNumber meets the minimum.

type FlushFailure

type FlushFailure struct {
	SinkID string
	Err    error
}

FlushFailure pairs a sink id with the error it returned from Flush.

type FlushResult

type FlushResult struct {
	// Success is true when every effective sink flushed without error.
	Success bool
	// Partial is true when at least one (but not all) sinks flushed
	// successfully.
	Partial bool
	// FailedSinks lists sinks that returned an error from Flush, with the
	// underlying error for diagnostics.
	FailedSinks []FlushFailure
}

FlushResult records the outcome of a Logger.Flush call. The returned shape mirrors the spec §13 contract; Phase 1 sinks always either succeed or fail with the underlying I/O error.

type InMemorySink

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

InMemorySink accumulates LogRecords in a capacity-bounded ring buffer. Per spec ADR-0001 §7.2 — Phase 1 sink primarily used for tests and application self-checks.

The oldest records are dropped automatically when capacity is exceeded.

func NewInMemorySink

func NewInMemorySink(capacity int, minSeverity int) *InMemorySink

NewInMemorySink constructs a ring buffer with the given capacity. A capacity <= 0 is treated as 1 to keep the sink alive.

func (*InMemorySink) Capacity

func (s *InMemorySink) Capacity() int

Capacity returns the configured ring buffer size.

func (*InMemorySink) Clear

func (s *InMemorySink) Clear()

Clear empties the captured-records buffer. The sink remains usable.

func (*InMemorySink) Close

func (s *InMemorySink) Close() error

Close marks the sink as closed; subsequent Emit calls become no-ops. Captured records remain accessible via Records.

func (*InMemorySink) Emit

func (s *InMemorySink) Emit(record *LogRecord)

Emit appends record to the ring; if at capacity, drops the oldest entry.

func (*InMemorySink) Flush

func (s *InMemorySink) Flush(_ float64) error

Flush is a no-op for in-memory storage.

func (*InMemorySink) ID

func (s *InMemorySink) ID() string

ID returns the URI-style sink identifier ("in-memory:cap=<N>#<seq>").

func (*InMemorySink) Records

func (s *InMemorySink) Records() []*LogRecord

Records returns a snapshot copy of the captured records. Mutating the returned slice does not affect the sink's internal buffer.

func (*InMemorySink) SupportsSeverity

func (s *InMemorySink) SupportsSeverity(severityNumber int) bool

SupportsSeverity reports whether severityNumber meets the minimum.

type InstrumentationScope

type InstrumentationScope struct {
	Name       string `json:"name"`
	Version    string `json:"version,omitempty"`
	Attributes Attrs  `json:"attributes,omitempty"`
}

InstrumentationScope describes the logger that emitted a LogRecord. Per spec ADR-0001 §4.1 — name + version + optional attrs.

Name matches the logger name (e.g., "dagstack.rag.retriever"); version is the semantic version of the package or plugin.

type LogRecord

type LogRecord struct {
	// TimeUnixNano is the emit time in nanoseconds since the Unix epoch.
	TimeUnixNano int64

	// SeverityNumber is the OTel severity in the [1, 24] range.
	SeverityNumber int

	// SeverityText is one of the 6 canonical strings (see CanonicalSeverityTexts).
	SeverityText string

	// Body is the primary message payload. May be a string or a structured
	// value (map/array/scalar) — represented as any.
	Body any

	// Attributes is the per-record key-value context.
	Attributes Attrs

	// InstrumentationScope is the logger self-descriptor (§4.1). May be nil
	// for direct LogRecord construction outside the Logger API.
	InstrumentationScope *InstrumentationScope

	// Resource is the process/service/host attribute set (§4.2). May be nil.
	Resource *Resource

	// TraceID is the W3C Trace Context — 16 random bytes when an active span
	// is present; nil otherwise.
	TraceID []byte

	// SpanID is the W3C Trace Context — 8 random bytes when an active span
	// is present; nil otherwise.
	SpanID []byte

	// TraceFlags is the W3C flags byte (sampled bit etc.). Zero when no
	// active span.
	TraceFlags uint8

	// ObservedTimeUnixNano is the ingest time at the sink, in nanoseconds.
	// The producer leaves this 0; the sink fills it in (per spec §1).
	ObservedTimeUnixNano int64
}

LogRecord is the OTel Log Data Model v1.24-compatible log record.

Per spec ADR-0001 §1: internal field names match the OTel normative spec (TimeUnixNano, ObservedTimeUnixNano, SeverityNumber, SeverityText, Body, Attributes, Resource, InstrumentationScope, TraceId as 16 bytes, SpanId as 8 bytes, TraceFlags).

Wire serialization (OTLP / JSON) lives in separate functions, see wire.go:

  • dagstack JSON-lines: snake_case keys, hex trace/span ids, raw int timestamps.
  • OTel JSON (Phase 2+): camelCase keys, stringified int timestamps.
  • OTLP protobuf (Phase 2+): native OTel wire.

ObservedTimeUnixNano — the sink sets it on ingestion when zero (per spec §1 ownership rule).

type Logger

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

Logger is the primary handle for emitting LogRecords. Per spec ADR-0001 §3 it provides named loggers with dot-hierarchy, severity emits, child bindings, and scoped sink overrides.

Construct via Get — direct construction is reserved for the binding internals. The hierarchy is the dot-prefix of the name:

"dagstack.rag.retriever" → parent "dagstack.rag" → "dagstack" → root ""

Sinks and min-severity inherit from the parent unless overridden on the child via SetSinks / SetMinSeverity.

Context propagation reads OTel trace state from a context.Context. Use the *Ctx variants (InfoCtx, ErrorCtx, ...) to enable trace_id/span_id auto-injection; the non-Ctx variants (Info, Error, ...) skip propagation.

func Get

func Get(name string) *Logger

Get returns the cached logger with the given name; if absent, creates one and links it into the parent chain. Pass an empty name to obtain the root logger; pass a non-empty version to associate it with the instrumentation_scope.

Repeated Get calls with the same name return the same instance. To attach or update an instrumentation-scope version, use GetVersioned.

func GetVersioned

func GetVersioned(name, version string) *Logger

GetVersioned returns the cached logger for name, attaching or updating the supplied instrumentation-scope version on the singleton. Calling GetVersioned with the same name and a different version updates the existing logger's scope in place.

func (*Logger) AppendSinks

func (l *Logger) AppendSinks(extra ...Sink) *Logger

AppendSinks returns a detached child logger whose sink list is the parent chain's effective sinks plus the supplied extras.

func (*Logger) Child

func (l *Logger) Child(attrs Attrs) *Logger

Child returns a detached child logger with the supplied attributes pre-bound to every record. Child-bound attrs are merged before call-site attrs, so call-site values win on collision.

func (*Logger) Close

func (l *Logger) Close() error

Close calls Close on every effective sink. Errors are aggregated into the returned slice; Close on an already-closed sink is a no-op.

func (*Logger) Debug

func (l *Logger) Debug(body any, attrs Attrs)

Debug emits a DEBUG-severity record (severity_number=5). attrs may be nil.

func (*Logger) DebugCtx

func (l *Logger) DebugCtx(ctx context.Context, body any, attrs Attrs)

DebugCtx emits a DEBUG record with trace context auto-injected from ctx.

func (*Logger) EffectiveMinSeverity

func (l *Logger) EffectiveMinSeverity() int

EffectiveMinSeverity resolves the early-drop threshold — explicit on this logger or inherited.

func (*Logger) EffectiveResource

func (l *Logger) EffectiveResource() *Resource

EffectiveResource resolves the Resource — explicit or inherited.

func (*Logger) EffectiveSecretSuffixes added in v0.2.0

func (l *Logger) EffectiveSecretSuffixes() []string

EffectiveSecretSuffixes resolves the redaction-suffix list applied by this logger — explicit on this node or inherited from the parent chain. Returns DefaultSecretSuffixes when no override is registered anywhere up the chain. An explicit empty list (disable-all per spec §10.4) is preserved through inheritance — the returned slice is non-nil zero-length, distinguishable from "no override" via the suffixesExplicit flag on the resolving node.

The returned slice is a snapshot copy; mutations do not affect the logger.

TODO(#105): collapse the four chain-walks (sinks / min-severity / resource / suffixes) into one upward traversal — current impl acquires N locks per emit per dimension.

func (*Logger) EffectiveSinks

func (l *Logger) EffectiveSinks() []Sink

EffectiveSinks resolves the sink list — explicit on this logger or inherited from the parent chain.

func (*Logger) Error

func (l *Logger) Error(body any, attrs Attrs)

Error emits an ERROR-severity record (severity_number=17). attrs may be nil.

func (*Logger) ErrorCtx

func (l *Logger) ErrorCtx(ctx context.Context, body any, attrs Attrs)

ErrorCtx emits an ERROR record with trace context auto-injected from ctx.

func (*Logger) Exception

func (l *Logger) Exception(err error, body any, attrs Attrs)

Exception emits an ERROR-severity record with OTel exception.* attributes per spec §3.2 — exception.type, exception.message, exception.stacktrace.

The stacktrace is captured via runtime/debug.Stack at the call site; for errors that wrap a stack via errors.New / fmt.Errorf the captured stack shows the logging call point (the most useful frame for triage).

body may be nil — in that case err.Error() is used as the LogRecord body. Extra attrs override exception.* keys when supplied.

func (*Logger) ExceptionCtx

func (l *Logger) ExceptionCtx(ctx context.Context, err error, body any, attrs Attrs)

ExceptionCtx emits an ERROR record with OTel exception.* attributes plus trace context propagation from ctx.

func (*Logger) Fatal

func (l *Logger) Fatal(body any, attrs Attrs)

Fatal emits a FATAL-severity record (severity_number=21). attrs may be nil.

func (*Logger) FatalCtx

func (l *Logger) FatalCtx(ctx context.Context, body any, attrs Attrs)

FatalCtx emits a FATAL record with trace context auto-injected from ctx.

func (*Logger) Flush

func (l *Logger) Flush(timeoutSeconds float64) (*FlushResult, error)

Flush attempts to flush every effective sink. timeoutSeconds is forwarded to each Sink.Flush; the global Flush itself does not enforce a deadline, so a cooperatively-implemented sink keeps the budget honest.

Phase 1: every built-in sink (ConsoleSink, FileSink, InMemorySink) is synchronous, so timeoutSeconds is accepted for forward compatibility but is NOT enforced — none of the built-in sinks ever return a timeout error. Phase 2 (OTLPSink and friends) MUST honour the deadline and return a wrapped context.DeadlineExceeded; see spec ADR-0001 §7.1.

func (*Logger) Info

func (l *Logger) Info(body any, attrs Attrs)

Info emits an INFO-severity record (severity_number=9). attrs may be nil.

func (*Logger) InfoCtx

func (l *Logger) InfoCtx(ctx context.Context, body any, attrs Attrs)

InfoCtx emits an INFO record with trace context auto-injected from ctx.

func (*Logger) Log

func (l *Logger) Log(severityNumber int, body any, attrs Attrs)

Log emits a record with the explicit severity_number (must be in [1, 24]). Use this for intermediate values like TRACE2 or INFO3 that share a severity_text bucket but a different numeric granularity.

func (*Logger) LogCtx

func (l *Logger) LogCtx(ctx context.Context, severityNumber int, body any, attrs Attrs)

LogCtx is the generic emitter with explicit severity and context-aware trace propagation.

func (*Logger) Name

func (l *Logger) Name() string

Name returns the logger's dot-notation name (empty for root).

func (*Logger) OnReconfigure

func (l *Logger) OnReconfigure(_ func()) *Subscription

OnReconfigure registers a callback to fire when the logger's effective configuration changes. Phase 1 watch-based reconfigure is not implemented — the returned Subscription has Active=false and the callback never fires.

func (*Logger) ScopeSinks

func (l *Logger) ScopeSinks(ctx context.Context, sinks []Sink, fn func(context.Context) error) error

ScopeSinks runs fn with a temporary sink override on this logger, restoring the previous sink set (and its explicit/inherit state) on return. Per spec §6.2 the Go idiom is the callback form — it pairs with context.Context and does not require defer at the call site.

The override is applied to this logger instance directly, so any goroutines emitting through Logger.Get(name) during fn observe the same sinks. fn returns its error value; ScopeSinks does not modify it.

func (*Logger) SetMinSeverity

func (l *Logger) SetMinSeverity(severityNumber int)

SetMinSeverity sets the explicit early-drop threshold for this logger.

Mutates the shared registry node (visible to all consumers of the same name). Children inherit unless they set their own threshold.

func (*Logger) SetRedactionSuffixes added in v0.2.0

func (l *Logger) SetRedactionSuffixes(suffixes []string)

SetRedactionSuffixes installs the effective secret-suffix list on this logger (typically called on the root logger via Configure → spec §10.4).

Mutates the shared registry node. The suffix list MUST already be validated and lowercased — use BuildEffectiveSuffixes to derive it from a RedactionConfig.

Pass nil to fall back to inherited behaviour (parent's suffixes or DefaultSecretSuffixes at the root).

func (*Logger) SetResource

func (l *Logger) SetResource(r *Resource)

SetResource installs an explicit Resource on this logger.

Mutates the shared registry node (visible to all consumers of the same name). Children inherit unless they set their own Resource.

func (*Logger) SetSinks

func (l *Logger) SetSinks(sinks []Sink)

SetSinks installs an explicit sink list on this logger.

Mutates the shared registry node (visible to all consumers of the same name). Children inherit unless they set their own sinks.

func (*Logger) Trace

func (l *Logger) Trace(body any, attrs Attrs)

Trace emits a TRACE-severity record (severity_number=1). attrs may be nil.

func (*Logger) TraceCtx

func (l *Logger) TraceCtx(ctx context.Context, body any, attrs Attrs)

TraceCtx emits a TRACE record with trace_id/span_id auto-injected from ctx.

func (*Logger) Version

func (l *Logger) Version() string

Version returns the instrumentation_scope version, or empty when unset.

func (*Logger) Warn

func (l *Logger) Warn(body any, attrs Attrs)

Warn emits a WARN-severity record (severity_number=13). attrs may be nil.

func (*Logger) WarnCtx

func (l *Logger) WarnCtx(ctx context.Context, body any, attrs Attrs)

WarnCtx emits a WARN record with trace context auto-injected from ctx.

func (*Logger) WithSinks

func (l *Logger) WithSinks(sinks ...Sink) *Logger

WithSinks returns a detached child logger whose sink list is replaced with the supplied set. The child is not cached in the global registry.

func (*Logger) WithoutSinks

func (l *Logger) WithoutSinks() *Logger

WithoutSinks returns a detached child logger with an empty sink list — emits go to /dev/null. Useful for silencing a sub-tree of operations.

type RedactionConfig added in v0.2.0

type RedactionConfig struct {
	// ExtraSuffixes are additional secret suffixes registered by the
	// application. Each entry MUST be lowercase ASCII, contain no
	// whitespace, and be non-empty (validated at option-construction time).
	ExtraSuffixes []string

	// ReplaceDefaults, when true, swaps the base set for ExtraSuffixes
	// instead of unioning. With ReplaceDefaults=true and an empty
	// ExtraSuffixes list, all suffix-based redaction is disabled — the
	// binding emits a WARN diagnostic on dagstack.logger.internal in
	// that case (spec §10.4 disable-all warning).
	ReplaceDefaults bool
}

RedactionConfig is the public Phase 1 surface for tuning suffix-based redaction (spec ADR-0001 §10.4). Applications register a config via WithRedactionConfig at Configure time.

The zero value (no extras, ReplaceDefaults=false) keeps the Phase 1 baseline: the 6-element DefaultSecretSuffixes set is applied with no additions. Calls without WithRedactionConfig keep the same baseline.

type Resource

type Resource struct {
	Attributes Attrs `json:"attributes,omitempty"`
}

Resource carries process/service/host-level attributes shared across all loggers in the same process (OTel Resource). Per spec ADR-0001 §4.2 — typical keys: service.name, service.version, deployment.environment, host.name, process.pid, telemetry.sdk.{name,version,language}.

type Severity

type Severity int

Severity is the OTel severity_number, an integer in the inclusive range [1, 24]. Per spec ADR-0001 §2:

  • 1-4 bucket → severity_text "TRACE"
  • 5-8 bucket → severity_text "DEBUG"
  • 9-12 bucket → severity_text "INFO" (default INFO=9)
  • 13-16 bucket → severity_text "WARN"
  • 17-20 bucket → severity_text "ERROR"
  • 21-24 bucket → severity_text "FATAL"

Backends filter by exact match on severity_text, so the canonical 6-string set never grows. Numeric granularity (TRACE2, INFO3, ...) is carried in severity_number; intermediate values go through Logger.Log.

const (
	SeverityTrace Severity = 1
	SeverityDebug Severity = 5
	SeverityInfo  Severity = 9
	SeverityWarn  Severity = 13
	SeverityError Severity = 17
	SeverityFatal Severity = 21
)

Baseline severity values matching the public API methods.

type Sink

type Sink interface {
	// ID returns a URI-style identifier for diagnostics.
	ID() string

	// Emit delivers a record to the sink. Phase 1: synchronous; Phase 2: enqueue.
	// Errors during emit are absorbed by the sink and surfaced via metrics or
	// the dagstack.logger.internal diagnostic channel — Emit itself never
	// blocks the caller and never returns an error.
	Emit(record *LogRecord)

	// Flush blocks until buffered records are delivered, or until the
	// timeout is exhausted. Phase 1 sinks are synchronous, so
	// timeoutSeconds is accepted for forward compatibility but is NOT
	// enforced — Phase 1 implementations never return a timeout error.
	// Phase 2 (OTLPSink and friends) MUST honour the deadline and
	// return a wrapped context.DeadlineExceeded.
	Flush(timeoutSeconds float64) error

	// Close flushes pending records and releases resources. Idempotent.
	Close() error

	// SupportsSeverity is the early-drop hint: returns false when the sink
	// will not emit a record at the given severity_number. Logger uses this
	// to avoid building a record for sinks that would discard it.
	SupportsSeverity(severityNumber int) bool
}

Sink is the destination for LogRecords per spec ADR-0001 §7.1.

Implementations must be safe for concurrent use. Phase 1 sinks (ConsoleSink, FileSink, InMemorySink) use synchronous local I/O — the non-blocking property of Logger.Info is provided by OS buffering. Phase 2 sinks (OTLPSink, ...) will introduce true async batching with internal queues; Emit then enqueues the record for a worker.

ID is a URI-style identifier used in diagnostics and metrics:

"console:json"
"file:/var/log/app.jsonl"
"in-memory:cap=100#1"

type Subscription

type Subscription struct {
	// Path echoes the subscription path for introspection.
	Path string

	// Active is true iff a watch-capable backend is registered AND covers
	// the subscription path. Always false in Phase 1.
	Active bool

	// InactiveReason carries a human-readable diagnostic when Active=false.
	// Phase 1 sets it to a fixed message:
	//
	//   "Phase 1 logger does not support watch-based reconfigure"
	InactiveReason string
	// contains filtered or unexported fields
}

Subscription is a placeholder handle returned by Logger.OnReconfigure per spec ADR-0001 §7.2.

In Phase 1 watch-based reconfigure is not implemented — every subscription is constructed with Active=false and InactiveReason populated. The handle is forward-compatible: when Phase 2 introduces a Watcher (file or admin API), the same Subscription type carries the active subscription with a real Unsubscribe callback.

func NewInactiveSubscription

func NewInactiveSubscription(path, reason string) *Subscription

NewInactiveSubscription constructs a Subscription whose Active field is false. Use this to signal that a subscription was accepted but will never fire — typically because watch is not implemented in this binding phase.

func NewSubscription

func NewSubscription(path string, unsubscribe func()) *Subscription

NewSubscription constructs an active Subscription bound to the given cancellation callback. Reserved for Phase 2+ Watcher implementations.

func (*Subscription) Unsubscribe

func (s *Subscription) Unsubscribe()

Unsubscribe cancels the subscription. Idempotent — subsequent calls are no-op. After Unsubscribe returns, the callback is guaranteed not to fire.

Directories

Path Synopsis
Package docs_examples contains automated tests for the Go snippets in dagstack-logger-docs.
Package docs_examples contains automated tests for the Go snippets in dagstack-logger-docs.

Jump to

Keyboard shortcuts

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