From 02fc18f009af1919960aede20a25777544159044 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Dec 2025 15:11:30 +0100 Subject: [PATCH] fix: Accept string instance_id in telemetry endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The orchestrator sends instance_id as a string (e.g., "letsbe-orchestrator") but the endpoint was expecting a UUID path parameter. This caused 422 validation errors when orchestrators tried to send telemetry. - Changed path parameter from UUID to str - Lookup instance by Instance.instance_id (string) instead of Instance.id (UUID) - Store telemetry with instance.id (UUID) for correct FK relationship - Updated TelemetryPayload schema to use str instead of UUID 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/routes/telemetry.py | 10 +++++----- app/schemas/telemetry.py | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/routes/telemetry.py b/app/routes/telemetry.py index 0aa28c1..65b8b0f 100644 --- a/app/routes/telemetry.py +++ b/app/routes/telemetry.py @@ -7,7 +7,6 @@ It validates authentication, stores metrics, and updates instance state. import hashlib import logging import secrets -from uuid import UUID from fastapi import APIRouter, Header, HTTPException, status from sqlalchemy import select @@ -27,7 +26,7 @@ router = APIRouter(prefix="/api/v1/instances", tags=["Telemetry"]) @router.post("/{instance_id}/telemetry", response_model=TelemetryResponse) async def receive_telemetry( - instance_id: UUID, + instance_id: str, payload: TelemetryPayload, db: AsyncSessionDep, hub_api_key: str = Header(..., alias="X-Hub-Api-Key"), @@ -64,8 +63,8 @@ async def receive_telemetry( }, ) - # Find instance by UUID (id column, not instance_id string) - result = await db.execute(select(Instance).where(Instance.id == instance_id)) + # Find instance by instance_id string (e.g., "letsbe-orchestrator") + result = await db.execute(select(Instance).where(Instance.instance_id == instance_id)) instance = result.scalar_one_or_none() if instance is None: @@ -114,8 +113,9 @@ async def receive_telemetry( # Store telemetry sample # Use PostgreSQL upsert to handle duplicates gracefully + # Note: instance_id in DB is the UUID (instance.id), not the string instance_id telemetry_data = { - "instance_id": instance_id, + "instance_id": instance.id, "window_start": payload.window_start, "window_end": payload.window_end, "uptime_seconds": payload.uptime_seconds, diff --git a/app/schemas/telemetry.py b/app/schemas/telemetry.py index b9993f1..3dfb2b4 100644 --- a/app/schemas/telemetry.py +++ b/app/schemas/telemetry.py @@ -6,7 +6,6 @@ unknown fields, preventing accidental PII leaks. from datetime import datetime from typing import Optional -from uuid import UUID from pydantic import BaseModel, ConfigDict, Field @@ -87,7 +86,7 @@ class TelemetryPayload(BaseModel): model_config = ConfigDict(extra="forbid") - instance_id: UUID = Field(..., description="Instance UUID (must match path)") + instance_id: str = Field(..., description="Instance ID string (must match path)") window_start: datetime = Field(..., description="Start of telemetry window") window_end: datetime = Field(..., description="End of telemetry window") uptime_seconds: int = Field(ge=0, description="Orchestrator uptime in seconds")