Skip to content

Litestar replaces lite-bootstrap's root log handler and writes every litestar record twice #338

Closed

Description

@lesnik512

LitestarLoggingInstrument._configure_structlog_loggers passes StructLoggingConfig(standard_lib_logging_config=None) (lite_bootstrap/bootstrappers/litestar_bootstrapper.py:195-201). Litestar's StructLoggingConfig.__post_init__ treats None as "use the default" and replaces it with a stdlib LoggingConfig (litestar/logging/config.py:509-519 in litestar 2.24.0), whose configure() runs dictConfig at app startup. Observed after startup on 705d8a3:

  • The root logger's handlers are [QueueHandler]. lite-bootstrap's root StreamHandler with ProcessorFormatter (lite_bootstrap/instruments/logging_instrument.py:176-191) is gone, so stdlib records from any library come out in Litestar's format (no logger, no tracing, an extra message key) on stderr instead of lite-bootstrap's JSON on stdout.
  • The litestar logger has Litestar's QueueHandler, and MemoryLoggerFactory adds its MemoryHandler on first use (lite_bootstrap/instruments/logging_factory.py:120-133). Each record on that logger (the access log from litestar_logging_middleware_enabled, Litestar's exception log) is written twice: once as lite-bootstrap JSON, once by Litestar's queue listener thread as a JSON string nested inside another JSON object.

The duplicate also costs time. Measured on 2026-10-24 at 705d8a3 on Linux with 2 CPUs (CPython 3.14.9), with litestar_logging_middleware_enabled=True and the fix below as a prototype: 29 µs per request in-process (+74.4 to +45.2 µs over no logging) and 61 µs on Granian (1 worker, ab -k -c 16, +115.4 to +54.0 µs). With the access log off and nothing else logging, it costs nothing. The tables and scripts are to be published with #335.

A fix that worked in the prototype: pass standard_lib_logging_config=LoggingConfig(configure_root_logger=False, loggers={"litestar": {"level": ..., "handlers": [], "propagate": False}}). That leaves one line per record, keeps lite-bootstrap's root handler, and renders foreign records as lite-bootstrap JSON.

Separately, the access log renders content_type as ["",{}], Litestar's (media_type, options) tuple. Out of scope here; worth its own issue if it matters.

Work

  • Test first, both failing today: after a Litestar app bootstrapped with logging on has started (TestClient as a context manager), the root logger still carries lite-bootstrap's StreamHandler and no QueueHandler; and with litestar_logging_middleware_enabled=True one request produces exactly one "HTTP Request" and one "HTTP Response" record across stdout and stderr.
  • Pass the LoggingConfig above from _configure_structlog_loggers, with the litestar level taken from logging_log_level.
  • Check that Litestar's exception log still reaches lite-bootstrap's output once, and that teardown() leaves no Litestar handler behind.
  • Docs: none, unless docs/integrations/litestar.md shows the old output.

Verify: the new tests pass along with the existing access-log tests in tests/test_litestar_bootstrap.py.

Changes the output of stdlib records on Litestar back to lite-bootstrap's JSON on stdout, as on every other framework. A service that parsed Litestar's format on stderr has to adjust. No config change.

Activity

  1. added
    bugSomething isn't working
    needs-triageMaintainer needs to evaluate this issue
    on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-triageMaintainer needs to evaluate this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions