fix: Accept string instance_id in telemetry endpoint

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 <noreply@anthropic.com>
This commit is contained in:
Matt 2025-12-24 15:11:30 +01:00
parent 2a1270bfbd
commit 02fc18f009
2 changed files with 6 additions and 7 deletions

View File

@ -7,7 +7,6 @@ It validates authentication, stores metrics, and updates instance state.
import hashlib import hashlib
import logging import logging
import secrets import secrets
from uuid import UUID
from fastapi import APIRouter, Header, HTTPException, status from fastapi import APIRouter, Header, HTTPException, status
from sqlalchemy import select from sqlalchemy import select
@ -27,7 +26,7 @@ router = APIRouter(prefix="/api/v1/instances", tags=["Telemetry"])
@router.post("/{instance_id}/telemetry", response_model=TelemetryResponse) @router.post("/{instance_id}/telemetry", response_model=TelemetryResponse)
async def receive_telemetry( async def receive_telemetry(
instance_id: UUID, instance_id: str,
payload: TelemetryPayload, payload: TelemetryPayload,
db: AsyncSessionDep, db: AsyncSessionDep,
hub_api_key: str = Header(..., alias="X-Hub-Api-Key"), 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) # Find instance by instance_id string (e.g., "letsbe-orchestrator")
result = await db.execute(select(Instance).where(Instance.id == instance_id)) result = await db.execute(select(Instance).where(Instance.instance_id == instance_id))
instance = result.scalar_one_or_none() instance = result.scalar_one_or_none()
if instance is None: if instance is None:
@ -114,8 +113,9 @@ async def receive_telemetry(
# Store telemetry sample # Store telemetry sample
# Use PostgreSQL upsert to handle duplicates gracefully # Use PostgreSQL upsert to handle duplicates gracefully
# Note: instance_id in DB is the UUID (instance.id), not the string instance_id
telemetry_data = { telemetry_data = {
"instance_id": instance_id, "instance_id": instance.id,
"window_start": payload.window_start, "window_start": payload.window_start,
"window_end": payload.window_end, "window_end": payload.window_end,
"uptime_seconds": payload.uptime_seconds, "uptime_seconds": payload.uptime_seconds,

View File

@ -6,7 +6,6 @@ unknown fields, preventing accidental PII leaks.
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@ -87,7 +86,7 @@ class TelemetryPayload(BaseModel):
model_config = ConfigDict(extra="forbid") 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_start: datetime = Field(..., description="Start of telemetry window")
window_end: datetime = Field(..., description="End of telemetry window") window_end: datetime = Field(..., description="End of telemetry window")
uptime_seconds: int = Field(ge=0, description="Orchestrator uptime in seconds") uptime_seconds: int = Field(ge=0, description="Orchestrator uptime in seconds")