Add traffic telemetry export
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import uuid
|
||||
import time
|
||||
import asyncio
|
||||
import csv
|
||||
import io
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
@@ -386,6 +389,60 @@ async def get_traffic_feed(limit: int = 50, db: AsyncSession = Depends(get_db)):
|
||||
return res.scalars().all()
|
||||
|
||||
|
||||
@router.get("/api/traffic/export")
|
||||
async def export_traffic(
|
||||
format: str = Query("json", pattern="^(csv|json)$"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
res = await db.execute(select(RequestRecord).order_by(desc(RequestRecord.timestamp)))
|
||||
records = res.scalars().all()
|
||||
fields = [
|
||||
"timestamp",
|
||||
"request_id",
|
||||
"prompt_preview",
|
||||
"task_type",
|
||||
"complexity",
|
||||
"selected_model",
|
||||
"input_tokens",
|
||||
"output_tokens",
|
||||
"total_tokens",
|
||||
"cost_saved",
|
||||
"total_latency_ms",
|
||||
]
|
||||
rows = [
|
||||
{
|
||||
"timestamp": record.timestamp.isoformat() if record.timestamp else "",
|
||||
"request_id": record.request_id,
|
||||
"prompt_preview": record.prompt[:200],
|
||||
"task_type": record.task_type,
|
||||
"complexity": record.complexity,
|
||||
"selected_model": record.selected_model,
|
||||
"input_tokens": record.input_tokens,
|
||||
"output_tokens": record.output_tokens,
|
||||
"total_tokens": record.total_tokens,
|
||||
"cost_saved": record.cost_saved,
|
||||
"total_latency_ms": record.total_latency_ms,
|
||||
}
|
||||
for record in records
|
||||
]
|
||||
|
||||
if format == "json":
|
||||
return JSONResponse(
|
||||
content=rows,
|
||||
headers={"Content-Disposition": "attachment; filename=traffic-export.json"},
|
||||
)
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.DictWriter(output, fieldnames=fields)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=traffic-export.csv"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/decisions/{decision_id_or_request_id}")
|
||||
async def get_decision_details(decision_id_or_request_id: str, db: AsyncSession = Depends(get_db)):
|
||||
res_dec = await db.execute(
|
||||
|
||||
@@ -64,3 +64,64 @@ async def test_api_analytics():
|
||||
data = res.json()
|
||||
assert "total_requests" in data
|
||||
assert "savings" in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_traffic_export_json_contains_audit_fields():
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
generate_res = await client.post(
|
||||
"/api/generate",
|
||||
json={"prompt": "Write a Python function for a traffic export"},
|
||||
)
|
||||
assert generate_res.status_code == 200
|
||||
|
||||
export_res = await client.get("/api/traffic/export?format=json")
|
||||
|
||||
assert export_res.status_code == 200
|
||||
assert export_res.headers["content-type"].startswith("application/json")
|
||||
assert "attachment" in export_res.headers["content-disposition"]
|
||||
records = export_res.json()
|
||||
assert records
|
||||
assert {
|
||||
"timestamp",
|
||||
"request_id",
|
||||
"prompt_preview",
|
||||
"task_type",
|
||||
"complexity",
|
||||
"selected_model",
|
||||
"input_tokens",
|
||||
"output_tokens",
|
||||
"total_tokens",
|
||||
"cost_saved",
|
||||
"total_latency_ms",
|
||||
}.issubset(records[0])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_traffic_export_csv_returns_downloadable_rows():
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
generate_res = await client.post(
|
||||
"/api/generate",
|
||||
json={"prompt": "Summarize this traffic record"},
|
||||
)
|
||||
assert generate_res.status_code == 200
|
||||
|
||||
export_res = await client.get("/api/traffic/export?format=csv")
|
||||
|
||||
assert export_res.status_code == 200
|
||||
assert export_res.headers["content-type"].startswith("text/csv")
|
||||
assert "traffic-export.csv" in export_res.headers["content-disposition"]
|
||||
lines = export_res.text.splitlines()
|
||||
assert lines[0].startswith("timestamp,request_id,prompt_preview,")
|
||||
assert len(lines) >= 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_traffic_export_rejects_unknown_format():
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
export_res = await client.get("/api/traffic/export?format=xml")
|
||||
|
||||
assert export_res.status_code == 422
|
||||
|
||||
Reference in New Issue
Block a user