Skip to content

Performance Guide

obslog is designed to be cheap when idle and predictable under load (PRODUCT.md §15.1, NFR-PERF-001..006). Optimization decisions are measurement-driven (PRIN-001); this guide explains the model and how to measure.

Pay for what you use

A log call whose level is below the active threshold short-circuits before building a record or evaluating any lazy field (NFR-PERF-001). In the committed benchmarks a disabled emit is roughly an order of magnitude cheaper than an enabled one — the gate is a single integer comparison.

# `expensive()` runs only if DEBUG is enabled:
log.debug("cache.snapshot", state=obslog.lazy(expensive))

Wrap any costly field value in obslog.lazy(...) (API-007) so it is computed only when the record is actually emitted.

Cheap context

bind() returns a new logger holding only the merged context delta (NFR-PERF-002); it does not touch sinks or build records. Ambient context() uses contextvars, which is O(1) to read on the hot path.

Allocation-light emit path

On an enabled emit the record is built once and passed to sinks by reference (NFR-PERF-003). The resource mapping is shared (read-only) rather than copied per record. Source/caller capture (stack inspection) is opt-in because of its cost (NFR-PERF-004).

High-volume delivery: Batching

For sinks backed by expensive I/O (network exporters, disk), wrap them in obslog.sinks.Batching to amortize flushes (NFR-PERF-006). It buffers records and flushes by size and/or on a background interval, and prefers a downstream emit_batch(records) method when present. Batching is an optional sink concern — it does not change the core Sink contract (FR-005).

from obslog.sinks import Batching, Stream

with Batching(Stream(), max_batch=500, flush_interval=1.0) as sink:
    obslog.configure(sinks=[sink])
    ...  # flushed on size, interval, close(), and at process exit

Measuring

The benchmark suite lives in tests/benchmarks/ and is deselected from the normal test run. Run it explicitly:

pytest tests/benchmarks -m benchmark

CI runs it on every push and uploads the results, so regressions in the enabled-emit, disabled-emit, bind, and context paths are visible over time (NFR-PERF-005).