@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Application lifespan manager."""
settings = get_settings()
# Startup
logger.info(
"server_start",
host=settings.server.host,
port=settings.server.port,
url=f"http://{settings.server.host}:{settings.server.port}",
)
logger.debug(
"server_configured", host=settings.server.host, port=settings.server.port
)
# Log Claude CLI configuration
if settings.claude.cli_path:
logger.debug("claude_cli_configured", cli_path=settings.claude.cli_path)
else:
logger.debug("claude_cli_auto_detect")
logger.debug(
"claude_cli_search_paths", paths=settings.claude.get_searched_paths()
)
# Validate authentication token at startup
try:
credentials_manager = CredentialsManager()
validation = await credentials_manager.validate()
if validation.valid and not validation.expired:
credentials = validation.credentials
oauth_token = credentials.claude_ai_oauth if credentials else None
if oauth_token and oauth_token.expires_at_datetime:
hours_until_expiry = int(
(
oauth_token.expires_at_datetime - datetime.now(UTC)
).total_seconds()
/ 3600
)
logger.info(
"auth_token_valid",
expires_in_hours=hours_until_expiry,
subscription_type=oauth_token.subscription_type,
credentials_path=str(validation.path) if validation.path else None,
)
else:
logger.info("auth_token_valid", credentials_path=str(validation.path))
elif validation.expired:
logger.warning(
"auth_token_expired",
message="Authentication token has expired. Please run 'ccproxy auth login' to refresh.",
credentials_path=str(validation.path) if validation.path else None,
)
else:
logger.warning(
"auth_token_invalid",
message="Authentication token is invalid. Please run 'ccproxy auth login'.",
credentials_path=str(validation.path) if validation.path else None,
)
except CredentialsNotFoundError:
logger.warning(
"auth_token_not_found",
message="No authentication credentials found. Please run 'ccproxy auth login' to authenticate.",
searched_paths=settings.auth.storage.storage_paths,
)
except Exception as e:
logger.error(
"auth_token_validation_error",
error=str(e),
message="Failed to validate authentication token. The server will continue without authentication.",
)
# Validate Claude binary at startup
claude_path, found_in_path = settings.claude.find_claude_cli()
if claude_path:
logger.info(
"claude_binary_found",
path=claude_path,
found_in_path=found_in_path,
message=f"Claude CLI binary found at: {claude_path}",
)
else:
searched_paths = settings.claude.get_searched_paths()
logger.warning(
"claude_binary_not_found",
message="Claude CLI binary not found. Please install Claude CLI to use SDK features.",
searched_paths=searched_paths,
install_command="npm install -g @anthropic-ai/claude-code",
)
# Start scheduler system
try:
scheduler = await start_scheduler(settings)
app.state.scheduler = scheduler
logger.debug("scheduler_initialized")
except Exception as e:
logger.error("scheduler_initialization_failed", error=str(e))
# Continue startup even if scheduler fails (graceful degradation)
# Initialize log storage if needed and backend is duckdb
if (
settings.observability.needs_storage_backend
and settings.observability.log_storage_backend == "duckdb"
):
try:
storage = SimpleDuckDBStorage(
database_path=settings.observability.duckdb_path
)
await storage.initialize()
app.state.log_storage = storage
logger.debug(
"log_storage_initialized",
backend="duckdb",
path=str(settings.observability.duckdb_path),
collection_enabled=settings.observability.logs_collection_enabled,
)
except Exception as e:
logger.error("log_storage_initialization_failed", error=str(e))
# Continue without log storage (graceful degradation)
yield
# Shutdown
logger.debug("server_stop")
# Stop scheduler system
try:
scheduler = getattr(app.state, "scheduler", None)
await stop_scheduler(scheduler)
logger.debug("scheduler_stopped")
except Exception as e:
logger.error("scheduler_stop_failed", error=str(e))
# Close log storage if initialized
if hasattr(app.state, "log_storage") and app.state.log_storage:
try:
await app.state.log_storage.close()
logger.debug("log_storage_closed")
except Exception as e:
logger.error("log_storage_close_failed", error=str(e))