Ergonomic Python logging that starts with a tiny API and scales to structured observability.
from ultilog import get_logger
log = get_logger()
log.info("app.started")No explicit settings required. The package lazily configures logging on first access, installs a Rich console handler, and returns a standard-library logger.
pip install ultilogWith extras:
pip install "ultilog[structlog]" # structlog processor bridge
pip install "ultilog[otel]" # OpenTelemetry traces, logs, metrics
pip install "ultilog[web]" # FastAPI / Starlette middleware
pip install "ultilog[full]" # everythingfrom ultilog import get_logger
log = get_logger()
log.info("hello")from ultilog import setup_dev, setup_prod, setup_test, get_logger
setup_dev() # super-pretty Rich, DEBUG, tracebacks with locals
setup_prod(service_name="my-api") # JSON, INFO, OTel correlation auto-attached
setup_test() # plain WARNING, quiet test output
log = get_logger(__name__)
log.info("ready")log = get_logger(__name__)
log = get_logger("my.service")from ultilog import setup
setup(level="DEBUG", mode="json", force=True)| Preset | Mode | Level | Rich |
|---|---|---|---|
dev (default) |
rich |
INFO | Enabled, tracebacks + locals |
test |
plain |
WARNING | Disabled |
prod |
json |
INFO | Disabled |
setup(preset="prod", force=True)If the opentelemetry package is installed, ultilog auto-attaches a trace correlation filter so trace_id and span_id appear on log records inside any active span — no extra setup required. Install with:
pip install "ultilog[otel]"Pretty console output with colors, tracebacks, and path info.
setup(mode="rich", force=True)
get_logger("demo").info("colored output")Simple stream logging for CI, containers, or piped output.
setup(mode="plain", force=True)
get_logger("demo").info("plain output")Machine-readable JSON logs for production and log aggregators.
setup(mode="json", force=True)
get_logger("api").info("request.finished")
# {"level": "INFO", "logger": "api", "message": "request.finished", ...}Context belongs at runtime boundaries, not logger creation time. Use logging_context to bind values that appear in every log record within a scope:
from ultilog import get_logger, logging_context
log = get_logger("worker")
with logging_context(job_id="job_1", queue="emails"):
log.info("job.started") # job_id=job_1 queue=emails
log.info("job.finished") # job_id=job_1 queue=emails
# context automatically restoredContext is contextvars-based, so it works correctly with asyncio and nested scopes:
with logging_context(outer="1"):
with logging_context(inner="2"):
log.info("both") # outer=1 inner=2
log.info("outer only") # outer=1Lower-level helpers are available for integrations:
from ultilog import bind_context, clear_context, get_context
bind_context(request_id="req_123")
get_context() # {"request_id": "req_123"}
clear_context()from fastapi import FastAPI
from ultilog.integrations import install_fastapi_logging
app = FastAPI()
install_fastapi_logging(app)
# Every request gets logging context with request_id, http.method, http.pathfrom ultilog.integrations import UltilogASGIMiddleware
app = UltilogASGIMiddleware(app)from ultilog.integrations import install_celery_logging
install_celery_logging(app)
# Tasks get context with celery_task_id and celery_task_namefrom ultilog.integrations import install_httpx_logging
client = httpx.Client()
install_httpx_logging(client)
# Logs outgoing HTTP requests at DEBUG levelfrom ultilog.integrations import install_sqlalchemy_logging
install_sqlalchemy_logging(engine, level=logging.DEBUG)When structlog is installed, ultilog can configure it with pre-built processor chains:
from ultilog.structlog import configure_structlog
configure_structlog() # console renderer for devChoose a renderer that matches your mode:
from ultilog.models.structlog import StructlogSettings
configure_structlog(StructlogSettings(renderer="json"))With the otel extra installed, configure traces, logs, and metrics:
from ultilog.otel.traces import configure_otel_traces
from ultilog.otel.logs import configure_otel_logs
from ultilog.otel.metrics import configure_otel_metrics
configure_otel_traces(service_name="my-api")
configure_otel_logs(service_name="my-api")
configure_otel_metrics(service_name="my-api")Or configure all signals at once:
from ultilog.otel.exporters import configure_exporters
from ultilog.models.otel import OTelSettings
configure_exporters(OTelSettings(
enabled=True,
service_name="my-api",
traces_enabled=True,
logs_enabled=True,
))Trace/log correlation is automatic when a span is active:
from ultilog.otel.correlation import TraceCorrelationFilter
handler.addFilter(TraceCorrelationFilter())
# Log records get trace_id and span_id attributesSettings use the ULTILOG_ prefix with __ for nesting:
export ULTILOG_PRESET=prod
export ULTILOG_LOGGING__LEVEL=DEBUG
export ULTILOG_LOGGING__MODE=json
export ULTILOG_RICH__SHOW_PATH=falseultilog doctor --json # runtime diagnostics
ultilog show-config # dump effective settings
ultilog validate # check configuration
ultilog demo --mode plain # emit a demo log line
ultilog demo --mode jsonOr via module:
python -m ultilog doctor --jsonultilog provides test utilities so downstream projects can isolate logging state:
from ultilog.testing.reset import reset_ultilog
from ultilog.testing.capture import capture_logs
reset_ultilog() # reset package state
with capture_logs("my.logger") as records:
get_logger("my.logger").info("captured")
assert records[0].getMessage() == "captured"For full control, use configure() with an explicit settings object:
from ultilog import configure, UltilogSettings
settings = UltilogSettings(
preset="prod",
logging=LoggingSettings(level="DEBUG", mode="json"),
context=ContextSettings(enabled=True),
)
configure(settings, force=True)pdm sync -G dev -G docs
pdm run pytest # tests
pdm run ruff check . # lint
pdm run mypy src/ultilog # type-check
pdm run mkdocs serve # docs previewMIT