Creating and naming loggers
- Use
get_logger(name)to obtain a singletonFemtoLogger. Names must not be empty, start or end with., or contain consecutive dots. - Logger parents are derived from dotted names.
get_logger("api.v1")creates a parentapilogger that ultimately propagates toroot. - Call
logger.set_propagate(False)to stop parent propagation. The default is to bubble records to ancestors, just like the stdlib. reset_manager()clears all registered loggers. It is intended for tests and is not thread-safe.
Emitting records
logger.log(level, message)accepts the case-insensitive names"TRACE","DEBUG","INFO","WARN","WARNING","ERROR", and"CRITICAL".- The method returns the formatted string when the record passes level checks
(default format is
"{logger} [LEVEL] message"), orNonewhen the record is filtered out. This differs fromlogging.Logger.log(), which always returnsNone. - Convenience methods
logger.debug(message),logger.info(message),logger.warning(message),logger.error(message),logger.critical(message), andlogger.exception(message)are available. Each accepts a pre-formattedmessagestring plus optionalexc_infoandstack_infokeyword arguments, identical tolog(). Unlike the stdlib,*args/**kwargslazy formatting is not supported — build the final message string before calling these methods.exception()behaves likeerror()but defaultsexc_infotoTrue. log()accepts the keyword-only argumentsexc_infoandstack_infofor capturing exception tracebacks and call stacks alongside the log message.exc_infoaccepts any of the following forms:True— capture the current exception viasys.exc_info().- An exception instance — capture that exception's traceback.
- A
(type, value, traceback)3-tuple — use directly. FalseorNone(default) — no capture.
stack_info is a boolean (False by default). When True, the current call
stack is appended to the record. Both payloads are rendered by the default
formatter and are available as structured data to handle_record handlers.
try:
db.execute(query)
except DatabaseError:
logger.log("ERROR", "query failed", exc_info=True)
# Capture just the call stack (no exception needed):
logger.log("DEBUG", "checkpoint reached", stack_info=True)
logger.isEnabledFor(level)returnsTruewhen the logger would process a record at the given level. Use it for expensive message construction that should be skipped when the level is filtered out.getLogger(name)is an alias forget_logger(name), provided for drop-in compatibility with code written againstlogging.getLogger.- There is no equivalent to
extraor lazy formatting. Build the final message string before callinglog().
Exception schema versioning
Structured exc_info and stack_info payloads carry a schema_version field.
This allows the payload schema to evolve without silent breakage.
EXCEPTION_SCHEMA_VERSIONis the current version exposed by the Python API.- Rust consumers accept versions in the inclusive range
MIN_EXCEPTION_SCHEMA_VERSION..=EXCEPTION_SCHEMA_VERSION. - Backward compatibility is guaranteed within that range. Missing optional fields default safely during deserialization.
- Forward compatibility is not guaranteed. A payload from a newer producer may deserialize, but explicit validation is required before processing.
Rust-side validation returns stable variants from SchemaVersionError:
VersionTooNew { found, max_supported }VersionTooOld { found, min_supported }
Use these variants to choose explicit behaviour when versions mismatch:
use femtologging_rs::{
ExceptionPayload, SchemaVersionError, SchemaVersioned,
};
fn process_payload(json: &str) -> Result<(), SchemaVersionError> {
let payload: ExceptionPayload =
serde_json::from_str(json).expect("valid JSON");
match payload.validate_version() {
Ok(()) => {
// Normal processing path.
Ok(())
}
Err(SchemaVersionError::VersionTooNew {
found,
max_supported,
}) => {
// Degrade safely: keep raw payload and skip schema-dependent logic.
eprintln!(
"payload schema {} is newer than supported {}",
found, max_supported
);
Err(SchemaVersionError::VersionTooNew {
found,
max_supported,
})
}
Err(SchemaVersionError::VersionTooOld {
found,
min_supported,
}) => {
// Reject unsupported historical payloads explicitly.
eprintln!(
"payload schema {} is older than minimum {}",
found, min_supported
);
Err(SchemaVersionError::VersionTooOld {
found,
min_supported,
})
}
}
}
When consuming payloads in Python from external sources, compare
payload["schema_version"] with femtologging.EXCEPTION_SCHEMA_VERSION and
apply the same policy: reject too-old payloads, and explicitly degrade or
reject too-new payloads.
Increment the schema version only for breaking changes (required field additions, removals, renamed fields, or semantic/type changes). Adding optional fields with safe defaults does not require a version bump.
Managing handlers and custom sinks
- The built-in handlers implement the Rust-side
FemtoHandlerTrait. Python handlers can be attached by supplying any object that exposes a callablehandle(logger_name: str, level: str, message: str)method. - Python handlers run inside the logger’s worker thread. Ensure they are thread-safe and fast; slow handlers block the logger worker and can cause additional record drops.
logger.add_handler(handler)accepts either a Rust-backed handler or a Python handler as described above. Uselogger.remove_handler(handler)orlogger.clear_handlers()to detach them. Removals only affect records that are enqueued after the call because previously queued items already captured their handler list.- Use
logger.get_dropped()to inspect how many records have been discarded because the logger queue was full or shutting down.