Working with loggers

Updated Jul 05, 2026

Creating and naming loggers

  • Use get_logger(name) to obtain a singleton FemtoLogger. 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 parent api logger that ultimately propagates to root.
  • 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"), or None when the record is filtered out. This differs from logging.Logger.log(), which always returns None.
  • Convenience methods logger.debug(message), logger.info(message), logger.warning(message), logger.error(message), logger.critical(message), and logger.exception(message) are available. Each accepts a pre-formatted message string plus optional exc_info and stack_info keyword arguments, identical to log(). Unlike the stdlib, *args / **kwargs lazy formatting is not supported — build the final message string before calling these methods. exception() behaves like error() but defaults exc_info to True.
  • log() accepts the keyword-only arguments exc_info and stack_info for capturing exception tracebacks and call stacks alongside the log message. exc_info accepts any of the following forms:
  • True — capture the current exception via sys.exc_info().
  • An exception instance — capture that exception's traceback.
  • A (type, value, traceback) 3-tuple — use directly.
  • False or None (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) returns True when 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 for get_logger(name), provided for drop-in compatibility with code written against logging.getLogger.
  • There is no equivalent to extra or lazy formatting. Build the final message string before calling log().

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_VERSION is 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 callable handle(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. Use logger.remove_handler(handler) or logger.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.