Use log_context(...) to add structured key-values to every record emitted in
the current Python task while the context is active:
import femtologging
logger = femtologging.get_logger("service")
with femtologging.log_context(request_id=42, user="alice"):
logger.info("request accepted")
Behavioural guarantees:
- Python context uses
contextvars.ContextVarsemantics. It is safe to holdlog_context(...)across anawait: another task on the same event-loop thread cannot observe its fields. A child task inherits the snapshot that was active when it was created, and later changes stay task-local. - Python context values are captured when a Python logging API creates its record and are merged before queueing.
- Inline structured fields emitted by Rust macros override outer context keys.
- Context values must be
str,int,float,bool, orNone. - Callback-filter enrichment uses the same scalar contract: keys must be
non-empty strings, and values must be
str,int,float,bool, orNone. - Keys must not collide with stdlib
logging.LogRecordattributes or femtologging-reserved metadata names. - Enrichment is bounded to 64 keys per record, 64 UTF-8 bytes per key, 1,024 UTF-8 bytes per value, and 16 kibibytes (KiB) total serialized enrichment per record.
Rust context and bridge boundaries
Rust push_log_context remains OS-thread-local. Its LogContextGuard is
neither Send nor Sync, records the thread that created it, and removes only
its own frame when dropped. Do not hold a Rust guard across an .await in a
future that may migrate between executor threads; use a task-local API supplied
by that runtime instead.
Python task-local fields and Rust scoped fields do not transfer implicitly.
Python FemtoLogger methods and module-level functions capture only the active
Python fields. Rust macros and records received through the log and tracing
bridges merge Rust scoped fields active on their emitting OS thread. Explicit
key-values supplied by Rust macros or tracing events override same-named scoped
fields. Establish Rust context at the Rust emission boundary when bridged
events need shared request metadata.
Callback enrichment validation
Python callback filters may mutate the temporary logging.LogRecord object and
return a truthy result to accept the record. femtologging copies only the
validated scalar additions from that record into Rust-owned metadata before the
record is queued.
Accepted enrichment must satisfy all of the following:
- Keys are non-empty
strinstances. - Values are limited to
str,int,float,bool, orNone. - Keys do not overwrite built-in
logging.LogRecordfields such asname,msg,levelname,created, or femtologging-reserved metadata fields. - The serialized payload stays within 64 keys, 64 UTF-8 bytes per key, 1,024 UTF-8 bytes per value, and 16 KiB total.
Example of accepted enrichment:
import logging
import femtologging
def request_filter(record: logging.LogRecord) -> bool:
record.request_id = "req-123"
record.user_id = 42
record.cache_hit = False
return True
logger = femtologging.get_logger(
"service",
filters=[femtologging.PythonCallbackFilterBuilder(request_filter)],
)
Collisions and invalid values are rejected before the record enters the worker thread. For example, this filter tries to overwrite a stdlib field and attach an unsupported value:
import logging
def invalid_filter(record: logging.LogRecord) -> bool:
record.name = "rewritten"
record.payload = {"not": "allowed"}
return True
When validation fails, femtologging raises a Python exception inside the callback path, catches it at the logger boundary, emits a warning, and drops that record. The process keeps running, but the invalid record is not queued or delivered to handlers.