FemtoStreamHandler
FemtoStreamHandler.stderr()(default) and.stdout()log to the respective standard streams. Calls return immediately; the handler thread flushes the underlyingio::Write.handler.flush()waits (up to one second by default) for the worker to flush buffered writes and returnsTrueonly when the worker acknowledgesOk(()). It returnsFalseif the underlying I/O flush fails, the handler has already been closed, the worker channel has been dropped, or the timeout expires.handler.close()shuts down the worker thread and should be called before process exit.- To tune capacity, flush timeout, or formatters use
StreamHandlerBuilder. It provides.with_capacity(n),.with_flush_after_ms(ms), and.with_formatter(callable_or_id)fluent methods before calling.build().
FemtoFileHandler
- Constructor signature:
FemtoFileHandler(path, capacity=1024, flush_interval=1, policy="drop"). The flush interval counts records, not seconds. policycontrols queue overflow handling:"drop"(default) discards records and raisesRuntimeError."block"blocks the caller until the worker makes room."timeout:N"blocks forNmilliseconds before giving up.- Use
handler.flush()andhandler.close()to ensure on-disk consistency. Always close the handler when the application shuts down.close()is idempotent and safe to call multiple times; only the first call performs shutdown work. - In Rust,
close()requires exclusive mutable access (&mut self). If the handler is shared across threads, synchronize close calls externally (for example with a mutex) instead of invokingclose()concurrently. FileHandlerBuildermirrors these options and also exposes.with_overflow_policy(OverflowPolicy.drop()/block()/timeout(ms))and.with_formatter(…). Formatter identifiers other than"default"are not wired up yet; pass a callable (taking a mapping and returning a string) to attach custom formatting logic.
FemtoRotatingFileHandler
- Wraps
FemtoFileHandlerwith size-based rotation. Instantiate viaFemtoRotatingFileHandler(path, options=HandlerOptions(…)). HandlerOptionsfields:capacity,flush_interval, andpolicymirrorFemtoFileHandler.rotation=(max_bytes, backup_count)enables rollover when both values are greater than zero. Set(0, 0)to disable rotation entirely.- Rotation renames the active log file to
.1, shifts older backups up tobackup_count, and truncates the live file. If opening a fresh file fails the implementation falls back to appending to the existing file and logs the reason. handler.close()follows the same contract asFemtoFileHandler: it is idempotent, only the first call performs shutdown work, and concurrent close calls must be synchronized by the caller.RotatingFileHandlerBuilderprovides the same fluent API as the file builder plus.with_max_bytes()and.with_backup_count().
FemtoTimedRotatingFileHandler
- Wraps
FemtoFileHandlerwith time-based rotation. Instantiate viaFemtoTimedRotatingFileHandler(path, options=TimedHandlerOptions(…)). TimedHandlerOptionskeeps the queue fields (capacity,flush_interval,policy) and addswhen,interval,backup_count,utc, and optionalat_time.- Supported
whenvalues areS,M,H,D,MIDNIGHT, andW0-W6.at_timeis only valid for daily, midnight, and weekday schedules. - Rotation runs on the worker thread.
backup_count == 0keeps all timestamped backups instead of disabling rotation. TimedRotatingFileHandlerBuildermirrors the direct handler surface with.with_when(),.with_interval(),.with_backup_count(),.with_utc(), and.with_at_time().
FemtoSocketHandler
- Socket handlers must be built via
SocketHandlerBuilder. Typical usage:
from femtologging import BackoffConfig, SocketHandlerBuilder
socket_handler = (
SocketHandlerBuilder()
.with_tcp("127.0.0.1", 9020)
.with_capacity(2048)
.with_connect_timeout_ms(5000)
.with_write_timeout_ms(1000)
.with_tls("logs.example.com", insecure=False)
.with_backoff(
BackoffConfig({
"base_ms": 100,
"cap_ms": 5000,
"reset_after_ms": 30000,
"deadline_ms": 120000,
})
)
.build()
)
- Default transport is TCP to
localhost:9020. Call.with_unix_path()to use Unix sockets on POSIX systems. TLS only works with TCP transports; attempting to combine TLS and Unix sockets raisesHandlerConfigError. - Records are serialized to MessagePack maps:
{logger, level, message, timestamp_ns, filename, line_number, module_path, thread_id, thread_name, key_values}and framed with a 4-byte big-endian length prefix. - The worker reconnects automatically using exponential backoff. Payloads that
exceed the configured
max_frame_sizeare dropped. handler.flush()forces the worker to flush the active socket. Always callhandler.close()to terminate the worker thread cleanly.
FemtoHTTPHandler
- HTTP handlers are built via
HTTPHandlerBuilderand send one serialized record per request from a dedicated worker thread. - Use
.with_endpoint(url, method="POST")to configure the destination in a single call. Legacy.with_url()and.with_method()setters remain available for compatibility. - Use
.with_auth({"token": "..."})for bearer tokens or.with_auth({"username": "...", "password": "..."})for basic auth. Legacy.with_basic_auth()and.with_bearer_token()setters remain available for compatibility. with_headers(...),with_capacity(...), timeout setters, and.with_json_format()mirror the socket/file builder style. Validation failures raiseValueErrorbefore the handler is built.
Custom Python handlers
class Collector:
def __init__(self) -> None:
self.records: list[tuple[str, str, str]] = []
def handle(self, logger: str, level: str, message: str) -> None:
# Runs inside the FemtoLogger worker thread
self.records.append((logger, level, message))
collector = Collector()
logger.add_handler(collector)
The handler's handle method must be callable and thread-safe. Exceptions are
printed to stderr and counted as handler errors, so prefer defensive code and
avoid raising from handle.
Handler stability contract
Handler capabilities are inspected once when add_handler() is called:
- The presence of a callable
handle_recordmethod is cached at registration time and determines which dispatch path is used for all subsequent records. - Do not mutate the handler after calling
add_handler(). Adding or removinghandle_recordafter registration has no effect and results in undefined behaviour. - Ensure the handler is fully configured before passing it to
add_handler().
This design keeps per-record overhead low by avoiding repeated attribute lookups on the hot path and aligns with the standard library expectation that handlers are configured before use.
The following diagram shows how capability detection works at registration time and how the cached result determines dispatch for all subsequent records:
Figure 1: Capability detection at registration and dispatch based on cached result.
This next diagram illustrates why mutating the handler after registration has no effect—the capability was already cached:
Figure 2: Mutating the handler after registration does not change dispatch behaviour.
Using standard library (stdlib) `logging.Handler` subclasses
Python's standard library ships with a rich set of handler classes
(FileHandler, RotatingFileHandler, SMTPHandler, SysLogHandler, etc.).
These handlers implement the stdlib emit(LogRecord) interface, which is
incompatible with femtologging's handle_record(dict) protocol.
StdlibHandlerAdapter bridges the gap. It wraps any logging.Handler
subclass, translates femtologging record dicts into logging.LogRecord
instances, and delegates to the wrapped handler's handle() method so that
attached filters and I/O locking apply.
import logging
from femtologging import FemtoLogger, StdlibHandlerAdapter
# Wrap a stdlib FileHandler for use with femtologging
file_handler = logging.FileHandler("app.log")
file_handler.setFormatter(
logging.Formatter("%(asctime)s %(name)s [%(levelname)s] %(message)s")
)
adapter = StdlibHandlerAdapter(file_handler)
logger = FemtoLogger("myapp")
logger.add_handler(adapter)
logger.log("INFO", "Application started")
The adapter maps femtologging levels to their stdlib equivalents (TRACE maps to
level 5, DEBUG to 10, INFO to 20, WARN to 30, ERROR to 40, CRITICAL to 50). A
custom TRACE level name is registered with stdlib's logging module so that
formatters render it as TRACE rather than Level 5.
Metadata fields such as filename, line_number, thread_name, thread_id,
and timestamp are forwarded to the LogRecord where matching attributes
exist. Custom key-value pairs from metadata.key_values are propagated into
the LogRecord.__dict__, making them available to stdlib formatters (e.g.
%(request_id)s) and filters.
Limitations:
exc_infois provided as pre-formatted text (exc_text), not as the original(type, value, traceback)tuple. Stdlib formatters that inspectrecord.exc_infodirectly will not find a live traceback object.pathnameandfuncNameare set to defaults because femtologging does not capture these values.relativeCreatedis recomputed frommetadata.timestampwhen present, so elapsed-time values can be accurate.