Choosing a Python logging library: six options compared

Compare logging, Loguru, structlog, Eliot, Logbook, and picologging by configuration, structured output, integration effort, and operational requirements.

Best practices
Choosing a Python logging library: six options compared illustration

Choosing a Python logging library is partly an API decision and partly an integration decision. A convenient logging call is useful, but your framework, background workers, and third-party libraries still need to produce records that your collection pipeline can understand.

Python's standard logging module is a practical starting point. Loguru reduces configuration work, structlog makes structured events explicit, and picologging targets the cost of the logging implementation itself. Eliot and Logbook offer different programming models that may fit an existing codebase or a specific requirement.

The right choice depends on what you need to change. If inconsistent fields are the problem, a faster logger will not fix them. If synchronous output is blocking an event loop, changing the message API alone will not remove the I/O.

Compare the programming models first

Library Main design choice Structured output Configuration and integration Evaluate it when
logging Standard logger, handler, formatter, and filter model Add a JSON formatter More explicit setup; broad library compatibility You want a predictable default that fits the existing Python ecosystem
Loguru A shared logger with configurable sinks serialize=True Small initial configuration; bridge standard-library records as needed Scripts and services benefit from built-in rotation, retention, and context binding
structlog Event dictionaries processed through a processor chain Built into the event model Configure processors and, if used, the standard-library integration Your team needs a consistent event schema and request context
Eliot Nested actions with causal relationships Structured action records Instrument operations as actions and configure a destination You need to reconstruct the steps within a task
Logbook An alternative logging API with handler stacks Depends on handler and formatting choices A separate ecosystem to integrate and maintain An existing application uses it or its handler model meets a specific need
picologging A native implementation of familiar logging APIs Use compatible formatters Verify required API and handler support Profiling shows logging CPU overhead is material

There is no meaningful universal performance ranking for these libraries. Message formatting, serialization, locks, queueing, and the destination can each dominate. Benchmark the configuration you intend to deploy, including exceptions and realistic field sizes.

Standard logging: the lowest-friction default

The standard library gives you named loggers, handlers that select destinations, formatters that render records, and filters that adjust or reject them. dictConfig lets the application configure that system at startup. Many Python frameworks and dependencies already emit standard-library records, so a common root handler can collect them without changing their code.

The trade-off is configuration effort. JSON is not the default; add a formatter such as python-json-logger or implement one. Message interpolation uses % placeholders when arguments are passed separately, although the outer record format can use other supported styles.

For a conventional service, logging plus centralized configuration and a JSON formatter is often enough. Do not replace it simply because another library has a shorter hello-world example.

Loguru: fewer moving parts to configure

Loguru bundles sink configuration, file rotation, retention, compression, and JSON serialization behind its logger API:

from loguru import logger

logger.remove()  # Remove the default stderr sink for this file-only example.
logger.add(
    "logs/app.log",
    rotation="100 MB",
    retention="30 days",
    compression="zip",
    serialize=True,
    level="INFO",
    diagnose=False,
)

logger.info("User {user} placed order {order}", user="user-42", order="A-1024")

bind() attaches fields to a logger, and contextualize() provides scoped context. logger.catch can record unhandled exceptions around a function, but check its exception propagation behavior before adding it to application code. The example disables diagnostic variable output so exception reporting does not expose local values. See the Loguru logger API.

The integration work is easy to miss: dependencies using logging do not automatically start using Loguru. If all records need to share its sinks, install an interception handler and test exception information, levels, and caller attribution. Also inspect the serialized envelope; Loguru's JSON fields are not identical to those emitted by a standard JSON formatter.

Loguru fits teams that value a compact configuration and are willing to own that bridge. Project size alone is not a reason to accept or reject it.

structlog: make the event schema explicit

With structlog, an event begins as a dictionary. Processors add or transform fields before a renderer produces the final output:

import structlog

structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.add_log_level,
        structlog.processors.JSONRenderer(ensure_ascii=False),
    ],
)
log = structlog.get_logger()
log.info("Order payment completed", order_id="A-1024", amount=99.00)

This is useful when events must carry consistent business identifiers and request context. A bound logger retains its bound fields, and processors can normalize exceptions as well as ordinary messages.

structlog can operate independently or integrate with standard logging; it is not merely a mandatory wrapper around it. When combining them, configure both sides and decide where rendering occurs. A ProcessorFormatter can help give third-party records and structlog events a consistent format. The standard-library integration guide describes those choices.

The cost is learning the processor and context model. That investment is easier to justify across services with a shared schema than in a short script that only needs a few diagnostics.

Eliot: describe work as nested actions

Eliot records the beginning and end of actions, including nested actions, so a task can be reconstructed as a tree:

from eliot import start_action

with start_action(action_type="process_order", order_id="A-1024"):
    validate()  # Application-defined validation.
    charge()    # Application-defined payment operation.

Configure an Eliot destination separately before expecting output from this fragment. Action records include identifiers and hierarchy information rather than leaving you to infer every relationship from timestamps. See Eliot's introduction.

That model requires explicit instrumentation around the operations you want to understand. It is useful for detailed task histories, but it is not interchangeable with end-to-end distributed tracing. If the requirement is to follow requests through many services, compare the work of adopting Eliot with instrumenting APM and attaching a trace_id to ordinary logs. Existing tracing instrumentation may cover more of that requirement with less application-specific code.

Logbook: an alternative handler model

Logbook provides a logging API built around handlers and handler stacks. That model can be useful in applications that need scoped handling behavior, and existing Logbook applications have no automatic reason to migrate merely because another library is more familiar.

For a new service, weigh that API against the cost of integrating dependencies that use the standard library. Check the project's current release support, Python compatibility, and the handlers you require. The Logbook documentation remains the appropriate reference; a blanket claim that the project is inactive is not a sound selection criterion.

Choose it for a concrete fit, rather than as a general replacement for an API you dislike.

picologging: profile before replacing the implementation

picologging implements a familiar logging API in native code. At the call site, migration can look small:

import picologging as logging

logger = logging.getLogger(__name__)
logger.info("Worker started")

You still need a handler and an appropriate level to see that INFO record. This fragment demonstrates the API, not a complete configuration.

The project targets compatibility with standard logging, but you should verify the required features and version support rather than assume every handler, filter, formatter, or extension is interchangeable. Changing this import also does not replace the logging module inside third-party packages. Check the picologging documentation and run your own integration tests.

It is worth evaluating when profiling attributes a meaningful amount of CPU time to logging. If disk or network I/O dominates, faster record construction may have little effect on request latency.

Match the choice to the problem

For a typical application, start with standard logging, dictConfig, and a JSON formatter. Use Loguru when built-in sink management reduces work enough to justify its integration layer. Use structlog when a shared event schema and contextual fields are central requirements.

Evaluate picologging after measuring a logging bottleneck. Evaluate Eliot when the action tree itself is valuable. Keep Logbook where it fits the application, and assess its current support before adopting it elsewhere.

Across all six, define the output contract before standardizing the implementation: timestamp format, severity, message, logger or component name, service, and correlation identifiers. Teams can use different libraries and still produce compatible telemetry.

Send a consistent event schema to Guance

The collection architecture can remain the same even when applications use different libraries:

  1. Emit JSON to stdout in containers or to a managed file on hosts.
  2. Configure DataKit to collect that stream or file and assign source and service consistently.
  3. Parse each library's actual JSON shape, then normalize time and severity with Pipeline where needed.
  4. Verify filtering, alert rules, retention, and trace correlation against the resulting fields.

Do not assume JSON is parsed automatically in every collector configuration. Loguru's nested records, structlog's event field, and standard-library message fields may need different mappings. Use the DataKit collection overview to choose the input and Pipeline to define the transformation.

After collection, use the Log Explorer for investigation and configure indexes according to retention and access requirements. Correlation with APM still requires matching trace identifiers; choosing a particular logging library does not supply them automatically.

Frequently asked questions

How do I route third-party logs through Loguru?

Implement a standard-library logging.Handler that forwards records into Loguru, then install it at the application entry point. For example, after defining InterceptHandler, logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) replaces the root handlers. This does not necessarily override library loggers with their own handlers or propagate=False; inspect those separately. Never install a two-way bridge that forwards records back into itself.

Does structlog replace standard logging?

It can provide the application-facing API and write output independently, or use standard logging for dispatch. Choose one rendering path deliberately; otherwise you can end up with JSON encoded inside another JSON message.

Should every project use the same library?

A shared output schema is usually the more important requirement. Standardize field names, timestamp semantics, severity mapping, and the collection destination. Standardize the library too when doing so materially reduces configuration and support work.

What matters for asyncio services?

Synchronous handlers can block the event loop. QueueHandler and QueueListener move output work to a listener thread; plan for queue capacity and shutdown flushing. Loguru offers queued sinks through enqueue=True. Lower CPU overhead in picologging does not make a blocking destination asynchronous. Test behavior under a slow or unavailable destination, not only under normal load.

Related reading

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting