Third-Party Library Compatibility Guide¶
LogXide automatically intercepts logging from third-party libraries. This guide documents compatibility status, configuration requirements, and known limitations for popular Python libraries.
Status Legend¶
| Icon | Meaning |
|---|---|
| ✅ Tested | Verified with examples and/or integration tests |
| ✅ Expected | Uses standard logging patterns; no known issues |
| ⚠️ Partial | Works with specific configuration or has limitations |
| ⚠️ Limited | Works for basic use cases but significant features are unsupported |
| ⚠️ Untested | Expected to work based on architecture analysis, but not verified |
| ❌ Incompatible | Fundamental architectural conflict |
Quick Reference Matrix¶
| Library | Status | Logger Names | Notes |
|---|---|---|---|
| Flask | ✅ Tested | flask.app, werkzeug |
Details |
| Django | ✅ Tested | django.* |
Details |
| FastAPI | ✅ Tested | fastapi |
Details |
| Sentry | ✅ Tested | Native integration | Details |
| SQLAlchemy | ✅ Tested | sqlalchemy.engine, sqlalchemy.pool |
Details |
| Flask-SQLAlchemy | ✅ Tested | sqlalchemy.engine |
Details |
| requests / urllib3 | ✅ Tested | urllib3.connectionpool, urllib3.connection |
Details |
| httpx | ✅ Expected | httpx, httpcore |
Details |
| Uvicorn | ✅ Tested | uvicorn, uvicorn.error, uvicorn.access |
Details |
| Gunicorn | ✅ Expected | gunicorn.error, gunicorn.access |
Details |
| Hypercorn | ✅ Expected | hypercorn.* |
Standard logging usage |
| boto3 / botocore | ✅ Expected | boto3, botocore.* |
Details |
| aiohttp | ✅ Expected | aiohttp, aiohttp.server, aiohttp.access |
Details |
| Celery | ⚠️ Partial | celery.* |
Details |
| pytest | ⚠️ Partial | N/A | Details |
| unittest | ⚠️ Limited | N/A | Details |
| python-json-logger | ⚠️ Limited | N/A | Details |
| Scrapy | ⚠️ Untested | scrapy.* |
Details |
| structlog | ❌ Incompatible | N/A | Details |
| Pandas / NumPy | ✅ Expected | pandas.* |
Minimal logging |
How LogXide Intercepts Third-Party Logging¶
When you import logxide (outside of pytest), it performs two key actions:
-
Patches
logging.getLogger()— Every call tologging.getLogger(name)returns a stdlib Logger whose logging methods (debug,info,warning,error,critical,exception,log) are replaced with logxide equivalents. This means any library that usesimport logging; logger = logging.getLogger(__name__)automatically routes through logxide's Rust backend. -
Replaces
sys.modules["logging"]— Theloggingmodule insys.modulesis replaced with a_LoggingModuleinstance that mirrors the stdlib API but delegates to logxide internally.
Import Order Matters
Always import logxide before initializing your framework or third-party libraries. This ensures all loggers created during initialization are intercepted.
Handler Migration on Import
When logxide is imported, it calls _migrate_existing_loggers() which clears all handlers from existing stdlib loggers and sets propagate = True. This means any handlers that third-party libraries attached to their loggers during import will be removed. This is by design (to route everything through logxide's Rust pipeline), but it means libraries that configure handlers at import time will lose that configuration. Import logxide first to avoid this issue.
What Works Automatically¶
Any library that only uses:
logging.getLogger(name)to get loggers.debug(),.info(),.warning(),.error(),.critical(),.exception()to emit logslogging.basicConfig()for configurationsetLevel()on loggersaddHandler()with Rust-compatible handlersaddFilter()withlogging.FilterinstancesisEnabledFor()level checks
What Breaks¶
Libraries that:
- Subclass
logging.Logger— Rust type, not subclassable - Subclass
logging.LogRecord— Rust type, not subclassable - Override
logging.Formatter.format()— On the Rust code path (primary), customformat()methods are not called because Rust handles formatting directly. However, if a Python handler is attached, its formatter'sformat()method will be called on the Python LogRecord passed to that handler. - Use Python
logging.Handlersubclasses — They are accepted and their.handle()method is called with a Python LogRecord, but log processing also goes through the Rust pipeline independently. This means logs are processed twice (Rust + Python handler), not "bypassed." - Use
StringIOstream capture — Not supported (Rust writes directly) - Rely on
caplogin pytest — Must usecaplog_logxideinstead
Custom Python Handlers
Python logging.Handler subclasses are accepted via addHandler(). LogXide stores them separately and calls their .handle() method with a Python LogRecord object after the Rust pipeline processes the log. This means both Rust handlers and Python handlers fire for each log event. For maximum performance, use Rust-native handlers: FileHandler, StreamHandler, RotatingFileHandler, HTTPHandler, OTLPHandler.
Alternative: Explicit Stdlib Interception¶
If you need to capture logs from libraries that were imported before logxide, use intercept_stdlib():
from logxide import logging
from logxide.interceptor import intercept_stdlib
# Redirect ALL existing stdlib loggers to logxide
intercept_stdlib()
This replaces the root logger's handlers with an InterceptHandler that forwards all stdlib log records to logxide.
Web Frameworks¶
Flask¶
Status: ✅ Tested | Integration doc: Flask Integration | Example: examples/flask_integration.py
Flask uses app.logger, which internally calls logging.getLogger('flask.app'). Werkzeug (Flask's WSGI server) logs to the werkzeug logger. Both are automatically intercepted.
from logxide import logging
from flask import Flask
app = Flask(__name__)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@app.route('/')
def hello():
logger.info('Hello endpoint accessed')
return {'message': 'Hello from Flask!'}
What works:
app.logger— automatically routed through logxide- Werkzeug request/response logging
- Flask-SQLAlchemy SQL query logging (see SQLAlchemy)
- Sentry integration with
FlaskIntegration()(see Sentry) - Request logging middleware via
@app.before_request/@app.after_request - Error handling with
@app.errorhandlerandlogger.exception()
Full documentation: Flask Integration
Django¶
Status: ✅ Tested | Integration doc: Django Integration | Example: examples/django_integration.py
Django uses the LOGGING dictionary configuration in settings.py, processed via logging.config.dictConfig(). LogXide's module replacement ensures this works.
# manage.py or wsgi.py — import logxide early
from logxide import logging
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
application = get_wsgi_application()
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{asctime} - {name} - {levelname} - {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
},
}
What works:
dictConfig-basedLOGGINGsetting- Django request logging (
django.request) - Django SQL logging (
django.db.backends) - Signal-based logging in models
- Management command logging
- Sentry integration with
DjangoIntegration()
Known caveats:
- Django's
AdminEmailHandleris a customlogging.Handlersubclass. It executes but is called separately from the Rust pipeline. - Django's built-in
Filtersubclasses (RequireDebugFalse,RequireDebugTrue) work because logxide supportslogging.Filter. - Import logxide in
manage.pyorwsgi.pybeforedjango.setup()is called. dictConfighandler resolution: Django'sLOGGINGdict useslogging.config.dictConfig(), which resolves handler class names (e.g.,'logging.StreamHandler') against the real stdlibloggingmodule. Handlers created this way are Python stdlib handlers, not logxide's Rust handlers. They will still be called (logxide accepts Python handlers), but won't benefit from the Rust performance pipeline. For Rust-native file logging, use logxide'sbasicConfig(filename=...)instead ofdictConfigfile handlers.
Full documentation: Django Integration
FastAPI / Uvicorn¶
Status: ✅ Tested | Integration doc: FastAPI Integration | Examples: examples/fastapi_demo.py, examples/fastapi_advanced.py
FastAPI uses stdlib logging directly. Uvicorn creates loggers at uvicorn, uvicorn.error, and uvicorn.access. All are automatically intercepted.
from logxide import logging
from fastapi import FastAPI
app = FastAPI()
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@app.get("/")
async def root():
logger.info("Root endpoint accessed")
return {"status": "ok"}
What works:
- FastAPI endpoint logging
- Uvicorn access and error logging (
uvicorn.access,uvicorn.error) - Background task logging
- SQLAlchemy integration via FastAPI
Depends - ASGI middleware logging
- Sentry integration with
SentryAsgiMiddleware
Uvicorn Log Configuration
When using logxide, you can let logxide handle all formatting and just set log levels:
Full documentation: FastAPI Integration
HTTP Client Libraries¶
requests / urllib3¶
Status: ✅ Tested | Example: examples/third_party_integration.py
requests uses urllib3 internally, which logs to urllib3.connectionpool, urllib3.connection, etc. using standard logging.getLogger(__name__). All automatically intercepted.
from logxide import logging
import requests
# Enable urllib3 debug logging to see connection details
logging.getLogger('urllib3').setLevel(logging.DEBUG)
response = requests.get('https://example.com')
# urllib3 connection logs automatically captured by logxide
What works:
- Connection pool logging (
urllib3.connectionpool) - SSL/TLS handshake logging
- Retry logic logging
- Request/response debug logging
NullHandlercompatibility (requests addsNullHandlerat import time)
httpx¶
Status: ✅ Expected
httpx is a development dependency in logxide's test suite. It uses standard logging.getLogger(__name__) patterns, logging to httpx and httpcore.
from logxide import logging
import httpx
logging.getLogger('httpx').setLevel(logging.DEBUG)
async with httpx.AsyncClient() as client:
response = await client.get('https://example.com')
aiohttp¶
Status: ✅ Expected
aiohttp logs to aiohttp, aiohttp.server, aiohttp.access, and aiohttp.web using standard logging.getLogger(__name__). Expected to work automatically.
Database Libraries¶
SQLAlchemy¶
Status: ✅ Tested | Example: examples/third_party_integration.py
SQLAlchemy logs SQL statements through sqlalchemy.engine and connection pool events through sqlalchemy.pool. Uses standard logging.getLogger(name) via its internal InstanceLogger wrapper.
from logxide import logging
from sqlalchemy import create_engine, text
# Enable SQL query logging
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
engine = create_engine('sqlite:///:memory:', echo=True)
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
# SQL statement logged through logxide
What works:
- SQL statement logging (
sqlalchemy.engine) - Connection pool logging (
sqlalchemy.pool) echo=Trueparameter oncreate_engine()isEnabledFor()level checks (used internally by SQLAlchemy for performance)- Flask-SQLAlchemy and FastAPI + SQLAlchemy patterns
- Alembic migration logging (expected)
SQLAlchemy Performance
SQLAlchemy calls logger.isEnabledFor(level) before formatting SQL statements to avoid overhead. LogXide supports this method, so SQL logging only incurs formatting cost when the appropriate log level is enabled.
ASGI / WSGI Servers¶
Gunicorn¶
Status: ✅ Expected
Gunicorn uses logging.getLogger("gunicorn.error") and logging.getLogger("gunicorn.access") for its logging. Both are automatically intercepted.
# gunicorn.conf.py
from logxide import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
Caveats:
- Gunicorn's
--log-configINI-style config file useslogging.config.fileConfig(). This works through the module replacement, but you may prefer logxide'sbasicConfig()instead. - Gunicorn forks worker processes. Each worker reconfigures logging independently. Import logxide in the gunicorn config file to ensure all workers are patched.
- Gunicorn's custom
Loggerclass (gunicorn.glogging.Logger) uses standardlogging.getLogger()internally.
Task Queues & Workers¶
Celery¶
Status: ⚠️ Partial (Requires Configuration)
Celery has aggressive logging management. By default, it hijacks the root logger — removing all existing handlers and installing its own. This conflicts with logxide's configuration.
from logxide import logging
from celery import Celery
from celery.signals import setup_logging
app = Celery('myapp')
@setup_logging.connect
def configure_logging(**kwargs):
"""Prevent Celery from hijacking the root logger."""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
What works:
- Basic task logging via
celery.utils.log.get_task_logger() - Worker lifecycle logging
- Logger hierarchy for celery namespaces
What requires configuration:
- Root logger hijacking — Set
worker_hijack_root_logger = Falsein Celery config, or use thesetup_loggingsignal (shown above) to prevent Celery from overwriting logxide's handlers. - TaskFormatter — Celery's
TaskFormatteris alogging.Formattersubclass that accesses extra attributes (task_id,task_name) on LogRecord. On the Rust code path, this formatter is not called. It would only work if Celery attaches its own Python handler, in which case it receives a Python LogRecord alongside the Rust pipeline output.
Celery Root Logger Hijacking
Without configuration, Celery removes all root logger handlers on worker startup. Use the setup_logging signal or set worker_hijack_root_logger = False to prevent this.
Cloud SDKs¶
boto3 / botocore¶
Status: ✅ Expected
boto3 and botocore use standard logging.getLogger(__name__) throughout. Logger names include boto3, botocore, botocore.endpoint, botocore.hooks, botocore.credentials, and botocore.retryhandler.
from logxide import logging
import boto3
# Silence verbose botocore logging
logging.getLogger('botocore').setLevel(logging.WARNING)
# Or enable debug for troubleshooting
logging.getLogger('botocore.endpoint').setLevel(logging.DEBUG)
Wire Logging Security
botocore.endpoint at DEBUG level logs full HTTP requests and responses, which may include AWS credentials or sensitive data. Keep this logger at WARNING or above in production.
Error Tracking & Monitoring¶
Sentry¶
Status: ✅ Tested (Native Integration) | Integration doc: Sentry Integration | Example: examples/sentry_integration.py
LogXide includes a dedicated SentryHandler with zero-configuration auto-detection. When sentry_sdk is initialized, logxide automatically sends WARNING+ logs to Sentry.
import sentry_sdk
sentry_sdk.init(dsn="https://your-dsn@sentry.io/project-id")
from logxide import logging
logger = logging.getLogger(__name__)
logger.warning("Sent to Sentry automatically")
logger.error("Errors tracked with full context")
Features:
- Automatic detection of Sentry SDK configuration
- Level filtering (WARNING and above sent to Sentry)
- Exception capture with full stack traces via
logger.exception() - Rich context from
extraparameter - Breadcrumb support
- Framework integrations:
FlaskIntegration,DjangoIntegration,SentryAsgiMiddleware - Manual
SentryHandlerconfiguration for fine-grained control
Full documentation: Sentry Integration
Datadog / New Relic / Other APMs¶
Status: ⚠️ Untested
Most APM tools hook into Python's stdlib logging module. Since logxide replaces sys.modules["logging"], these tools should detect logxide's _LoggingModule as the logging module. However, APMs that:
- Monkey-patch
logging.Loggerclass methods directly may conflict - Install custom
logging.Handlersubclasses will work but bypass Rust performance - Rely on
LogRecordinternal attributes may have issues with logxide's RustLogRecord
If you use these tools with logxide, please report your results.
Structured Logging Libraries¶
structlog¶
Status: ❌ Not Compatible
structlog is fundamentally incompatible with logxide due to deep architectural differences:
-
Custom Logger subclass — structlog's
_FixedFindCallerLoggerextendslogging.Loggerto fix stack frame detection. LogXide's Logger is a Rust type that cannot be subclassed. -
ProcessorFormatter — structlog's
ProcessorFormatterextendslogging.Formatterwith a customformat()method that processes both structlog and stdlib records. On logxide's primary Rust code path, this customformat()is not called because Rust handles formatting. Even if a Python handler is attached, structlog's processor pipeline expects full control over the formatting chain, which conflicts with logxide's dual-pipeline architecture. -
Processor pipeline — structlog wraps log records through a chain of Python processors before emission. This pipeline requires Python-side LogRecord manipulation that is incompatible with logxide's Rust pipeline.
Alternative: Use logxide's HTTPHandler with transform_callback for structured JSON output, or use logxide's format strings for structured log formatting:
from logxide import HTTPHandler
handler = HTTPHandler(
url="https://logs.example.com",
transform_callback=lambda records: [
{"event": r["msg"], "level": r["levelname"], "timestamp": r["asctime"]}
for r in records
]
)
python-json-logger¶
Status: ⚠️ Limited
python-json-logger provides JsonFormatter, a logging.Formatter subclass with a custom format() method. On logxide's primary Rust code path, the custom format() is not called. The formatter would only work if attached to a Python handler (which receives a separate Python LogRecord), but the Rust pipeline still formats and outputs independently.
Workaround: Use logxide's HTTPHandler with transform_callback for JSON-formatted log output:
from logxide import HTTPHandler
handler = HTTPHandler(
url="https://logs.example.com",
transform_callback=lambda records: {
"logs": [
{"msg": r["msg"], "level": r["levelname"], "logger": r["name"]}
for r in records
]
}
)
Testing¶
pytest¶
Status: ⚠️ Partial | Testing doc: Testing Guide
LogXide intentionally skips sys.modules replacement when pytest is detected to avoid breaking pytest internals. This means the standard caplog fixture does not capture logxide output.
Use caplog_logxide instead:
def test_logging(caplog_logxide):
from logxide import logging
logger = logging.getLogger('test')
logger.info('test message')
assert 'test message' in caplog_logxide.text
What works:
caplog_logxidefixture for log capture- Logger creation and configuration in tests
- Handler and filter testing
What doesn't work:
- Standard
caplogfixture (does not capture logxide output) StringIOstream capture (logxide writes through Rust, not Python streams)
unittest¶
Status: ⚠️ Limited
unittest does not provide built-in log capture equivalent to pytest's caplog. For log-based assertions, use file-based logging:
logging.flush() is LogXide-specific
The logging.flush() call used below is a logxide extension — it does not exist in stdlib logging. If your code needs to be portable between logxide and stdlib, guard the call: if hasattr(logging, 'flush'): logging.flush()
import unittest
import tempfile
import os
from logxide import logging
class TestMyApp(unittest.TestCase):
def test_logging_output(self):
with tempfile.NamedTemporaryFile(
mode='r', suffix='.log', delete=False
) as f:
logging.basicConfig(filename=f.name, level=logging.INFO)
logger = logging.getLogger('test')
logger.info('test message')
logging.flush()
f.seek(0)
content = f.read()
self.assertIn('test message', content)
os.unlink(f.name)
Other Popular Libraries¶
Scrapy¶
Status: ⚠️ Untested
Scrapy bridges Twisted's logging system (twisted.python.log) with Python's stdlib logging. This adds complexity:
- Scrapy installs a Twisted log observer that forwards to stdlib logging
- Each spider has its own logger via
self.logger - The
LOG_LEVELsetting affects all logging globally
LogXide should intercept the stdlib side of Scrapy's logging, but the Twisted bridge layer has not been tested.
Pandas / NumPy / SciPy¶
Status: ✅ Expected
These libraries use minimal logging. When they do log (typically warnings about deprecated features or data issues), they use standard logging.getLogger(__name__) or Python's warnings module. No compatibility concerns.
Compatibility Decision Tree¶
Use this to determine if your third-party library will work with logxide:
Does the library use logging.getLogger() for loggers?
|
+-- YES --> Does it only call .debug()/.info()/.warning()/.error()/.critical()?
| |
| +-- YES --> Full compatibility (auto-intercepted)
| |
| +-- NO --> Does it subclass logging.Logger?
| |
| +-- YES --> Not compatible (Rust type, not subclassable)
| |
| +-- NO --> Does it use custom logging.Formatter subclass?
| |
| +-- YES --> Formatter's format() bypasses Rust pipeline
| |
| +-- NO --> Does it use custom logging.Handler subclass?
| |
| +-- YES --> Works (called alongside Rust pipeline), may cause duplicate output
| |
| +-- NO --> Full compatibility
|
+-- NO --> Does it use its own logging system?
|
+-- YES --> Not intercepted by logxide (separate system)
|
+-- NO --> Probably doesn't log --> No issues
Troubleshooting¶
Library logs not appearing¶
- Check import order — LogXide must be imported before the library
- Check logger level — The logger may default to WARNING
- Try
intercept_stdlib()— Captures loggers created before logxide was imported
from logxide import logging
from logxide.interceptor import intercept_stdlib
intercept_stdlib() # Capture all existing loggers
Conflicting logging configurations¶
Some libraries call logging.basicConfig() on import. LogXide's patched basicConfig() handles this, but if you experience issues:
# Import logxide FIRST, always
from logxide import logging
import problematic_library # Its basicConfig() call is intercepted
Custom handlers causing duplicate output¶
Python logging.Handler subclasses are called in addition to the Rust pipeline, not instead of it. This means each log event is processed twice — once by Rust handlers and once by your Python handler. If you see duplicate output, this is likely the cause.
To use only the Rust pipeline, replace Python handlers with Rust-native equivalents:
| Python Handler | LogXide Equivalent |
|---|---|
logging.FileHandler |
logxide.FileHandler |
logging.StreamHandler |
logxide.StreamHandler |
logging.handlers.RotatingFileHandler |
logxide.RotatingFileHandler |
| Custom HTTP handler | logxide.HTTPHandler |
| Custom OTLP handler | logxide.OTLPHandler |
Reporting Compatibility¶
If you test logxide with a library not listed here, please report your findings:
- What worked and what didn't
- Library version and Python version tested
- Any configuration needed
File an issue at the logxide repository with the label compatibility.