-
Notifications
You must be signed in to change notification settings - Fork 86
LCORE- 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
e5ec49b
4de6157
5b4d6c7
77c2e0d
29e13ad
aa7d197
b913dbf
0905eb5
40a2df0
8bf8157
c72642f
0e5128b
d1280fe
1f10395
237530c
56206b4
67b4fbb
c32fc26
0839360
b3b1059
c0ac8e1
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,19 +1,32 @@ | ||
| """Log utilities.""" | ||
|
samdoran marked this conversation as resolved.
|
||
|
|
||
| import logging | ||
| import logging.config | ||
| import os | ||
| import sys | ||
| import typing as t | ||
| from copy import deepcopy | ||
| from datetime import datetime | ||
| 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 🧩 Analysis chain🌐 Web query:
💡 Result: No: in Pydantic v2, Citations:
Replace
🤖 Prompt for AI Agents |
||
| from rich.text import Text | ||
|
|
||
| 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, | ||
| ) | ||
|
|
||
|
|
||
| def _ms_time_format(dt: datetime) -> Text: | ||
| """Format datetime object with zero padded milliseconds.""" | ||
| return Text(dt.strftime("%Y-%m-%d %H:%M:%S.") + f"{dt.microsecond // 1000:03d}") | ||
|
|
||
|
|
||
| def resolve_log_level() -> int: | ||
| """ | ||
| Resolve and validate the log level from environment variable. | ||
|
|
@@ -50,62 +63,70 @@ 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.""" | ||
| # The need for this function should be removed in the future. | ||
| # | ||
| # 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}") | ||
|
samdoran marked this conversation as resolved.
|
||
|
|
||
|
|
||
| @lru_cache | ||
| def setup_logging() -> dict[t.Any, t.Any]: | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| """Create logging configuration.""" | ||
| 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": _ms_time_format, | ||
| "level": log_level, | ||
| }, | ||
| }, | ||
| "loggers": { | ||
| DEFAULT_LOGGER_NAME: { | ||
| "handlers": [handler], | ||
| "level": log_level, | ||
| "propagate": False, | ||
| }, | ||
| "llama_stack_client": { | ||
| "handlers": [handler], | ||
| "level": log_level, | ||
| "propagate": False, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| # Create a deep copy of uvicorn's logging config to avoid mutating global state. | ||
| merged_config = deep_update(deepcopy(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"] = ( | ||
| "%(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:%S" | ||
|
|
||
| # 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,24 +4,30 @@ | |
|
|
||
| import uvicorn | ||
|
|
||
| from log import get_logger, resolve_log_level | ||
| from log import get_logger, resolve_log_level, setup_logging | ||
| from models.config import ServiceConfiguration | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
| def start_uvicorn(configuration: ServiceConfiguration) -> None: | ||
| def start_uvicorn( | ||
| configuration: ServiceConfiguration, | ||
| log_config: dict | None = None, | ||
| ) -> None: | ||
| """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. | ||
| 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): Logging configuration. | ||
| """ | ||
|
Comment on lines
+25
to
26
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. Docstring is out of sync with signature and missing
As per coding guidelines, functions must follow “Google Python docstring conventions with required sections: Parameters, Returns, Raises, and Attributes for classes.” 🤖 Prompt for AI Agents |
||
| log_level = resolve_log_level() | ||
| logger.info("Starting Uvicorn with log level %s", logging.getLevelName(log_level)) | ||
| if log_config is None: | ||
| log_config = setup_logging() | ||
|
|
||
| # please note: | ||
| # TLS fields can be None, which means we will pass those values as None to uvicorn.run | ||
|
|
@@ -30,6 +36,7 @@ def start_uvicorn(configuration: ServiceConfiguration) -> None: | |
| host=configuration.host, | ||
| port=configuration.port, | ||
| workers=configuration.workers, | ||
| log_config=log_config, | ||
| log_level=log_level, | ||
| ssl_keyfile=configuration.tls_config.tls_key_path, | ||
| ssl_certfile=configuration.tls_config.tls_certificate_path, | ||
|
|
||
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.
setup_logging()runs too early and can neutralize--verbose.Calling
setup_logging()at import time sets handler levels before CLI args are parsed. If--verboseis passed later, logger levels change to DEBUG but handlers may still filter at INFO. Move setup until after argument parsing (or clear cache and rerun after settingLIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR).🤖 Prompt for AI Agents