Skip to content

Commit a644feb

Browse files
mattgodboltclaude
andcommitted
Fix FastAPI logging initialization using lifespan context manager
- Replace module-level resource initialization with lifespan context manager - Create configure_logging() function for proper logging setup - Move shared resources (anthropic_client, prompt, settings) to app.state - Update endpoints to access resources from app.state via Request object - Eliminate multiple get_settings() calls at module import time - Use proper FastAPI 2024 best practices for resource management - Maintain backward compatibility and all tests pass This fixes the issue where settings were locked at import time and makes testing much easier since settings can now be mocked before app creation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3c56886 commit a644feb

File tree

1 file changed

+55
-22
lines changed

1 file changed

+55
-22
lines changed

app/main.py

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import logging
2+
from contextlib import asynccontextmanager
23
from pathlib import Path
34

45
from anthropic import Anthropic
56
from anthropic import __version__ as anthropic_version
6-
from fastapi import FastAPI
7+
from fastapi import FastAPI, Request
78
from fastapi.middleware.cors import CORSMiddleware
89
from mangum import Mangum
910

@@ -20,14 +21,47 @@
2021
from app.metrics import get_metrics_provider
2122
from app.prompt import Prompt
2223

23-
# Configure logging based on settings
24-
settings = get_settings()
25-
log_level = getattr(logging, settings.log_level.upper(), logging.INFO)
26-
logging.basicConfig(level=log_level)
27-
logger = logging.getLogger()
28-
logger.setLevel(log_level)
2924

30-
app = FastAPI(root_path=get_settings().root_path)
25+
def configure_logging(log_level: str) -> None:
26+
"""Configure logging with the specified level."""
27+
level = getattr(logging, log_level.upper(), logging.INFO)
28+
logging.basicConfig(
29+
level=level,
30+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
31+
force=True, # Reconfigure if already configured
32+
)
33+
34+
35+
@asynccontextmanager
36+
async def lifespan(app: FastAPI):
37+
"""Configure app on startup, cleanup on shutdown."""
38+
# Startup
39+
settings = get_settings()
40+
configure_logging(settings.log_level)
41+
logger = logging.getLogger(__name__)
42+
43+
# Store shared resources in app.state
44+
app.state.settings = settings
45+
app.state.anthropic_client = Anthropic(api_key=settings.anthropic_api_key)
46+
47+
# Load the prompt configuration
48+
prompt_config_path = Path(__file__).parent / "prompt.yaml"
49+
app.state.prompt = Prompt(prompt_config_path)
50+
51+
logger.info(f"Application started with log level: {settings.log_level}")
52+
logger.info(f"Anthropic SDK version: {anthropic_version}")
53+
logger.info(f"Loaded prompt configuration from {prompt_config_path}")
54+
55+
yield
56+
57+
# Shutdown
58+
logger.info("Application shutting down")
59+
60+
61+
# Get settings once for app-level configuration
62+
# This is acceptable since these settings don't change during runtime
63+
_app_settings = get_settings()
64+
app = FastAPI(root_path=_app_settings.root_path, lifespan=lifespan)
3165

3266
# Configure CORS - allows all origins for public API
3367
app.add_middleware(
@@ -40,18 +74,10 @@
4074
)
4175
handler = Mangum(app)
4276

43-
anthropic_client = Anthropic(api_key=get_settings().anthropic_api_key)
44-
logger.info(f"Anthropic SDK version: {anthropic_version}")
45-
46-
# Load the prompt configuration
47-
prompt_config_path = Path(__file__).parent / "prompt.yaml"
48-
prompt = Prompt(prompt_config_path)
49-
logger.info(f"Loaded prompt configuration from {prompt_config_path}")
5077

51-
52-
def get_cache_provider():
78+
def get_cache_provider(settings) -> NoOpCacheProvider | S3CacheProvider:
5379
"""Get the configured cache provider."""
54-
settings = get_settings()
80+
logger = logging.getLogger(__name__)
5581

5682
if not settings.cache_enabled:
5783
logger.info("Caching disabled by configuration")
@@ -73,8 +99,9 @@ def get_cache_provider():
7399

74100

75101
@app.get("/", response_model=AvailableOptions)
76-
async def get_options() -> AvailableOptions:
102+
async def get_options(request: Request) -> AvailableOptions:
77103
"""Get available options for the explain API."""
104+
prompt = request.app.state.prompt
78105
async with get_metrics_provider() as metrics_provider:
79106
metrics_provider.put_metric("ClaudeExplainOptionsRequest", 1)
80107
return AvailableOptions(
@@ -96,8 +123,14 @@ async def get_options() -> AvailableOptions:
96123

97124

98125
@app.post("/")
99-
async def explain(request: ExplainRequest) -> ExplainResponse:
126+
async def explain(explain_request: ExplainRequest, request: Request) -> ExplainResponse:
100127
"""Explain a Compiler Explorer compilation from its source and output assembly."""
101128
async with get_metrics_provider() as metrics_provider:
102-
cache_provider = get_cache_provider()
103-
return await process_request(request, anthropic_client, prompt, metrics_provider, cache_provider)
129+
cache_provider = get_cache_provider(request.app.state.settings)
130+
return await process_request(
131+
explain_request,
132+
request.app.state.anthropic_client,
133+
request.app.state.prompt,
134+
metrics_provider,
135+
cache_provider,
136+
)

0 commit comments

Comments
 (0)