arcade-mcp/libs/arcade-serve/arcade_serve/core/components.py
Sterling Dreyer 069ce70fb2
Instrumentation for outbound requests (#726)
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> Adds OpenTelemetry instrumentation for outbound HTTP (httpx, aiohttp,
requests), guards span environment attribute, and updates
package/version deps.
> 
> - **Telemetry**:
> - Instrument outbound HTTP clients with OpenTelemetry: `httpx`,
`aiohttp-client`, and `requests`.
>   - Tighten excluded span types using `Literal`.
> - **Core**:
> - Guard setting `environment` span attribute in `CallToolComponent`
only if present on `worker`.
> - **Packaging**:
>   - Bump `arcade-serve` to `3.2.1`.
> - Add new dependencies for HTTP client instrumentation and `aiohttp`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9ab57f3c33d6033ff9ec4c6a40445a85328b169a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2025-12-12 15:30:11 -08:00

106 lines
3.2 KiB
Python

from arcade_core.schema import (
ToolCallRequest,
ToolCallResponse,
)
from opentelemetry import trace
from arcade_serve.core.common import (
CatalogResponse,
HealthCheckResponse,
RequestData,
Router,
Worker,
WorkerComponent,
)
class CatalogComponent(WorkerComponent):
def __init__(self, worker: Worker) -> None:
self.worker = worker
def register(self, router: Router) -> None:
"""
Register the catalog route with the router.
"""
router.add_route(
"tools",
self,
method="GET",
response_type=CatalogResponse,
operation_id="get_catalog",
description="Get the catalog of tools",
summary="Get the catalog of tools",
tags=["Arcade"],
)
async def __call__(self, request: RequestData) -> CatalogResponse:
"""
Handle the request to get the catalog.
"""
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("Catalog"):
return self.worker.get_catalog()
class CallToolComponent(WorkerComponent):
def __init__(self, worker: Worker) -> None:
self.worker = worker
def register(self, router: Router) -> None:
"""
Register the call tool route with the router.
"""
router.add_route(
"tools/invoke",
self,
method="POST",
response_type=ToolCallResponse,
operation_id="call_tool",
description="Call a tool",
summary="Call a tool",
tags=["Arcade"],
)
async def __call__(self, request: RequestData) -> ToolCallResponse:
"""
Handle the request to call (invoke) a tool.
"""
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("CallTool") as current_span:
call_tool_request_data = request.body_json
call_tool_request = ToolCallRequest.model_validate(call_tool_request_data)
current_span.set_attribute("tool_name", str(call_tool_request.tool.name))
current_span.set_attribute("toolkit_version", str(call_tool_request.tool.version))
current_span.set_attribute("toolkit_name", str(call_tool_request.tool.toolkit))
if hasattr(self.worker, "environment"):
current_span.set_attribute("environment", self.worker.environment)
return await self.worker.call_tool(call_tool_request)
class HealthCheckComponent(WorkerComponent):
def __init__(self, worker: Worker) -> None:
self.worker = worker
def register(self, router: Router) -> None:
"""
Register the health check route with the router.
"""
router.add_route(
"health",
self,
method="GET",
response_type=HealthCheckResponse,
operation_id="health_check",
description="Health check",
summary="Health check",
tags=["Arcade"],
require_auth=False,
)
async def __call__(self, request: RequestData) -> HealthCheckResponse:
"""
Handle the request to check the health of the worker.
"""
return self.worker.health_check()