From 832a55bedf4da86ce04540fec949184ecc5006c5 Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Fri, 13 Feb 2026 09:20:07 -0600 Subject: [PATCH 1/4] feat(logging): add configurable log level via LIGHTSPEED_STACK_LOG_LEVEL env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces runtime-configurable logging via the LIGHTSPEED_STACK_LOG_LEVEL environment variable, allowing deployment-time control of log verbosity without code changes. Key changes: - Added LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR and DEFAULT_LOG_LEVEL constants - Modified get_logger() to read log level from environment with defensive validation - Updated lightspeed_stack.py basicConfig() to respect the environment variable - Added force=True to basicConfig() to override llama_stack_client's early logging setup - Revived --verbose CLI flag to set DEBUG level and update all existing loggers - Added comprehensive unit tests covering default, custom, case-insensitive, invalid, and all valid log levels The --verbose flag now provides a convenient CLI shortcut for enabling debug logging, while the environment variable enables fine-grained control in containerized deployments. Signed-off-by: Pavel Tišnovský Signed-off-by: Major Hayden --- src/constants.py | 6 ++++++ src/lightspeed_stack.py | 22 +++++++++++++++++++- src/log.py | 30 +++++++++++++++++++++++---- tests/unit/test_log.py | 45 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/constants.py b/src/constants.py index 628aeb45c..1fe7ea4e7 100644 --- a/src/constants.py +++ b/src/constants.py @@ -168,3 +168,9 @@ # quota limiters constants USER_QUOTA_LIMITER = "user_limiter" CLUSTER_QUOTA_LIMITER = "cluster_limiter" + +# Logging configuration constants +# Environment variable name for configurable log level +LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR = "LIGHTSPEED_STACK_LOG_LEVEL" +# Default log level when environment variable is not set +DEFAULT_LOG_LEVEL = "INFO" diff --git a/src/lightspeed_stack.py b/src/lightspeed_stack.py index 8df663c09..374d79315 100644 --- a/src/lightspeed_stack.py +++ b/src/lightspeed_stack.py @@ -6,6 +6,7 @@ import logging import os +import sys from argparse import ArgumentParser @@ -16,10 +17,20 @@ from runners.uvicorn import start_uvicorn from runners.quota_scheduler import start_quota_scheduler from utils import schema_dumper +from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL FORMAT = "%(message)s" +# Read log level from environment variable with validation +log_level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL) +log_level = getattr(logging, log_level_str.upper(), None) +if not isinstance(log_level, int): + print( + f"WARNING: Invalid log level '{log_level_str}', falling back to {DEFAULT_LOG_LEVEL}", + file=sys.stderr, + ) + log_level = getattr(logging, DEFAULT_LOG_LEVEL) logging.basicConfig( - level="INFO", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] + level=log_level, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()], force=True ) logger = get_logger(__name__) @@ -83,6 +94,7 @@ def main() -> None: Start the Lightspeed Core Stack service process based on CLI flags and configuration. Parses command-line arguments, loads the configured settings, and then: + - If --verbose is provided, sets all loggers to DEBUG level. - If --dump-configuration is provided, writes the active configuration to configuration.json and exits (exits with status 1 on failure). - If --dump-schema is provided, writes the active configuration schema to @@ -101,6 +113,14 @@ def main() -> None: parser = create_argument_parser() args = parser.parse_args() + if args.verbose: + os.environ[LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR] = "DEBUG" + logging.getLogger().setLevel(logging.DEBUG) + for logger_name in logging.Logger.manager.loggerDict: + existing_logger = logging.getLogger(logger_name) + if isinstance(existing_logger, logging.Logger): + existing_logger.setLevel(logging.DEBUG) + configuration.load_configuration(args.config_file) logger.info("Configuration: %s", configuration.configuration) logger.info( diff --git a/src/log.py b/src/log.py index cf2b68780..7ed4cefda 100644 --- a/src/log.py +++ b/src/log.py @@ -1,16 +1,20 @@ """Log utilities.""" import logging +import os from rich.logging import RichHandler +from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL + def get_logger(name: str) -> logging.Logger: """ Get a logger configured for Rich console output. - The returned logger has its level set to DEBUG, its handlers replaced with - a single RichHandler for rich-formatted console output, and propagation to - ancestor loggers disabled. + 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 + RichHandler for rich-formatted console output, and propagation to ancestor + loggers disabled. Parameters: name (str): Name of the logger to retrieve or create. @@ -19,7 +23,25 @@ def get_logger(name: str) -> logging.Logger: logging.Logger: The configured logger instance. """ logger = logging.getLogger(name) - logger.setLevel(logging.DEBUG) + + # Skip reconfiguration if logger already has a RichHandler from a prior call + if any(isinstance(h, RichHandler) for h in logger.handlers): + return logger + + # Read log level from environment variable with default fallback + level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL) + + # Validate the level string and convert to logging level constant + validated_level = getattr(logging, level_str.upper(), None) + if not isinstance(validated_level, int): + logger.warning( + "Invalid log level '%s', falling back to %s", + level_str, + DEFAULT_LOG_LEVEL, + ) + validated_level = getattr(logging, DEFAULT_LOG_LEVEL) + + logger.setLevel(validated_level) logger.handlers = [RichHandler()] logger.propagate = False return logger diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py index 5be9dcb84..cb31d384f 100644 --- a/tests/unit/test_log.py +++ b/tests/unit/test_log.py @@ -1,6 +1,10 @@ """Unit tests for functions defined in src/log.py.""" +import logging +import pytest + from log import get_logger +from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR def test_get_logger() -> None: @@ -12,3 +16,44 @@ def test_get_logger() -> None: # at least one handler need to be set assert len(logger.handlers) >= 1 + + +def test_get_logger_invalid_env_var_fallback(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that invalid env var value falls back to INFO level.""" + monkeypatch.setenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, "FOOBAR") + + logger = get_logger("test_invalid") + assert logger.level == logging.INFO + + +@pytest.mark.parametrize( + "level_name,expected_level", + [ + ("DEBUG", logging.DEBUG), + ("debug", logging.DEBUG), + ("INFO", logging.INFO), + ("info", logging.INFO), + ("WARNING", logging.WARNING), + ("warning", logging.WARNING), + ("ERROR", logging.ERROR), + ("error", logging.ERROR), + ("CRITICAL", logging.CRITICAL), + ("critical", logging.CRITICAL), + ], +) +def test_get_logger_log_level( + monkeypatch: pytest.MonkeyPatch, level_name: str, expected_level: int +) -> None: + """Test that all valid log levels work correctly, case-insensitively.""" + monkeypatch.setenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, level_name) + + logger = get_logger(f"test_{level_name}") + assert logger.level == expected_level + + +def test_get_logger_default_log_level(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that get_logger() uses INFO level by default when env var is not set.""" + monkeypatch.delenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, raising=False) + + logger = get_logger("test_default") + assert logger.level == logging.INFO From 199cc67f9fe707179636de2adcca726e51ac1124 Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Fri, 13 Feb 2026 09:34:34 -0600 Subject: [PATCH 2/4] refactor(logging): unify all modules to use get_logger() with __name__ All 43+ modules now use the centralized get_logger() function from log.py, ensuring consistent log level configuration via the LIGHTSPEED_STACK_LOG_LEVEL environment variable. Non-standard logger names have been standardized to __name__ for clarity and consistency across the codebase. Signed-off-by: Major Hayden --- src/a2a_storage/in_memory_context_store.py | 4 ++-- src/a2a_storage/postgres_context_store.py | 4 ++-- src/a2a_storage/sqlite_context_store.py | 4 ++-- src/a2a_storage/storage_factory.py | 4 ++-- src/app/database.py | 4 +++- src/app/endpoints/a2a.py | 4 ++-- src/app/endpoints/authorized.py | 4 ++-- src/app/endpoints/config.py | 4 ++-- src/app/endpoints/conversations_v1.py | 4 ++-- src/app/endpoints/conversations_v2.py | 4 ++-- src/app/endpoints/feedback.py | 4 ++-- src/app/endpoints/health.py | 4 ++-- src/app/endpoints/info.py | 4 ++-- src/app/endpoints/mcp_auth.py | 4 ++-- src/app/endpoints/models.py | 4 ++-- src/app/endpoints/providers.py | 4 ++-- src/app/endpoints/query.py | 4 ++-- src/app/endpoints/rags.py | 4 ++-- src/app/endpoints/rlsapi_v1.py | 4 ++-- src/app/endpoints/root.py | 4 ++-- src/app/endpoints/shields.py | 4 ++-- src/app/endpoints/streaming_query.py | 4 ++-- src/app/endpoints/tools.py | 4 ++-- src/app/main.py | 1 - src/authentication/__init__.py | 4 ++-- src/authentication/jwk_token.py | 4 ++-- src/authentication/k8s.py | 4 ++-- src/authentication/rh_identity.py | 4 ++-- src/authorization/azure_token_manager.py | 4 ++-- src/authorization/middleware.py | 4 ++-- src/authorization/resolvers.py | 4 ++-- src/cache/cache_factory.py | 2 +- src/cache/in_memory_cache.py | 2 +- src/cache/noop_cache.py | 2 +- src/cache/postgres_cache.py | 2 +- src/cache/sqlite_cache.py | 2 +- src/client.py | 4 ++-- src/configuration.py | 4 ++-- src/lightspeed_stack.py | 2 +- src/llama_stack_configuration.py | 4 ++-- src/models/config.py | 4 ++-- src/observability/splunk.py | 4 ++-- src/utils/llama_stack_version.py | 5 ++--- src/utils/mcp_auth_headers.py | 4 ++-- src/utils/mcp_headers.py | 4 ++-- src/utils/query.py | 4 ++-- src/utils/responses.py | 4 ++-- src/utils/shields.py | 4 ++-- src/utils/token_counter.py | 4 ++-- src/utils/tool_formatter.py | 4 ++-- src/utils/transcripts.py | 4 ++-- .../unit/authorization/test_azure_token_manager.py | 14 ++++++++++---- 52 files changed, 105 insertions(+), 99 deletions(-) diff --git a/src/a2a_storage/in_memory_context_store.py b/src/a2a_storage/in_memory_context_store.py index a505a2912..0699ccd03 100644 --- a/src/a2a_storage/in_memory_context_store.py +++ b/src/a2a_storage/in_memory_context_store.py @@ -1,12 +1,12 @@ """In-memory implementation of A2A context store.""" import asyncio -import logging from typing import Optional from a2a_storage.context_store import A2AContextStore +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class InMemoryA2AContextStore(A2AContextStore): diff --git a/src/a2a_storage/postgres_context_store.py b/src/a2a_storage/postgres_context_store.py index 7c537cf46..5057cc958 100644 --- a/src/a2a_storage/postgres_context_store.py +++ b/src/a2a_storage/postgres_context_store.py @@ -1,6 +1,5 @@ """PostgreSQL implementation of A2A context store.""" -import logging from typing import Optional from sqlalchemy import Column, String, Table, MetaData, select, delete @@ -8,8 +7,9 @@ from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker from a2a_storage.context_store import A2AContextStore +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Define the table metadata metadata = MetaData() diff --git a/src/a2a_storage/sqlite_context_store.py b/src/a2a_storage/sqlite_context_store.py index 229f31e94..d34a801d9 100644 --- a/src/a2a_storage/sqlite_context_store.py +++ b/src/a2a_storage/sqlite_context_store.py @@ -1,14 +1,14 @@ """SQLite implementation of A2A context store.""" -import logging from typing import Optional from sqlalchemy import Column, String, Table, MetaData, select, delete from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker from a2a_storage.context_store import A2AContextStore +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Define the table metadata metadata = MetaData() diff --git a/src/a2a_storage/storage_factory.py b/src/a2a_storage/storage_factory.py index b7596b2f3..d2f5aae67 100644 --- a/src/a2a_storage/storage_factory.py +++ b/src/a2a_storage/storage_factory.py @@ -1,6 +1,5 @@ """Factory for creating A2A storage backends.""" -import logging from urllib.parse import quote_plus from typing import Optional @@ -13,8 +12,9 @@ from a2a_storage.sqlite_context_store import SQLiteA2AContextStore from a2a_storage.postgres_context_store import PostgresA2AContextStore from models.config import A2AStateConfiguration +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class A2AStorageFactory: diff --git a/src/app/database.py b/src/app/database.py index 745212dbf..edf5072e6 100644 --- a/src/app/database.py +++ b/src/app/database.py @@ -1,12 +1,14 @@ """Database engine management.""" +import logging from pathlib import Path from typing import Any, Optional from sqlalchemy import create_engine, text from sqlalchemy.engine.base import Engine from sqlalchemy.orm import sessionmaker, Session -from log import get_logger, logging + +from log import get_logger from configuration import configuration from models.database.base import Base from models.config import SQLiteDatabaseConfiguration, PostgreSQLDatabaseConfiguration diff --git a/src/app/endpoints/a2a.py b/src/app/endpoints/a2a.py index ac7557357..e45d0b9d9 100644 --- a/src/app/endpoints/a2a.py +++ b/src/app/endpoints/a2a.py @@ -2,7 +2,6 @@ import asyncio import json -import logging import uuid from datetime import datetime, timezone from typing import Annotated, Any, AsyncIterator, MutableMapping, Optional @@ -52,8 +51,9 @@ ) from utils.suid import normalize_conversation_id from version import __version__ +from log import get_logger -logger = logging.getLogger("app.endpoints.handlers") +logger = get_logger(__name__) router = APIRouter(tags=["a2a"]) auth_dependency = get_auth_dependency() diff --git a/src/app/endpoints/authorized.py b/src/app/endpoints/authorized.py index bce5be9bd..0b7ebc2bd 100644 --- a/src/app/endpoints/authorized.py +++ b/src/app/endpoints/authorized.py @@ -1,6 +1,5 @@ """Handler for REST API call to authorized endpoint.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, Depends @@ -8,8 +7,9 @@ from authentication import get_auth_dependency from authentication.interface import AuthTuple from models.responses import AuthorizedResponse, ForbiddenResponse, UnauthorizedResponse +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["authorized"]) authorized_responses: dict[int | str, dict[str, Any]] = { diff --git a/src/app/endpoints/config.py b/src/app/endpoints/config.py index 46b668fba..0158432d1 100644 --- a/src/app/endpoints/config.py +++ b/src/app/endpoints/config.py @@ -1,6 +1,5 @@ """Handler for REST API call to retrieve service configuration.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, Depends, Request @@ -17,8 +16,9 @@ UnauthorizedResponse, ) from utils.endpoints import check_configuration_loaded +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["config"]) diff --git a/src/app/endpoints/conversations_v1.py b/src/app/endpoints/conversations_v1.py index 7d2a40130..81ce2f0c3 100644 --- a/src/app/endpoints/conversations_v1.py +++ b/src/app/endpoints/conversations_v1.py @@ -1,6 +1,5 @@ """Handler for REST API calls to manage conversation history using Conversations API.""" -import logging from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request @@ -47,8 +46,9 @@ to_llama_stack_conversation_id, ) from utils.conversations import build_conversation_turns_from_items +from log import get_logger -logger = logging.getLogger("app.endpoints.handlers") +logger = get_logger(__name__) router = APIRouter(tags=["conversations_v1"]) conversation_get_responses: dict[int | str, dict[str, Any]] = { diff --git a/src/app/endpoints/conversations_v2.py b/src/app/endpoints/conversations_v2.py index f9e6ebbc8..35fa5b16d 100644 --- a/src/app/endpoints/conversations_v2.py +++ b/src/app/endpoints/conversations_v2.py @@ -1,6 +1,5 @@ """Handler for REST API calls to manage conversation history.""" -import logging from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request @@ -26,8 +25,9 @@ ) from utils.endpoints import check_configuration_loaded from utils.suid import check_suid +from log import get_logger -logger = logging.getLogger("app.endpoints.handlers") +logger = get_logger(__name__) router = APIRouter(tags=["conversations_v2"]) diff --git a/src/app/endpoints/feedback.py b/src/app/endpoints/feedback.py index 34488ae51..ac012680f 100644 --- a/src/app/endpoints/feedback.py +++ b/src/app/endpoints/feedback.py @@ -1,7 +1,6 @@ """Handler for REST API endpoint for user feedback.""" import json -import logging import threading from datetime import UTC, datetime from pathlib import Path @@ -26,8 +25,9 @@ ) from utils.endpoints import check_configuration_loaded, retrieve_conversation from utils.suid import get_suid +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(prefix="/feedback", tags=["feedback"]) feedback_status_lock = threading.Lock() diff --git a/src/app/endpoints/health.py b/src/app/endpoints/health.py index 61ad196da..7a8058d79 100644 --- a/src/app/endpoints/health.py +++ b/src/app/endpoints/health.py @@ -5,7 +5,6 @@ methods. For HEAD HTTP method, just the HTTP response code is used. """ -import logging from enum import Enum from typing import Annotated, Any @@ -16,6 +15,7 @@ from authentication.interface import AuthTuple from authorization.middleware import authorize from client import AsyncLlamaStackClientHolder +from log import get_logger from models.config import Action from models.responses import ( ForbiddenResponse, @@ -26,7 +26,7 @@ UnauthorizedResponse, ) -logger = logging.getLogger("app.endpoints.handlers") +logger = get_logger(__name__) router = APIRouter(tags=["health"]) diff --git a/src/app/endpoints/info.py b/src/app/endpoints/info.py index e28000532..d0e6d3d44 100644 --- a/src/app/endpoints/info.py +++ b/src/app/endpoints/info.py @@ -1,6 +1,5 @@ """Handler for REST API call to provide info.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException, Request @@ -19,8 +18,9 @@ UnauthorizedResponse, ) from version import __version__ +from log import get_logger -logger = logging.getLogger("app.endpoints.handlers") +logger = get_logger(__name__) router = APIRouter(tags=["info"]) diff --git a/src/app/endpoints/mcp_auth.py b/src/app/endpoints/mcp_auth.py index 28d64af0a..b40e48a9b 100644 --- a/src/app/endpoints/mcp_auth.py +++ b/src/app/endpoints/mcp_auth.py @@ -1,6 +1,5 @@ """Handler for REST API calls related to MCP server authentication.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, Depends, Request @@ -19,8 +18,9 @@ UnauthorizedResponse, ) from utils.endpoints import check_configuration_loaded +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(prefix="/mcp-auth", tags=["mcp-auth"]) diff --git a/src/app/endpoints/models.py b/src/app/endpoints/models.py index bfd52270a..cec79fd6c 100644 --- a/src/app/endpoints/models.py +++ b/src/app/endpoints/models.py @@ -1,6 +1,5 @@ """Handler for REST API call to list available models.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, HTTPException, Request, Query @@ -22,8 +21,9 @@ UnauthorizedResponse, ) from utils.endpoints import check_configuration_loaded +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["models"]) diff --git a/src/app/endpoints/providers.py b/src/app/endpoints/providers.py index ae2ec9b4e..9e927c13a 100644 --- a/src/app/endpoints/providers.py +++ b/src/app/endpoints/providers.py @@ -1,6 +1,5 @@ """Handler for REST API calls to list and retrieve available providers.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, HTTPException, Request @@ -24,8 +23,9 @@ UnauthorizedResponse, ) from utils.endpoints import check_configuration_loaded +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["providers"]) diff --git a/src/app/endpoints/query.py b/src/app/endpoints/query.py index ae8a90715..04804b245 100644 --- a/src/app/endpoints/query.py +++ b/src/app/endpoints/query.py @@ -2,7 +2,6 @@ """Handler for REST API call to provide answer to query using Response API.""" -import logging from datetime import UTC, datetime from typing import Annotated, Any, cast @@ -64,8 +63,9 @@ ) from utils.suid import normalize_conversation_id from utils.types import ResponsesApiParams, TurnSummary +from log import get_logger -logger = logging.getLogger("app.endpoints.handlers") +logger = get_logger(__name__) router = APIRouter(tags=["query"]) query_response: dict[int | str, dict[str, Any]] = { diff --git a/src/app/endpoints/rags.py b/src/app/endpoints/rags.py index acedf6911..19475263e 100644 --- a/src/app/endpoints/rags.py +++ b/src/app/endpoints/rags.py @@ -1,6 +1,5 @@ """Handler for REST API calls to list and retrieve available RAGs.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, HTTPException, Request @@ -23,8 +22,9 @@ UnauthorizedResponse, ) from utils.endpoints import check_configuration_loaded +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["rags"]) diff --git a/src/app/endpoints/rlsapi_v1.py b/src/app/endpoints/rlsapi_v1.py index 6336aa4a3..903ee1ac1 100644 --- a/src/app/endpoints/rlsapi_v1.py +++ b/src/app/endpoints/rlsapi_v1.py @@ -4,7 +4,6 @@ from the RHEL Lightspeed Command Line Assistant (CLA). """ -import logging import time from typing import Annotated, Any, cast @@ -34,8 +33,9 @@ from observability import InferenceEventData, build_inference_event, send_splunk_event from utils.responses import extract_text_from_response_output_item, get_mcp_tools from utils.suid import get_suid +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["rlsapi-v1"]) # Default values when RH Identity auth is not configured diff --git a/src/app/endpoints/root.py b/src/app/endpoints/root.py index 31996d7e4..77e9b1881 100644 --- a/src/app/endpoints/root.py +++ b/src/app/endpoints/root.py @@ -1,6 +1,5 @@ """Handler for the / endpoint.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, Depends, Request @@ -11,8 +10,9 @@ from authorization.middleware import authorize from models.config import Action from models.responses import ForbiddenResponse, UnauthorizedResponse +from log import get_logger -logger = logging.getLogger("app.endpoints.handlers") +logger = get_logger(__name__) router = APIRouter(tags=["root"]) diff --git a/src/app/endpoints/shields.py b/src/app/endpoints/shields.py index 5dd8b8b6c..acf1d4397 100644 --- a/src/app/endpoints/shields.py +++ b/src/app/endpoints/shields.py @@ -1,6 +1,5 @@ """Handler for REST API call to list available shields.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, HTTPException, Request @@ -21,8 +20,9 @@ UnauthorizedResponse, ) from utils.endpoints import check_configuration_loaded +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["shields"]) diff --git a/src/app/endpoints/streaming_query.py b/src/app/endpoints/streaming_query.py index 047278b1f..81ab092e2 100644 --- a/src/app/endpoints/streaming_query.py +++ b/src/app/endpoints/streaming_query.py @@ -1,7 +1,6 @@ """Streaming query handler using Responses API.""" import json -import logging from datetime import UTC, datetime from typing import Annotated, Any, AsyncIterator, Optional, cast @@ -85,8 +84,9 @@ from utils.suid import normalize_conversation_id from utils.token_counter import TokenCounter from utils.types import ResponsesApiParams, TurnSummary +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["streaming_query"]) streaming_query_responses: dict[int | str, dict[str, Any]] = { diff --git a/src/app/endpoints/tools.py b/src/app/endpoints/tools.py index 074a30d85..fdb18ef43 100644 --- a/src/app/endpoints/tools.py +++ b/src/app/endpoints/tools.py @@ -1,6 +1,5 @@ """Handler for REST API call to list available tools from MCP servers.""" -import logging from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException, Request @@ -21,8 +20,9 @@ ) from utils.endpoints import check_configuration_loaded from utils.tool_formatter import format_tools_list +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) router = APIRouter(tags=["tools"]) diff --git a/src/app/main.py b/src/app/main.py index e4ee83905..94daa1455 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -71,7 +71,6 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]: logger.info("Registering MCP servers") await register_mcp_servers_async(logger, configuration.configuration) - get_logger("app.endpoints.handlers") logger.info("App startup complete") initialize_database() diff --git a/src/authentication/__init__.py b/src/authentication/__init__.py index 0fc6bb8ba..c6d96cdfa 100644 --- a/src/authentication/__init__.py +++ b/src/authentication/__init__.py @@ -1,6 +1,5 @@ """This package contains authentication code and modules.""" -import logging import os import constants @@ -14,8 +13,9 @@ ) from authentication.interface import AuthInterface from configuration import LogicError, configuration +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def get_auth_dependency( diff --git a/src/authentication/jwk_token.py b/src/authentication/jwk_token.py index bed712c7f..90343e2b6 100644 --- a/src/authentication/jwk_token.py +++ b/src/authentication/jwk_token.py @@ -1,7 +1,6 @@ """Manage authentication flow for FastAPI endpoints with JWK based JWT auth.""" import json -import logging from asyncio import Lock from typing import Any, Callable @@ -23,8 +22,9 @@ ) from models.config import JwkConfiguration from models.responses import UnauthorizedResponse +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Global JWK registry to avoid re-fetching JWKs for each request. Cached for 1 # hour, keys are unlikely to change frequently. diff --git a/src/authentication/k8s.py b/src/authentication/k8s.py index 311f17b94..399a3d40c 100644 --- a/src/authentication/k8s.py +++ b/src/authentication/k8s.py @@ -1,6 +1,5 @@ """Manage authentication flow for FastAPI endpoints with K8S/OCP.""" -import logging import os from pathlib import Path from typing import Optional, Self @@ -14,6 +13,7 @@ from authentication.utils import extract_user_token from configuration import configuration from constants import DEFAULT_VIRTUAL_PATH +from log import get_logger from models.responses import ( ForbiddenResponse, InternalServerErrorResponse, @@ -21,7 +21,7 @@ UnauthorizedResponse, ) -logger = logging.getLogger(__name__) +logger = get_logger(__name__) CLUSTER_ID_LOCAL = "local" diff --git a/src/authentication/rh_identity.py b/src/authentication/rh_identity.py index fdbbdf7e9..7165a90fc 100644 --- a/src/authentication/rh_identity.py +++ b/src/authentication/rh_identity.py @@ -6,7 +6,6 @@ import base64 import json -import logging from typing import Optional from fastapi import HTTPException, Request @@ -14,8 +13,9 @@ from authentication.interface import NO_AUTH_TUPLE, AuthInterface, AuthTuple from configuration import configuration from constants import DEFAULT_VIRTUAL_PATH, NO_USER_TOKEN +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class RHIdentityData: diff --git a/src/authorization/azure_token_manager.py b/src/authorization/azure_token_manager.py index 69b3eca53..a92b8a4f8 100644 --- a/src/authorization/azure_token_manager.py +++ b/src/authorization/azure_token_manager.py @@ -1,6 +1,5 @@ """Azure Entra ID token manager for Azure OpenAI authentication.""" -import logging import os import time from typing import Optional @@ -12,8 +11,9 @@ from configuration import AzureEntraIdConfiguration from utils.types import Singleton +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Refresh token before actual expiration to avoid edge cases TOKEN_EXPIRATION_LEEWAY = 30 # seconds diff --git a/src/authorization/middleware.py b/src/authorization/middleware.py index 9e41d9eee..333108c4e 100644 --- a/src/authorization/middleware.py +++ b/src/authorization/middleware.py @@ -1,6 +1,5 @@ """Authorization middleware and decorators.""" -import logging from functools import lru_cache, wraps from typing import Any, Callable, Optional, Tuple @@ -17,13 +16,14 @@ RolesResolver, ) from configuration import configuration +from log import get_logger from models.config import Action from models.responses import ( ForbiddenResponse, InternalServerErrorResponse, ) -logger = logging.getLogger(__name__) +logger = get_logger(__name__) @lru_cache(maxsize=1) diff --git a/src/authorization/resolvers.py b/src/authorization/resolvers.py index df2575651..359064d4b 100644 --- a/src/authorization/resolvers.py +++ b/src/authorization/resolvers.py @@ -1,7 +1,6 @@ """Authorization resolvers for role evaluation and access control.""" from abc import ABC, abstractmethod -import logging import base64 import json from typing import Any @@ -11,8 +10,9 @@ from authentication.interface import AuthTuple from models.config import JwtRoleRule, AccessRule, JsonPathOperator, Action import constants +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) UserRoles = set[str] diff --git a/src/cache/cache_factory.py b/src/cache/cache_factory.py index f83eb83cc..2c890a96a 100644 --- a/src/cache/cache_factory.py +++ b/src/cache/cache_factory.py @@ -9,7 +9,7 @@ from cache.sqlite_cache import SQLiteCache from log import get_logger -logger = get_logger("cache.cache_factory") +logger = get_logger(__name__) # pylint: disable=R0903 diff --git a/src/cache/in_memory_cache.py b/src/cache/in_memory_cache.py index 97274424b..2461cb4ff 100644 --- a/src/cache/in_memory_cache.py +++ b/src/cache/in_memory_cache.py @@ -7,7 +7,7 @@ from log import get_logger from utils.connection_decorator import connection -logger = get_logger("cache.in_memory_cache") +logger = get_logger(__name__) class InMemoryCache(Cache): diff --git a/src/cache/noop_cache.py b/src/cache/noop_cache.py index 3b1ba9773..f0fa757ff 100644 --- a/src/cache/noop_cache.py +++ b/src/cache/noop_cache.py @@ -6,7 +6,7 @@ from log import get_logger from utils.connection_decorator import connection -logger = get_logger("cache.noop_cache") +logger = get_logger(__name__) class NoopCache(Cache): diff --git a/src/cache/postgres_cache.py b/src/cache/postgres_cache.py index 45ff3dce3..3b5dfbefe 100644 --- a/src/cache/postgres_cache.py +++ b/src/cache/postgres_cache.py @@ -13,7 +13,7 @@ from utils.types import ReferencedDocument, ToolCallSummary, ToolResultSummary from log import get_logger -logger = get_logger("cache.postgres_cache") +logger = get_logger(__name__) class PostgresCache(Cache): diff --git a/src/cache/sqlite_cache.py b/src/cache/sqlite_cache.py index 5cf765229..777b00e92 100644 --- a/src/cache/sqlite_cache.py +++ b/src/cache/sqlite_cache.py @@ -14,7 +14,7 @@ from utils.types import ReferencedDocument, ToolCallSummary, ToolResultSummary from log import get_logger -logger = get_logger("cache.sqlite_cache") +logger = get_logger(__name__) class SQLiteCache(Cache): diff --git a/src/client.py b/src/client.py index 94bf0349e..d7ac27406 100644 --- a/src/client.py +++ b/src/client.py @@ -1,7 +1,6 @@ """Llama Stack client retrieval class.""" import json -import logging import os import tempfile from typing import Optional @@ -16,8 +15,9 @@ from models.config import LlamaStackConfiguration from models.responses import ServiceUnavailableResponse from utils.types import Singleton +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class AsyncLlamaStackClientHolder(metaclass=Singleton): diff --git a/src/configuration.py b/src/configuration.py index 9a253ac7a..2690a5ecb 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -1,6 +1,5 @@ """Configuration loader.""" -import logging from typing import Any, Optional # We want to support environment variable replacement in the configuration @@ -32,8 +31,9 @@ from quota.quota_limiter import QuotaLimiter from quota.token_usage_history import TokenUsageHistory from quota.quota_limiter_factory import QuotaLimiterFactory +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class LogicError(Exception): diff --git a/src/lightspeed_stack.py b/src/lightspeed_stack.py index 374d79315..297a91caf 100644 --- a/src/lightspeed_stack.py +++ b/src/lightspeed_stack.py @@ -94,7 +94,7 @@ def main() -> None: Start the Lightspeed Core Stack service process based on CLI flags and configuration. Parses command-line arguments, loads the configured settings, and then: - - If --verbose is provided, sets all loggers to DEBUG level. + - If --verbose is provided, sets application loggers to DEBUG level. - If --dump-configuration is provided, writes the active configuration to configuration.json and exits (exits with status 1 on failure). - If --dump-schema is provided, writes the active configuration schema to diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index cf9c9269b..68f61e7a6 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -5,7 +5,6 @@ 2. As a module: `from llama_stack_configuration import generate_configuration` """ -import logging import os from argparse import ArgumentParser from pathlib import Path @@ -15,8 +14,9 @@ from azure.core.exceptions import ClientAuthenticationError from azure.identity import ClientSecretCredential, CredentialUnavailableError from llama_stack.core.stack import replace_env_vars +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class YamlDumper(yaml.Dumper): # pylint: disable=too-many-ancestors diff --git a/src/models/config.py b/src/models/config.py index 68ad168e4..7bb29446c 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -2,7 +2,6 @@ # pylint: disable=too-many-lines -import logging from pathlib import Path from typing import Optional, Any, Pattern from enum import Enum @@ -33,8 +32,9 @@ from utils import checks from utils.mcp_auth_headers import resolve_authorization_headers +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class ConfigurationBase(BaseModel): diff --git a/src/observability/splunk.py b/src/observability/splunk.py index ca663127f..6be46c642 100644 --- a/src/observability/splunk.py +++ b/src/observability/splunk.py @@ -1,6 +1,5 @@ """Async Splunk HEC client for sending telemetry events.""" -import logging import platform import time from typing import Any @@ -9,8 +8,9 @@ from configuration import configuration from version import __version__ +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def _get_hostname() -> str: diff --git a/src/utils/llama_stack_version.py b/src/utils/llama_stack_version.py index 7ac543bfc..4c004f0bd 100644 --- a/src/utils/llama_stack_version.py +++ b/src/utils/llama_stack_version.py @@ -1,19 +1,18 @@ """Check if the Llama Stack version is supported by the LCS.""" -import logging import re from semver import Version from llama_stack_client._client import AsyncLlamaStackClient - from constants import ( MINIMAL_SUPPORTED_LLAMA_STACK_VERSION, MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION, ) +from log import get_logger -logger = logging.getLogger("utils.llama_stack_version") +logger = get_logger(__name__) class InvalidLlamaStackVersionException(Exception): diff --git a/src/utils/mcp_auth_headers.py b/src/utils/mcp_auth_headers.py index 5236df703..d7760c3e0 100644 --- a/src/utils/mcp_auth_headers.py +++ b/src/utils/mcp_auth_headers.py @@ -1,11 +1,11 @@ """Utilities for resolving MCP server authorization headers.""" -import logging from pathlib import Path import constants +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def resolve_authorization_headers( diff --git a/src/utils/mcp_headers.py b/src/utils/mcp_headers.py index af6623ec5..c19dc4502 100644 --- a/src/utils/mcp_headers.py +++ b/src/utils/mcp_headers.py @@ -1,14 +1,14 @@ """MCP headers handling.""" import json -import logging from urllib.parse import urlparse from fastapi import Request from configuration import AppConfig +from log import get_logger -logger = logging.getLogger("app.endpoints.dependencies") +logger = get_logger(__name__) type McpHeaders = dict[str, dict[str, str]] diff --git a/src/utils/query.py b/src/utils/query.py index 39e39a152..740575a0f 100644 --- a/src/utils/query.py +++ b/src/utils/query.py @@ -1,6 +1,5 @@ """Utility functions for working with queries.""" -import logging from datetime import UTC, datetime from typing import Optional @@ -42,8 +41,9 @@ from utils.suid import normalize_conversation_id from utils.token_counter import TokenCounter from utils.types import TurnSummary +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def store_conversation_into_cache( diff --git a/src/utils/responses.py b/src/utils/responses.py index 4055eea6d..11337b348 100644 --- a/src/utils/responses.py +++ b/src/utils/responses.py @@ -1,7 +1,6 @@ """Utility functions for processing Responses API output.""" import json -import logging from typing import Any, Optional, cast from fastapi import HTTPException @@ -47,8 +46,9 @@ ToolCallSummary, ToolResultSummary, ) +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def extract_text_from_response_output_item(output_item: Any) -> str: diff --git a/src/utils/shields.py b/src/utils/shields.py index ecfa80f5c..7d5b7a4ad 100644 --- a/src/utils/shields.py +++ b/src/utils/shields.py @@ -1,6 +1,5 @@ """Utility functions for working with Llama Stack shields.""" -import logging from typing import Any, cast from fastapi import HTTPException @@ -12,8 +11,9 @@ NotFoundResponse, ) from utils.types import ShieldModerationResult +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) DEFAULT_VIOLATION_MESSAGE = "I cannot process this request due to policy restrictions." diff --git a/src/utils/token_counter.py b/src/utils/token_counter.py index 8c7b86c21..cbdfaee7c 100644 --- a/src/utils/token_counter.py +++ b/src/utils/token_counter.py @@ -1,9 +1,9 @@ """Helper classes to count tokens sent and received by the LLM.""" -import logging from dataclasses import dataclass +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) @dataclass diff --git a/src/utils/tool_formatter.py b/src/utils/tool_formatter.py index 8fd22d336..8b200ba1d 100644 --- a/src/utils/tool_formatter.py +++ b/src/utils/tool_formatter.py @@ -1,9 +1,9 @@ """Utility functions for formatting and parsing MCP tool descriptions.""" -import logging from typing import Any +from log import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def format_tool_response(tool_dict: dict[str, Any]) -> dict[str, Any]: diff --git a/src/utils/transcripts.py b/src/utils/transcripts.py index 738212676..aba969512 100644 --- a/src/utils/transcripts.py +++ b/src/utils/transcripts.py @@ -6,7 +6,6 @@ from datetime import UTC, datetime import json -import logging import os from pathlib import Path import hashlib @@ -16,8 +15,9 @@ from models.requests import Attachment, QueryRequest from utils.suid import get_suid from utils.types import TurnSummary +from log import get_logger -logger = logging.getLogger("utils.transcripts") +logger = get_logger(__name__) def _hash_user_id(user_id: str) -> str: diff --git a/tests/unit/authorization/test_azure_token_manager.py b/tests/unit/authorization/test_azure_token_manager.py index 603464cdc..0f97154c0 100644 --- a/tests/unit/authorization/test_azure_token_manager.py +++ b/tests/unit/authorization/test_azure_token_manager.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access +import logging import time from typing import Any, Generator @@ -135,10 +136,15 @@ def test_refresh_token_failure_logs_error( return_value=mock_credential_instance, ) - with caplog.at_level("WARNING"): - result = token_manager.refresh_token() - assert result is False - assert "Failed to retrieve Azure access token" in caplog.text + azure_logger = logging.getLogger("authorization.azure_token_manager") + azure_logger.propagate = True + try: + with caplog.at_level("WARNING"): + result = token_manager.refresh_token() + assert result is False + assert "Failed to retrieve Azure access token" in caplog.text + finally: + azure_logger.propagate = False def test_token_expired_property_dynamic( self, token_manager: AzureEntraIDManager, mocker: MockerFixture From 3abc393396e97a3f80a284250fd7d6faacbecd53 Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Fri, 13 Feb 2026 09:59:29 -0600 Subject: [PATCH 3/4] docs: update stale logger name in a2a_protocol.md Signed-off-by: Major Hayden --- docs/a2a_protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/a2a_protocol.md b/docs/a2a_protocol.md index d7c792b9e..0df32ec18 100644 --- a/docs/a2a_protocol.md +++ b/docs/a2a_protocol.md @@ -771,7 +771,7 @@ service: color_log: true ``` -Check logs for entries from `app.endpoints.handlers` logger. +Check logs for entries from `app.endpoints.health` logger. ## Protocol Version From c5aed53a5bf78fff435304bc6fa081773f06ec3a Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Fri, 13 Feb 2026 09:59:10 -0600 Subject: [PATCH 4/4] docs: update CLAUDE.md logging pattern to use get_logger() Signed-off-by: Major Hayden --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4ff5c314f..03006cc69 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ src/ #### Module Standards - All modules start with descriptive docstrings explaining purpose -- Use `logger = logging.getLogger(__name__)` pattern for module logging +- Use `logger = get_logger(__name__)` from `log.py` for module logging - Package `__init__.py` files contain brief package descriptions - Central `constants.py` for shared constants with descriptive comments - Type aliases defined at module level for clarity @@ -89,7 +89,7 @@ src/ - Handle `APIConnectionError` from Llama Stack #### Logging Standards -- Use `import logging` and module logger pattern: `logger = logging.getLogger(__name__)` +- Use `from log import get_logger` and module logger pattern: `logger = get_logger(__name__)` - Standard log levels with clear purposes: - `logger.debug()` - Detailed diagnostic information - `logger.info()` - General information about program execution