Compatibility Overview¶
LogXide is a high-performance logging library with a familiar API inspired by Python's standard logging module. It delivers significant performance improvements through its Rust native core, prioritizing speed over perfect compatibility.
For common use cases, LogXide provides a highly compatible experience. Standard patterns like getLogger(), basicConfig(), dictConfig, and built-in handlers work with minimal or no code changes. However, LogXide is not a drop-in replacement for every stdlib logging scenario. Its Rust core means some advanced patterns, particularly those involving custom Python subclasses or deep monkeypatching, are not supported.
This document provides a high-level compatibility overview. For detailed comparisons against specific logging libraries, see the deep-dive guides below.
Quick Compatibility Summary¶
| Feature | Status | Notes |
|---|---|---|
Basic logging API (getLogger, info, debug, etc.) |
✅ | Familiar stdlib-like API |
basicConfig() |
✅ | Direct mapping to LogXide handlers |
dictConfig() |
✅ | Use logxide.config.dictConfig for Django/FastAPI |
Standard formatters (%-style) |
✅ | Translated to the native Rust formatter when the pattern matches stdlib output |
{}-style / $-style formatters |
✅ | Work through the slower Python dispatch path, not the Rust formatter |
| FileHandler, StreamHandler, RotatingFileHandler | ✅ | Rust core with native fast path by default; Python fallback in documented cases |
Custom Python formatters (subclassed Formatter) |
⚠️ | Work, but switch the handler to the slower Python dispatch path |
| Custom Python handlers | ⚠️ | Accepted; a foreign Python handler runs once on the Python side (no fast-path GIL release) |
Subclassing LogRecord or Logger |
❌ | Rust types, not subclassable |
pytest caplog |
⚠️ | Use caplog_logxide fixture instead |
| StringIO capture | ⚠️ | Works on a handler (StreamHandler(buf)) via the stdlib path; basicConfig(stream=buf) does not capture |
The GIL and What Actually Runs in Rust¶
LogXide's performance comes from moving the log pipeline into a Rust core. How much of that runs without the GIL depends on the path a record takes:
Standard library logging: Creates a Python LogRecord object for every log call, then recursively bubbles it through all loggers while holding the GIL with threading.RLock().
LogXide fast path: When a record hits no Python filter, no Python handler, and no caller-info field, LogXide extracts the record's fields under the GIL and then releases it for the Rust dispatch. On that path, Rust-native handlers format and write without holding the GIL.
When the GIL is still held: A %-args call (for example logger.info("hi %s", name)) re-acquires the GIL inside emit() to run the % formatting, so args-bearing logs do not fully parallelize yet. Any Python handler or Python filter also runs under the GIL, as does caller-info collection when the format string needs it.
Because of this scoping, do not expect linear producer scaling across threads on current CPython GIL builds: the fast path shares a handler mutex and the sink I/O is serialized, so adding producer threads does not multiply throughput. Free-threaded CPython builds need separate verification.
Any custom logic that overrides standard Python implementations, such as subclassed Formatters with custom format() methods, runs on the Python dispatch path instead of natively.
Native Fast Path vs Python Fallback¶
The text-sink wrappers FileHandler, StreamHandler, and RotatingFileHandler emit through the native Rust path by default. The fast path applies only when the handler can be translated exactly; in every other case the wrapper switches to an explicit Python dispatch path, so behavior stays stdlib-correct and only speed changes.
The Rust writer exists only for default-style construction. FileHandler and RotatingFileHandler create one only with append mode='a', encoding=None, delay=False, and errors=None; any other combination (mode='w', a nondefault encoding, delay=True, custom errors) runs the pure stdlib handler. StreamHandler creates one only for the default stream, sys.stdout, or sys.stderr; any other file-like object (a StringIO, a network wrapper) runs the pure stdlib path. Replacing the stream with setStream() triggers a re-check and falls back if the new stream isn't the native one.
Formatting switches to Python dispatch when:
- the formatter is a
Formattersubclass that overridesformat(), or any offormatMessage/formatTime/formatException/formatStack - the style is
{or$instead of% - the format string needs semantics the native formatter doesn't replicate: precision specs like
%.2f, escaped percent (%%), or%(asctime)s, which depends on stdlib's default,mmmmilliseconds and converter behavior - the formatter carries stdlib
_defaults - the handler has Python filters attached
Supported Patterns ✅¶
- Basic Configuration:
logging.basicConfig()maps directly to LogXide - Structural Configuration:
logxide.config.dictConfigtranslates Python dictionary configurations (Django, FastAPI) to native Rust objects - Logger Hierarchy: Dot-delimited logger names (e.g.,
app.db.sql) bubble matching Python's resolution logic - Standard Formatting:
%-style placeholders whose output matches stdlib run in the native Rust formatter (withstrftime-styledatefmtsupport);{}-style placeholders, including{asctime}, work through the Python dispatch path - Standard Handlers: StreamHandler, FileHandler, RotatingFileHandler behavior replicated in Rust
- Exception Logging:
exc_info=Truecorrectly fetches and logs stack traces - Third-party Interception:
logxide.intercept_stdlib()captures logs from libraries using standard logging
Unsupported Patterns ❌¶
1. Custom Python Formatters¶
A logging.Formatter subclass with a custom format(self, record) method still runs. The handler detects it and switches to the Python dispatch path, which calls your format() on each record before handing the finished line to the Rust writer. What you give up is speed: the record is materialized as a Python object, the work holds the GIL, and throughput drops back to roughly the pre-0.2.0 level.
Alternative: keep format strings to plain %(...)-style patterns the native formatter can translate (see Native Fast Path vs Python Fallback), use JSON templates via logxide.HTTPHandler, or transform output at the application edge.
2. Custom Python Handlers¶
If you create a custom Python handler (e.g., class MailLog(logging.Handler)), LogXide accepts it via addHandler() and routes it through its Python dispatch path, so its .handle() method runs once with a Python LogRecord. It runs synchronously on the Python side and does not benefit from the fast-path GIL release. As of 0.2.0, a Rust-backed handler (e.g. logxide.FileHandler) attached to one logger is dispatched exactly once and never leaks records to unrelated loggers; earlier releases could double-emit or misroute such records.
3. Standard Library Unit Tests¶
LogXide fails CPython's test_logging.py unit tests. These tests validate locking behavior, internal .handlers array mutability, and .disabled states using memory assertions that conflict with Rust's encapsulated states and RwLocks.
Detailed Comparison Guides¶
For side-by-side comparisons with specific logging libraries, including benchmark data and migration guidance:
| Comparison | Description |
|---|---|
| LogXide vs stdlib | Handler-by-handler performance vs Python's logging module, feature matrix, and migration path for standard use cases |
| LogXide vs Loguru | Architecture differences, performance benchmarks, feature trade-offs, and when to choose each |
| LogXide vs Structlog | Structured logging capabilities, processor pipelines, context binding, and performance comparison |
| LogXide vs Picologging | Rust vs Cython implementation, Python 3.13+ compatibility, and feature ecosystem comparison |
Migration Checklist¶
When migrating an application to LogXide:
- Initialize early: Import and initialize LogXide before framework initialization (Django/Flask/FastAPI)
- Intercept stdlib: Call
logxide.intercept_stdlib()to capture logs from third-party dependencies - Use structural config: Prefer
logxide.config.dictConfigover custom instantiation - Check custom handlers: Verify any custom Python handlers are acceptable. They are accepted via
addHandler()and run once on the Python side (synchronously, without the fast-path GIL release). Rust-backed handlers run once on the Rust path. - Update tests: Replace
caplogwithcaplog_logxideand use file-based logging instead of StringIO
For detailed third-party library compatibility information, see the Third-Party Compatibility Guide.