From b37356d3bbec554dc1b89fc50d55344be2305568 Mon Sep 17 00:00:00 2001 From: Priyanka Punukollu Date: Sat, 28 Feb 2026 08:51:02 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20add=20structured=20error=20tracking=20w?= =?UTF-8?q?ith=20context=20=E2=80=94=20logger,=20error=5Flog=20store,=20me?= =?UTF-8?q?trics=20endpoint=20includes=20recent=20errors=20and=20stack=20t?= =?UTF-8?q?races?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- main.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index dddb6a4b5..fb2733c47 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,17 @@ import json +import logging import time +import traceback import os import uuid from datetime import datetime, timedelta +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", +) +logger = logging.getLogger("ghostfolio_agent") + from fastapi import FastAPI, Response, Depends, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse, StreamingResponse, HTMLResponse, JSONResponse @@ -86,10 +94,29 @@ graph = build_graph() feedback_log: list[dict] = [] cost_log: list[dict] = [] +error_log: list[dict] = [] # last 50 errors for metrics COST_PER_REQUEST_USD = (2000 * 0.000003) + (500 * 0.000015) +def log_error(error: Exception, context: dict) -> dict: + """Captures error with full context for debugging.""" + error_record = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "error_type": type(error).__name__, + "error_message": str(error), + "traceback": traceback.format_exc(), + "context": context, + } + error_log.append(error_record) + if len(error_log) > 50: + error_log.pop(0) + logger.error( + f"{error_record['error_type']}: {error_record['error_message']} | context={context}" + ) + return error_record + + class ChatRequest(BaseModel): query: str history: list[dict] = [] @@ -146,7 +173,32 @@ async def chat(req: ChatRequest, gf_token: str = Depends(require_auth)): "error": None, } - result = await graph.ainvoke(initial_state) + try: + result = await graph.ainvoke(initial_state) + except Exception as e: + log_error( + e, + { + "message": req.query[:200], + "session_id": None, + "query_type": "unknown", + }, + ) + latency_ms = int((time.time() - start_time) * 1000) + return { + "response": "I encountered an error processing your request. Please try again.", + "error": str(e), + "confidence": 0.0, + "verified": False, + "latency_ms": latency_ms, + "trace_id": trace_id, + "tokens": { + "estimated_input": 0, + "estimated_output": 0, + "estimated_total": 0, + "estimated_cost_usd": 0.0, + }, + } elapsed = round(time.time() - start_time, 2) latency_ms = int((time.time() - start_time) * 1000) @@ -772,3 +824,21 @@ async def costs(_auth: str = Depends(require_auth)): "output_price_per_million": 15.0, }, } + + +@app.get("/metrics") +async def get_metrics(): + """Observability metrics including recent errors.""" + total_requests = len(cost_log) + total_latency_ms = sum(c.get("latency_seconds", 0) * 1000 for c in cost_log[-100:]) + return { + "total_requests": total_requests, + "avg_latency_ms": round( + total_latency_ms / max(min(total_requests, 100), 1) + ) + if total_requests > 0 + else 0, + "recent_errors": error_log[-5:], + "error_count": len(error_log), + "last_updated": datetime.utcnow().isoformat() + "Z", + }