-
Notifications
You must be signed in to change notification settings - Fork 85
Unify logging configuration and setup #1703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bf6610b
e8f92ca
ea600a9
502862c
34833af
43a4bb7
0aebf95
922a427
e97c1fd
113ef13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,14 +1,19 @@ | ||||||
| """Log utilities.""" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Black formatting check fails — reformat The CI pipeline reports Black would reformat this file. Run 🧰 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 |
||||||
|
|
||||||
| import logging | ||||||
| import logging.config | ||||||
| import os | ||||||
| import sys | ||||||
| import typing as t | ||||||
| from functools import lru_cache | ||||||
|
|
||||||
| from rich.logging import RichHandler | ||||||
| import uvicorn.config | ||||||
| from pydantic.v1.utils import deep_update | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
♻️ Proposed replacementRemove the pydantic import and add a local helper: -from pydantic.v1.utils import deep_updatedef _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 resultThen replace all calls to 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| from constants import ( | ||||||
| DEFAULT_LOG_FORMAT, | ||||||
| DEFAULT_LOG_LEVEL, | ||||||
| DEFAULT_LOGGER_NAME, | ||||||
| LIGHTSPEED_STACK_DISABLE_RICH_HANDLER_ENV_VAR, | ||||||
| LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, | ||||||
| ) | ||||||
|
|
@@ -50,62 +55,67 @@ def resolve_log_level() -> int: | |||||
| return validated_level | ||||||
|
|
||||||
|
|
||||||
| def create_log_handler() -> logging.Handler: | ||||||
| """ | ||||||
| Create and return a configured log handler based on TTY availability and environment settings. | ||||||
|
|
||||||
| If LIGHTSPEED_STACK_DISABLE_RICH_HANDLER is set to any non-empty value, | ||||||
| returns a StreamHandler with plain-text formatting. Otherwise, if stderr | ||||||
| is connected to a terminal (TTY), returns a RichHandler for rich-formatted | ||||||
| console output. If neither condition is met, returns a StreamHandler with | ||||||
| plain-text formatting suitable for non-TTY environments (e.g., containers). | ||||||
|
|
||||||
| Returns: | ||||||
| logging.Handler: A configured handler instance (RichHandler or StreamHandler). | ||||||
| """ | ||||||
| # Check if RichHandler is explicitly disabled via environment variable | ||||||
| if os.environ.get(LIGHTSPEED_STACK_DISABLE_RICH_HANDLER_ENV_VAR): | ||||||
| handler = logging.StreamHandler() | ||||||
| handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT)) | ||||||
| return handler | ||||||
|
|
||||||
| if sys.stderr.isatty(): | ||||||
| # RichHandler's columnar layout assumes a real terminal. | ||||||
| # RichHandler handles its own formatting, so no formatter is set. | ||||||
| return RichHandler() | ||||||
|
|
||||||
| # In containers without a TTY, Rich falls back to 80 columns and | ||||||
| # the columns consume most of that width, leaving ~40 chars for the actual message. | ||||||
| # Tracebacks become nearly unreadable. Use a plain StreamHandler instead. | ||||||
| handler = logging.StreamHandler() | ||||||
| handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT)) | ||||||
| return handler | ||||||
|
|
||||||
|
|
||||||
| def get_logger(name: str) -> logging.Logger: | ||||||
| """ | ||||||
| Get a logger configured for Rich console output. | ||||||
|
|
||||||
| The returned logger has its level set based on the LIGHTSPEED_STACK_LOG_LEVEL | ||||||
| environment variable (defaults to INFO), its handlers replaced with a single | ||||||
| handler (RichHandler for TTY or StreamHandler for non-TTY), and propagation | ||||||
| to ancestor loggers disabled. | ||||||
|
|
||||||
| Parameters: | ||||||
| ---------- | ||||||
| name (str): Name of the logger to retrieve or create. | ||||||
|
|
||||||
| Returns: | ||||||
| ------- | ||||||
| logging.Logger: The configured logger instance. | ||||||
| """ | ||||||
| logger = logging.getLogger(name) | ||||||
| """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}") | ||||||
|
Comment on lines
+59
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial FIXME: inconsistent The FIXME at line 60 acknowledges that Would you like me to open a new issue to track streamlining the application's package layout so 🤖 Prompt for AI Agents |
||||||
|
|
||||||
|
|
||||||
| @lru_cache | ||||||
| def setup_logging() -> dict[t.Any, t.Any]: | ||||||
|
Comment on lines
+73
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The pydocstyle pipeline fails with 🛠 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 🪛 GitHub Actions: Pydocstyle / pydocstyle[error] 74-74: pydocstyle (uv tool run pydocstyle -v src tests) reported: D103: Missing docstring in public function 🤖 Prompt for AI Agents |
||||||
| handler = "default" | ||||||
| log_level = resolve_log_level() | ||||||
| if sys.stderr.isatty() and not os.environ.get( | ||||||
| LIGHTSPEED_STACK_DISABLE_RICH_HANDLER_ENV_VAR | ||||||
| ): | ||||||
| handler = "rich" | ||||||
|
|
||||||
| logging_conf = { | ||||||
| "version": 1, | ||||||
| "disable_existing_loggers": False, | ||||||
| "handlers": { | ||||||
| "rich": { | ||||||
| "()": "rich.logging.RichHandler", | ||||||
| "show_time": True, | ||||||
| "log_time_format": "%Y-%m-%d %H:%M:%S.%f", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The 🛠 Proposed fix- "log_time_format": "%Y-%m-%d %H:%M:%S.%f",
+ "log_time_format": "%Y-%m-%d %H:%M:%S",Rich's 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| "level": log_level, | ||||||
| }, | ||||||
| }, | ||||||
| "loggers": { | ||||||
| DEFAULT_LOGGER_NAME: { | ||||||
| "handlers": [handler], | ||||||
| "level": log_level, | ||||||
| "propagate": False, | ||||||
| }, | ||||||
| "llama_stack_client": { | ||||||
| "handlers": [handler], | ||||||
| "level": log_level, | ||||||
| "propagate": False, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
|
|
||||||
| 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" | ||||||
|
Comment on lines
+107
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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_CONFIGSimilarly in the rich path (lines 110–111), In tests this causes silent pollution: Root fix: build all conditional configuration inside 🛠 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 |
||||||
|
|
||||||
| # Skip reconfiguration if logger already has handlers from a prior call | ||||||
| if logger.handlers: | ||||||
| return logger | ||||||
| logging.config.dictConfig(merged_config) | ||||||
|
|
||||||
| logger.handlers = [create_log_handler()] | ||||||
| logger.propagate = False | ||||||
| logger.setLevel(resolve_log_level()) | ||||||
| return logger | ||||||
| return merged_config | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DEFAULT_LOGGER_NAMEis missing itsFinal[str]annotationEvery 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
As per coding guidelines,
src/constants.pyrequires "type hints usingFinal[type]" for shared constants.📝 Committable suggestion
🤖 Prompt for AI Agents