diff --git a/libs/arcade-cli/arcade_cli/main.py b/libs/arcade-cli/arcade_cli/main.py index d57d00a1..cb6931b4 100644 --- a/libs/arcade-cli/arcade_cli/main.py +++ b/libs/arcade-cli/arcade_cli/main.py @@ -227,6 +227,9 @@ def mcp( False, "--reload", help="Enable auto-reload on code changes (HTTP mode only)" ), debug: bool = typer.Option(False, "--debug", help="Enable debug mode with verbose logging"), + otel_enable: bool = typer.Option( + False, "--otel-enable", help="Send logs to OpenTelemetry", show_default=True + ), env_file: Optional[str] = typer.Option(None, "--env-file", help="Path to environment file"), name: Optional[str] = typer.Option(None, "--name", help="Server name"), version: Optional[str] = typer.Option(None, "--version", help="Server version"), @@ -250,7 +253,10 @@ def mcp( # Add optional arguments cmd.extend(["--host", host]) cmd.extend(["--port", str(port)]) - cmd.append("--debug") + if debug: + cmd.append("--debug") + if otel_enable: + cmd.append("--otel-enable") if tool_package: cmd.extend(["--tool-package", tool_package]) if discover_installed: diff --git a/libs/arcade-mcp-server/arcade_mcp_server/__main__.py b/libs/arcade-mcp-server/arcade_mcp_server/__main__.py index c32f2f84..36b01693 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/__main__.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/__main__.py @@ -264,6 +264,11 @@ Auto-discovery looks for Python files with @tool decorated functions in: action="store_true", help="Enable debug mode with verbose logging", ) + parser.add_argument( + "--otel-enable", + action="store_true", + help="Send logs to OpenTelemetry", + ) parser.add_argument( "--env-file", help="Path to environment file", @@ -336,6 +341,7 @@ Auto-discovery looks for Python files with @tool decorated functions in: port=args.port, reload=args.reload, debug=args.debug, + otel_enable=args.otel_enable, tool_package=args.tool_package, discover_installed=args.discover_installed, show_packages=args.show_packages, diff --git a/libs/arcade-mcp-server/arcade_mcp_server/worker.py b/libs/arcade-mcp-server/arcade_mcp_server/worker.py index 1def203d..69dab6ee 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/worker.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/worker.py @@ -5,12 +5,15 @@ Creates a FastAPI application that exposes both Arcade Worker endpoints and MCP Server endpoints over HTTP/SSE. MCP is always enabled in this integrated mode. """ +import asyncio +import logging from collections.abc import AsyncGenerator, AsyncIterator from contextlib import asynccontextmanager from typing import Any import uvicorn from arcade_core.catalog import ToolCatalog +from arcade_serve.fastapi.telemetry import OTELHandler from arcade_serve.fastapi.worker import FastAPIWorker from fastapi import FastAPI from loguru import logger @@ -74,6 +77,7 @@ def create_arcade_mcp( catalog: ToolCatalog, mcp_settings: MCPSettings | None = None, debug: bool = False, + otel_enable: bool = False, **kwargs: Any, ) -> FastAPI: """ @@ -87,12 +91,28 @@ def create_arcade_mcp( if secret is None: secret = "dev" # noqa: S105 + otel_handler = OTELHandler( + enable=otel_enable, + log_level=logging.DEBUG if debug else logging.INFO, + ) + @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: - async with create_lifespan(catalog, mcp_settings, **kwargs) as components: - app.state.mcp_server = components["mcp_server"] - app.state.session_manager = components["session_manager"] - yield + try: + logger.debug(f"Server lifespan startup. OTEL enabled: {otel_enable}") + async with create_lifespan(catalog, mcp_settings, **kwargs) as components: + app.state.mcp_server = components["mcp_server"] + app.state.session_manager = components["session_manager"] + yield + except (asyncio.CancelledError, KeyboardInterrupt): + logger.debug("Server lifespan cancelled.") + raise + finally: + logger.debug(f"Server lifespan shutdown. OTEL enabled: {otel_enable}") + if otel_enable and otel_handler: + otel_handler.shutdown() + await logger.complete() + logger.debug("Server lifespan shutdown complete.") app = FastAPI( title=(mcp_settings.server.title or mcp_settings.server.name), @@ -103,12 +123,14 @@ def create_arcade_mcp( lifespan=lifespan, **kwargs, ) + otel_handler.instrument_app(app) # Worker endpoints worker = FastAPIWorker( app=app, secret=secret, disable_auth=mcp_settings.arcade.auth_disabled, + otel_meter=otel_handler.get_meter(), ) worker.catalog = catalog @@ -191,6 +213,7 @@ def create_arcade_mcp_factory() -> FastAPI: # Read configuration from env vars that were set before running the server debug = os.environ.get("ARCADE_MCP_DEBUG", "false").lower() == "true" + otel_enable = os.environ.get("ARCADE_MCP_OTEL_ENABLE", "false").lower() == "true" tool_package = os.environ.get("ARCADE_MCP_TOOL_PACKAGE") discover_installed = os.environ.get("ARCADE_MCP_DISCOVER_INSTALLED", "false").lower() == "true" show_packages = os.environ.get("ARCADE_MCP_SHOW_PACKAGES", "false").lower() == "true" @@ -216,6 +239,8 @@ def create_arcade_mcp_factory() -> FastAPI: raise RuntimeError("No tools found") logger.info(f"Total tools loaded: {total_tools}") + if otel_enable: + logger.info("OpenTelemetry is enabled") # Build kwargs for server creation kwargs = {} @@ -228,6 +253,7 @@ def create_arcade_mcp_factory() -> FastAPI: catalog=catalog, mcp_settings=None, debug=debug, + otel_enable=otel_enable, **kwargs, ) @@ -238,6 +264,7 @@ def run_arcade_mcp( port: int = 7777, reload: bool = False, debug: bool = False, + otel_enable: bool = False, tool_package: str | None = None, discover_installed: bool = False, show_packages: bool = False, @@ -253,6 +280,7 @@ def run_arcade_mcp( if reload: # Set env vars for the app factory to read later os.environ["ARCADE_MCP_DEBUG"] = str(debug) + os.environ["ARCADE_MCP_OTEL_ENABLE"] = str(otel_enable) if tool_package: os.environ["ARCADE_MCP_TOOL_PACKAGE"] = tool_package os.environ["ARCADE_MCP_DISCOVER_INSTALLED"] = str(discover_installed) @@ -278,6 +306,7 @@ def run_arcade_mcp( app = create_arcade_mcp( catalog=catalog, debug=debug, + otel_enable=otel_enable, **kwargs, )