Skip to content

Unify logging configuration and setup#1703

Open
samdoran wants to merge 10 commits intolightspeed-core:mainfrom
samdoran:use-existing-logger
Open

Unify logging configuration and setup#1703
samdoran wants to merge 10 commits intolightspeed-core:mainfrom
samdoran:use-existing-logger

Conversation

@samdoran
Copy link
Copy Markdown
Contributor

@samdoran samdoran commented May 8, 2026

Description

Major overhaul of the logging system:

  • Unify the logging configuration from lightspeed-stack and uvicorn
  • Change the time format to include milliseconds
  • Pass the unified configuration into uvicorn so that uvicorn logging so that all application logging works consistently
  • Call setup_logging() early on so that logging is setup once and cache the result.
  • Manually set the root logger name of lightspeed_stack so that the logging configuration is applied correctly.

The end result of these changes is that calling get_logger(__name__) results in a logger instance where logger.info() and other methods display log messages as expected and formatted consistently.

Type of change

  • Refactor
  • New feature
  • Bug fix
  • CVE fix
  • Optimization
  • Documentation Update
  • Configuration Update
  • Bump-up service version
  • Bump-up dependent library
  • Bump-up library or tool used for development (does not change the final image)
  • CI configuration change
  • Konflux configuration change
  • Unit tests improvement
  • Integration tests improvement
  • End to end tests improvement
  • Benchmarks improvement

Tools used to create PR

Identify any AI code assistants used in this PR (for transparency and review context)

  • Assisted-by: Claude
  • Written by: me

Related Tickets & Documents

None

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

To run unit tests: pytest tests/unit/test_log.py
To test logging configuration without Rich logging: LIGHTSPEED_STACK_DISABLE_RICH_HANDLER=1 lightspeed-stack or python src/lightspeed_stack.py
To test logging configuration using the default format string: lightspeed-stack or python src/lightspeed_stack.py

Summary by CodeRabbit

  • Refactor
    • Restructured logging initialization for improved integration with the service framework.
    • Logging configuration now supports environment variable customization for handler selection and formatting.
    • Enhanced terminal-aware logging output detection for better console readability.

samdoran added 10 commits May 5, 2026 16:35
Meroge the existing config from uvicorn with the logging for lightspeed-stack
with some modifications and pass that to uvicorn. This ensures the logging
configs work together and do not clobber each other.

Call setup_logging() early in the main entrypoint.
…y in __name__.

The logging library assumes __name__ will be “package.module.module”. Since this
project does not have a package, the value for __name__ varies widely in each
module. Thise breaks design assumpmtions of logging.

To work around this, define a default logger name that is used as the primary
configuration and add a helper function to always get the logger with a name
that aligns with how logging works.
Since there is no root package for this project, manually set the logger
and get the filename in order to show where the log message was emitted.
Use levelprefix for uvicorn.logging.DefaultFormatte.
Move filename and position to end of line so that information is arranged
in most important order from left to right within the line, where the message
came from being least relevant in my thinking compared to the time, log level,
and actual message.
When rich is not selected, use the uvicorn.logging.DefaultFormatter for log
messages. Modify the default format slightly to include miliseconds in the
timestamp.
Add a description of the problem to be addressed in the future.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack

Walkthrough

This PR refactors logging initialization from inline setup scattered across modules into a centralized, cached setup_logging() function. Constants are extracted, core logging logic is consolidated in src/log.py, integration points are updated in the entrypoint and Uvicorn runner, and tests are adapted to the new architecture.

Changes

Logging Setup Refactoring

Layer / File(s) Summary
Logging Constants
src/constants.py
New DEFAULT_LOGGER_NAME constant ("lightspeed_stack") added; DEFAULT_LOG_FORMAT template updated with new field arrangement.
Core Logging Refactoring
src/log.py
Introduces cached setup_logging() that selects Rich or default handler based on TTY and env var, merges config into Uvicorn defaults, and applies via dictConfig. Removes create_log_handler(); simplifies get_logger() to return namespaced loggers under DEFAULT_LOGGER_NAME without handler setup.
App Startup Integration
src/lightspeed_stack.py, src/runners/uvicorn.py
Entrypoint calls setup_logging() at import time. Uvicorn runner signature extended with optional log_config parameter that defaults to setup_logging() output, passed to uvicorn.run().
Test Updates
tests/unit/test_log.py
Adds autouse fixture clearing setup_logging() cache before each test. Logger tests verify namespaced names and handler presence; log-level tests validate getEffectiveLevel(). Expands resolve_log_level tests to cover invalid and default cases. Removes create_log_handler test suite.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Unify logging configuration and setup' directly and clearly summarizes the main change: consolidating the logging system to merge lightspeed-stack and uvicorn logging into a unified configuration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runners/uvicorn.py (1)

17-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Docstring doesn't document the new log_config parameter

🛠 Proposed fix
     """Start the Uvicorn server using the provided service configuration.

     Parameters:
     ----------
         configuration (ServiceConfiguration): Configuration providing host,
         port, workers, and `tls_config` (including `tls_key_path`,
         `tls_certificate_path`, and `tls_key_password`). TLS fields may be None
         and will be forwarded to uvicorn.run as provided.
+        log_config (dict | None): Logging configuration dict passed to uvicorn.run.
+        When None, the centralized setup_logging() result is used.
     """

As per coding guidelines, all functions must "include descriptive docstrings" covering all parameters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runners/uvicorn.py` around lines 17 - 25, The docstring for the
Uvicorn-starting function (start_uvicorn in src/runners/uvicorn.py) is missing
documentation for the new log_config parameter; update the function docstring to
list and describe log_config (type: optional dict or logging config path,
default/behavior: forwarded to uvicorn.run to configure logging) alongside the
existing parameters so callers know what to pass and how it is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/constants.py`:
- Line 228: DEFAULT_LOGGER_NAME is missing the Final[str] annotation; update the
constant declaration for DEFAULT_LOGGER_NAME to be annotated with Final[str]
(e.g., DEFAULT_LOGGER_NAME: Final[str] = "lightspeed_stack") and ensure Final is
imported from typing at the top of src/constants.py (add "from typing import
Final" if it's not already present) so the constant is type-locked like the
others.

In `@src/log.py`:
- Line 1: The file src/log.py fails Black formatting; run the project formatter
and commit the changes: execute "uv tool run black src/log.py" (or "uv tool run
black src tests") to reformat the module (src.log) and then stage and commit the
updated file so the CI Black check passes.
- Around line 59-70: The logger helper uses a DEFAULT_LOGGER_NAME prefix to work
around inconsistent __name__ values across different entrypoints (see get_logger
and DEFAULT_LOGGER_NAME); open a tracking issue to remove that workaround by
standardizing the package/entrypoint layout so __name__ always yields the
expected root, document the change, and include a migration plan: update
get_logger to use __name__ (remove the DEFAULT_LOGGER_NAME concatenation),
refactor any callers that relied on the prefixed logger names (adjust tests and
configuration), and add a small integration test that verifies the root logger
name is consistent when run via all supported entrypoints.
- Around line 107-117: The setup_logging() function currently calls
deep_update(uvicorn.config.LOGGING_CONFIG, logging_conf) then mutates
merged_config which aliases uvicorn.config.LOGGING_CONFIG; instead construct all
conditional changes (the "rich" handler branch and the access/default formatter
entries: the handlers for "uvicorn" and "uvicorn.access" and the
formatters["access"] and formatters["default"]["fmt"/"datefmt"]) inside
logging_conf before calling deep_update so the single deep_update call merges
everything and merged_config is never mutated afterwards; update the code paths
that reference handler, merged_config and logging_conf accordingly so no
post-merge assignments (e.g., merged_config["formatters"][...] = ...) remain.
- Line 89: The Rich handler configuration uses "log_time_format" set to
"%Y-%m-%d %H:%M:%S.%f" which emits 6-digit microseconds instead of the intended
3-digit milliseconds; update the Rich-based logging path to remove the "%f"
subsecond directive (or omit log_time_format entirely) and rely on RichHandler's
own millisecond column via show_time=True, ensuring consistency with
DEFAULT_LOG_FORMAT which uses "%(msecs)03d"; modify the code that sets
log_time_format (symbol: log_time_format) and the Rich handler instantiation
(symbol: RichHandler, show_time) so the Rich path does not output 6-digit
microseconds.
- Line 11: The import of pydantic.v1.utils.deep_update in src/log.py uses an
internal v1-compat API — remove that import, add a small local helper function
named _deep_update(base: dict, override: dict) that recursively merges override
into base (preserving dict semantics), and replace all usages of
deep_update(...) with _deep_update(...); update any function or module
references in this file that call deep_update to call _deep_update instead so
there is no dependency on pydantic internals.
- Around line 73-74: Add a clear docstring to the public function
setup_logging() that explains what the function does (e.g., configures and
returns the logging configuration dict, applies any global logging settings or
handlers) and any side effects (such as calling logging.config.dictConfig or
mutating global logger state); ensure the docstring appears immediately below
the def setup_logging() line and follows project docstring style so pydocstyle
D103 is satisfied.

---

Outside diff comments:
In `@src/runners/uvicorn.py`:
- Around line 17-25: The docstring for the Uvicorn-starting function
(start_uvicorn in src/runners/uvicorn.py) is missing documentation for the new
log_config parameter; update the function docstring to list and describe
log_config (type: optional dict or logging config path, default/behavior:
forwarded to uvicorn.run to configure logging) alongside the existing parameters
so callers know what to pass and how it is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1c2b1d91-2f32-49b8-ae1e-1cede9a1feb3

📥 Commits

Reviewing files that changed from the base of the PR and between 4feeaed and 113ef13.

📒 Files selected for processing (5)
  • src/constants.py
  • src/lightspeed_stack.py
  • src/log.py
  • src/runners/uvicorn.py
  • tests/unit/test_log.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: unit_tests (3.12)
  • GitHub Check: build-pr
  • GitHub Check: Pylinter
  • GitHub Check: E2E: library mode / ci / group 3
  • GitHub Check: E2E: server mode / ci / group 2
  • GitHub Check: E2E: library mode / ci / group 2
  • GitHub Check: E2E: server mode / ci / group 3
  • GitHub Check: E2E: server mode / ci / group 1
  • GitHub Check: E2E: library mode / ci / group 1
  • GitHub Check: E2E Tests for Lightspeed Evaluation job
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.py: Use absolute imports for internal modules: from authentication import get_auth_dependency
Llama Stack imports: Use from llama_stack_client import AsyncLlamaStackClient
Check constants.py for shared constants before defining new ones
All modules must start with descriptive docstrings explaining purpose
Use logger = get_logger(__name__) from log.py for module logging
All functions must have complete type annotations for parameters and return types, use modern syntax (str | int), and include descriptive docstrings
Use snake_case with descriptive, action-oriented names for functions (get_, validate_, check_)
Avoid in-place parameter modification anti-patterns; return new data structures instead of modifying function parameters
Use async def for I/O operations and external API calls
Use standard log levels with clear purposes: debug() for diagnostic info, info() for program execution, warning() for unexpected events, error() for serious problems
All classes must have descriptive docstrings explaining purpose and use PascalCase with standard suffixes: Configuration, Error/Exception, Resolver, Interface
Abstract classes must use ABC with @abstractmethod decorators
Follow Google Python docstring conventions with required sections: Parameters, Returns, Raises, and Attributes for classes

Files:

  • src/constants.py
  • src/lightspeed_stack.py
  • src/runners/uvicorn.py
  • src/log.py
src/constants.py

📄 CodeRabbit inference engine (AGENTS.md)

Use constants.py for shared constants with descriptive comments and type hints using Final[type]

Files:

  • src/constants.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.py: Use pytest for all unit and integration tests; do not use unittest
Use pytest.mark.asyncio marker for async tests

Files:

  • tests/unit/test_log.py
🪛 GitHub Actions: Black / 0_black.txt
src/log.py

[error] 1-1: Black formatting check failed (uv tool run black --check src tests). File would be reformatted. Run 'uv tool run black src tests' (or 'black --write') to fix.

🪛 GitHub Actions: Black / black
src/log.py

[error] 1-1: Black --check failed: would reformat /home/runner/work/lightspeed-stack/lightspeed-stack/src/log.py (1 file would be reformatted). Run 'uv tool run black src/log.py' (or 'uv tool run black --write src tests') to apply formatting.

🪛 GitHub Actions: Pydocstyle / 0_pydocstyle.txt
src/log.py

[error] 74-74: pydocstyle failed (D103): Missing docstring in public function setup_logging.

🪛 GitHub Actions: Pydocstyle / pydocstyle
src/log.py

[error] 74-74: pydocstyle (uv tool run pydocstyle -v src tests) reported: D103: Missing docstring in public function setup_logging.

🔇 Additional comments (2)
src/lightspeed_stack.py (1)

13-19: LGTM — logging initialization order is correct

setup_logging() is called before get_logger(__name__), ensuring dictConfig is applied before the logger hierarchy is first accessed.

tests/unit/test_log.py (1)

14-16: The fixture is working correctly. deep_update from pydantic.v1.utils creates a new merged dictionary rather than mutating uvicorn.config.LOGGING_CONFIG in-place, so setup_logging() does not leave persistent global state mutations across tests. Clearing the @lru_cache ensures the function body re-executes for each test, which is the appropriate behavior since tests modify environment variables via monkeypatch between runs.

Comment thread src/constants.py
# Environment variable name for configurable log level
LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR: Final[str] = "LIGHTSPEED_STACK_LOG_LEVEL"
# Default log level when environment variable is not set
DEFAULT_LOGGER_NAME = "lightspeed_stack"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

DEFAULT_LOGGER_NAME is missing its Final[str] annotation

Every other constant in this file uses Final[type]. This one does not, leaving it open to accidental reassignment and breaking type-checker guarantees.

🛠 Proposed fix
-DEFAULT_LOGGER_NAME = "lightspeed_stack"
+DEFAULT_LOGGER_NAME: Final[str] = "lightspeed_stack"

As per coding guidelines, src/constants.py requires "type hints using Final[type]" for shared constants.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DEFAULT_LOGGER_NAME = "lightspeed_stack"
DEFAULT_LOGGER_NAME: Final[str] = "lightspeed_stack"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/constants.py` at line 228, DEFAULT_LOGGER_NAME is missing the Final[str]
annotation; update the constant declaration for DEFAULT_LOGGER_NAME to be
annotated with Final[str] (e.g., DEFAULT_LOGGER_NAME: Final[str] =
"lightspeed_stack") and ensure Final is imported from typing at the top of
src/constants.py (add "from typing import Final" if it's not already present) so
the constant is type-locked like the others.

Comment thread src/log.py
@@ -1,14 +1,19 @@
"""Log utilities."""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Black formatting check fails — reformat src/log.py before merge

The CI pipeline reports Black would reformat this file. Run uv tool run black src/log.py (or uv tool run black src tests) to fix.

🧰 Tools
🪛 GitHub Actions: Black / 0_black.txt

[error] 1-1: Black formatting check failed (uv tool run black --check src tests). File would be reformatted. Run 'uv tool run black src tests' (or 'black --write') to fix.

🪛 GitHub Actions: Black / black

[error] 1-1: Black --check failed: would reformat /home/runner/work/lightspeed-stack/lightspeed-stack/src/log.py (1 file would be reformatted). Run 'uv tool run black src/log.py' (or 'uv tool run black --write src tests') to apply formatting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log.py` at line 1, The file src/log.py fails Black formatting; run the
project formatter and commit the changes: execute "uv tool run black src/log.py"
(or "uv tool run black src tests") to reformat the module (src.log) and then
stage and commit the updated file so the CI Black check passes.

Comment thread src/log.py

from rich.logging import RichHandler
import uvicorn.config
from pydantic.v1.utils import deep_update
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

pydantic.v1.utils.deep_update is an internal v1-compat shim — replace with a self-contained helper

pydantic.v1.utils is part of the v1 compatibility layer bundled in pydantic v2, not a public API. Functionality marked as deprecated will not be removed until the next major V3 release, but deep_update from pydantic.v1.utils is an internal implementation detail, not a stable public export. Importing it creates an unnecessary coupling to pydantic's internals for a trivial utility. A 5-line replacement eliminates the dependency entirely.

♻️ Proposed replacement

Remove the pydantic import and add a local helper:

-from pydantic.v1.utils import deep_update
def _deep_update(base: dict, override: dict) -> dict:
    """Recursively merge override into base, returning a new dict."""
    result = base.copy()
    for key, value in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = _deep_update(result[key], value)
        else:
            result[key] = value
    return result

Then replace all calls to deep_update(...) with _deep_update(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log.py` at line 11, The import of pydantic.v1.utils.deep_update in
src/log.py uses an internal v1-compat API — remove that import, add a small
local helper function named _deep_update(base: dict, override: dict) that
recursively merges override into base (preserving dict semantics), and replace
all usages of deep_update(...) with _deep_update(...); update any function or
module references in this file that call deep_update to call _deep_update
instead so there is no dependency on pydantic internals.

Comment thread src/log.py
Comment on lines +59 to +70
"""Create a common logger for all modules in this package."""
# FIXME: Remove the need for this function.
#
# Normally this is derived from the package name (__name__).
#
# Since this program is sometimes called from from the entrypoint and
# sometimes called from src/lightspeed_stack.py, the value for __name__
# does not contain a consistent root value.
#
# How the application is installed and run needs to be streamlined so that
# __name__ provides the expected value in all cases.
return logging.getLogger(f"{DEFAULT_LOGGER_NAME}.{name}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

FIXME: inconsistent __name__ for the root logger — tracked for follow-up

The FIXME at line 60 acknowledges that get_logger(__name__) produces different root prefixes depending on how the application is launched.

Would you like me to open a new issue to track streamlining the application's package layout so __name__ always resolves to the expected root, removing the need for the DEFAULT_LOGGER_NAME prefix workaround entirely?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log.py` around lines 59 - 70, The logger helper uses a
DEFAULT_LOGGER_NAME prefix to work around inconsistent __name__ values across
different entrypoints (see get_logger and DEFAULT_LOGGER_NAME); open a tracking
issue to remove that workaround by standardizing the package/entrypoint layout
so __name__ always yields the expected root, document the change, and include a
migration plan: update get_logger to use __name__ (remove the
DEFAULT_LOGGER_NAME concatenation), refactor any callers that relied on the
prefixed logger names (adjust tests and configuration), and add a small
integration test that verifies the root logger name is consistent when run via
all supported entrypoints.

Comment thread src/log.py
Comment on lines +73 to +74
@lru_cache
def setup_logging() -> dict[t.Any, t.Any]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

setup_logging() is missing a docstring — CI reports D103

The pydocstyle pipeline fails with D103: Missing docstring in public function setup_logging. Add a docstring describing the function's behavior and side effects.

🛠 Proposed fix
 `@lru_cache`
 def setup_logging() -> dict[t.Any, t.Any]:
+    """Configure and return the merged logging configuration.
+
+    Selects a Rich or uvicorn default handler based on TTY detection and the
+    LIGHTSPEED_STACK_DISABLE_RICH_HANDLER environment variable, merges the
+    computed configuration into uvicorn's default logging config, applies it
+    via logging.config.dictConfig, and returns the merged configuration dict.
+    Result is cached; call setup_logging.cache_clear() to force re-initialization.
+
+    Returns:
+        dict: The merged logging configuration passed to dictConfig.
+    """
🧰 Tools
🪛 GitHub Actions: Pydocstyle / 0_pydocstyle.txt

[error] 74-74: pydocstyle failed (D103): Missing docstring in public function setup_logging.

🪛 GitHub Actions: Pydocstyle / pydocstyle

[error] 74-74: pydocstyle (uv tool run pydocstyle -v src tests) reported: D103: Missing docstring in public function setup_logging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log.py` around lines 73 - 74, Add a clear docstring to the public
function setup_logging() that explains what the function does (e.g., configures
and returns the logging configuration dict, applies any global logging settings
or handlers) and any side effects (such as calling logging.config.dictConfig or
mutating global logger state); ensure the docstring appears immediately below
the def setup_logging() line and follows project docstring style so pydocstyle
D103 is satisfied.

Comment thread src/log.py
"rich": {
"()": "rich.logging.RichHandler",
"show_time": True,
"log_time_format": "%Y-%m-%d %H:%M:%S.%f",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

log_time_format uses %f (microseconds, 6 digits) — PR goal was milliseconds

The %f strftime directive expands to 6-digit microseconds (e.g., 123456). The non-Rich path correctly uses %(msecs)03d for 3-digit milliseconds in DEFAULT_LOG_FORMAT. The Rich handler will show 6-digit precision instead of the intended 3-digit millisecond granularity.

🛠 Proposed fix
-                "log_time_format": "%Y-%m-%d %H:%M:%S.%f",
+                "log_time_format": "%Y-%m-%d %H:%M:%S",

Rich's RichHandler renders its own millisecond column separately when show_time=True, so the sub-second portion in log_time_format is typically redundant.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"log_time_format": "%Y-%m-%d %H:%M:%S.%f",
"log_time_format": "%Y-%m-%d %H:%M:%S",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log.py` at line 89, The Rich handler configuration uses "log_time_format"
set to "%Y-%m-%d %H:%M:%S.%f" which emits 6-digit microseconds instead of the
intended 3-digit milliseconds; update the Rich-based logging path to remove the
"%f" subsecond directive (or omit log_time_format entirely) and rely on
RichHandler's own millisecond column via show_time=True, ensuring consistency
with DEFAULT_LOG_FORMAT which uses "%(msecs)03d"; modify the code that sets
log_time_format (symbol: log_time_format) and the Rich handler instantiation
(symbol: RichHandler, show_time) so the Rich path does not output 6-digit
microseconds.

Comment thread src/log.py
Comment on lines +107 to +117
merged_config = deep_update(uvicorn.config.LOGGING_CONFIG, logging_conf)

if handler == "rich":
merged_config["loggers"]["uvicorn"]["handlers"] = [handler]
merged_config["loggers"]["uvicorn.access"]["handlers"] = [handler]
else:
merged_config["formatters"]["access"]["fmt"] = (
'ACCESS %(asctime)s.%(msecs)03d %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
)
merged_config["formatters"]["default"]["fmt"] = DEFAULT_LOG_FORMAT
merged_config["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

setup_logging() silently mutates uvicorn.config.LOGGING_CONFIG — corrupts global state on cache replay

deep_update(uvicorn.config.LOGGING_CONFIG, logging_conf) performs only a shallow copy at the top level. Because logging_conf contains no "formatters" key, merged_config["formatters"] is the same dict object as uvicorn.config.LOGGING_CONFIG["formatters"]. The subsequent assignments on lines 113–117 therefore permanently mutate uvicorn's module-level global:

merged_config["formatters"]["access"]["fmt"] = ...   # mutates uvicorn.config.LOGGING_CONFIG
merged_config["formatters"]["default"]["fmt"] = ...  # mutates uvicorn.config.LOGGING_CONFIG
merged_config["formatters"]["default"]["datefmt"] = ...  # mutates uvicorn.config.LOGGING_CONFIG

Similarly in the rich path (lines 110–111), merged_config["loggers"]["uvicorn"] is an alias for the original uvicorn logger dict, so merged_config["loggers"]["uvicorn"]["handlers"] = [handler] also mutates the global.

In tests this causes silent pollution: cache_clear() re-runs the function body against an already-mutated uvicorn.config.LOGGING_CONFIG, making the test environment progressively diverge from a clean state.

Root fix: build all conditional configuration inside logging_conf before the single deep_update call, so deep_update handles all merges correctly and no post-merge mutations are needed.

🛠 Proposed fix
-    logging_conf = {
+    # Build formatters/loggers conditionally so deep_update handles all merges
+    # and uvicorn.config.LOGGING_CONFIG is never mutated.
+    logging_conf: dict[t.Any, t.Any] = {
         "version": 1,
         "disable_existing_loggers": False,
         "handlers": {
             "rich": {
                 "()": "rich.logging.RichHandler",
                 "show_time": True,
-                "log_time_format": "%Y-%m-%d %H:%M:%S.%f",
+                "log_time_format": "%Y-%m-%d %H:%M:%S",
                 "level": log_level,
             },
         },
         "loggers": {
             DEFAULT_LOGGER_NAME: {
                 "handlers": [handler],
                 "level": log_level,
                 "propagate": False,
             },
             "llama_stack_client": {
                 "handlers": [handler],
                 "level": log_level,
                 "propagate": False,
             },
         },
     }

     if handler == "rich":
-        merged_config = deep_update(uvicorn.config.LOGGING_CONFIG, logging_conf)
-        merged_config["loggers"]["uvicorn"]["handlers"] = [handler]
-        merged_config["loggers"]["uvicorn.access"]["handlers"] = [handler]
-    else:
-        merged_config = deep_update(uvicorn.config.LOGGING_CONFIG, logging_conf)
-        merged_config["formatters"]["access"]["fmt"] = (
-            'ACCESS %(asctime)s.%(msecs)03d %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
-        )
-        merged_config["formatters"]["default"]["fmt"] = DEFAULT_LOG_FORMAT
-        merged_config["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M"
+        logging_conf["loggers"]["uvicorn"] = {"handlers": ["rich"]}
+        logging_conf["loggers"]["uvicorn.access"] = {"handlers": ["rich"]}
+    else:
+        logging_conf["formatters"] = {
+            "access": {
+                "fmt": 'ACCESS %(asctime)s.%(msecs)03d %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
+            },
+            "default": {
+                "fmt": DEFAULT_LOG_FORMAT,
+                "datefmt": "%Y-%m-%d %H:%M",
+            },
+        }

+    merged_config = deep_update(uvicorn.config.LOGGING_CONFIG, logging_conf)

     logging.config.dictConfig(merged_config)
     return merged_config
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log.py` around lines 107 - 117, The setup_logging() function currently
calls deep_update(uvicorn.config.LOGGING_CONFIG, logging_conf) then mutates
merged_config which aliases uvicorn.config.LOGGING_CONFIG; instead construct all
conditional changes (the "rich" handler branch and the access/default formatter
entries: the handlers for "uvicorn" and "uvicorn.access" and the
formatters["access"] and formatters["default"]["fmt"/"datefmt"]) inside
logging_conf before calling deep_update so the single deep_update call merges
everything and merged_config is never mutated afterwards; update the code paths
that reference handler, merged_config and logging_conf accordingly so no
post-merge assignments (e.g., merged_config["formatters"][...] = ...) remain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant