""" Developer Tools Enhancement - Webhooks, SDK Center, API Docs, Usage Dashboard. Phase 3.9: Extends the developer module with webhook management, SDK download center, interactive API documentation, and enhanced analytics. """ import json import secrets import hashlib import hmac import urllib.request import urllib.error from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy import text from sqlalchemy.orm import Session from ...database import get_db, SessionLocal from ...auth import get_current_admin router = APIRouter() CST_TZ = timezone(timedelta(hours=8)) def cst_now(): return datetime.now(CST_TZ) def fmt_time(dt): if dt is None: return None if dt.tzinfo is None: return dt.replace(tzinfo=CST_TZ).isoformat() return dt.isoformat() def _execute_raw(db, sql, params=None): """Execute raw SQL and return list of dicts.""" result = db.execute(text(sql), params or {}) rows = [] for row in result: rows.append(dict(row._mapping)) return rows # ============================================================ # Pydantic Models # ============================================================ class WebhookCreate(BaseModel): name: str url: str events: list = [] secret: str = "" headers: dict = {} class WebhookUpdate(BaseModel): name: str = None url: str = None events: list = None secret: str = None headers: dict = None status: str = None class WebhookTest(BaseModel): event: str = "test" payload: dict = {"message": "Test webhook delivery"} # ============================================================ # Webhook Management # ============================================================ SUPPORTED_EVENTS = [ {"value": "app.created", "label": "App Created", "desc": "Triggered when a new AI app is created"}, {"value": "app.published", "label": "App Published", "desc": "Triggered when an app is published to market"}, {"value": "agent.created", "label": "Agent Created", "desc": "Triggered when a new agent is created"}, {"value": "knowledge.updated", "label": "Knowledge Updated", "desc": "Triggered when knowledge base is updated"}, {"value": "api_key.created", "label": "API Key Created", "desc": "Triggered when a new API key is generated"}, {"value": "api_key.revoked", "label": "API Key Revoked", "desc": "Triggered when an API key is revoked"}, {"value": "user.registered", "label": "User Registered", "desc": "Triggered when a new user registers"}, {"value": "model.connected", "label": "Model Connected", "desc": "Triggered when a model connection test passes"}, {"value": "workflow.completed", "label": "Workflow Completed", "desc": "Triggered when a workflow execution finishes"}, {"value": "storage.threshold", "label": "Storage Threshold", "desc": "Triggered when storage usage exceeds threshold"}, ] @router.get("/webhooks", summary="List webhooks") async def list_webhooks( status: str = Query(None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """List all webhook configurations.""" where_clause = "WHERE 1=1" params = {} if status: where_clause += " AND status = :status" params["status"] = status count_sql = f"SELECT COUNT(*) as cnt FROM webhook_configs {where_clause}" count_rows = _execute_raw(db, count_sql, params) total = count_rows[0]["cnt"] if count_rows else 0 offset = (page - 1) * page_size list_sql = f"""SELECT id, name, url, events, secret, headers, status, last_triggered_at, last_status, total_deliveries, success_count, fail_count, created_at, updated_at FROM webhook_configs {where_clause} ORDER BY created_at DESC LIMIT :limit OFFSET :offset""" list_params = dict(params) list_params["limit"] = page_size list_params["offset"] = offset rows = _execute_raw(db, list_sql, list_params) items = [] for r in rows: events_raw = r.get("events") if isinstance(events_raw, str): try: events_val = json.loads(events_raw) except Exception: events_val = [] else: events_val = events_raw or [] headers_raw = r.get("headers") if isinstance(headers_raw, str): try: headers_val = json.loads(headers_raw) except Exception: headers_val = {} else: headers_val = headers_raw or {} success_rate = 0 if r.get("total_deliveries", 0) > 0: success_rate = round(r["success_count"] / r["total_deliveries"] * 100, 1) items.append({ "id": r["id"], "name": r["name"], "url": r["url"], "events": events_val, "has_secret": bool(r.get("secret")), "headers": headers_val, "status": r["status"], "last_triggered_at": fmt_time(r.get("last_triggered_at")), "last_status": r.get("last_status"), "total_deliveries": r.get("total_deliveries", 0), "success_count": r.get("success_count", 0), "fail_count": r.get("fail_count", 0), "success_rate": success_rate, "created_at": fmt_time(r.get("created_at")), "updated_at": fmt_time(r.get("updated_at")), }) return {"code": 0, "data": {"total": total, "page": page, "page_size": page_size, "items": items}} @router.get("/webhooks/events", summary="List supported webhook events") async def list_webhook_events( admin_user=Depends(get_current_admin), ): """List all supported webhook event types.""" return {"code": 0, "data": {"events": SUPPORTED_EVENTS}} @router.post("/webhooks/", summary="Create webhook") async def create_webhook( body: WebhookCreate, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """Create a new webhook configuration.""" webhook_id = "wh_" + secrets.token_hex(12) now = cst_now() secret_val = body.secret if body.secret else "whsec_" + secrets.token_hex(16) insert_sql = """INSERT INTO webhook_configs (id, name, url, events, secret, headers, status, last_triggered_at, last_status, total_deliveries, success_count, fail_count, created_at, updated_at) VALUES (:id, :name, :url, :events, :secret, :headers, 'active', NULL, NULL, 0, 0, 0, :now, :now)""" params = { "id": webhook_id, "name": body.name, "url": body.url, "events": json.dumps(body.events), "secret": secret_val, "headers": json.dumps(body.headers), "now": now, } db.execute(text(insert_sql), params) db.commit() return {"code": 0, "data": {"id": webhook_id, "name": body.name, "secret": secret_val, "message": "Webhook created"}} @router.put("/webhooks/{webhook_id}", summary="Update webhook") async def update_webhook( webhook_id: str, body: WebhookUpdate, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """Update an existing webhook configuration.""" check_sql = "SELECT id FROM webhook_configs WHERE id = :id" rows = _execute_raw(db, check_sql, {"id": webhook_id}) if not rows: raise HTTPException(status_code=404, detail="Webhook not found") updates = [] params = {"id": webhook_id, "now": cst_now()} if body.name is not None: updates.append("name = :name") params["name"] = body.name if body.url is not None: updates.append("url = :url") params["url"] = body.url if body.events is not None: updates.append("events = :events") params["events"] = json.dumps(body.events) if body.secret is not None: updates.append("secret = :secret") params["secret"] = body.secret if body.headers is not None: updates.append("headers = :headers") params["headers"] = json.dumps(body.headers) if body.status is not None: updates.append("status = :status") params["status"] = body.status if not updates: return {"code": 0, "message": "No changes"} updates.append("updated_at = :now") update_sql = f"UPDATE webhook_configs SET {', '.join(updates)} WHERE id = :id" db.execute(text(update_sql), params) db.commit() return {"code": 0, "message": "Webhook updated"} @router.delete("/webhooks/{webhook_id}", summary="Delete webhook") async def delete_webhook( webhook_id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """Delete a webhook configuration.""" check_sql = "SELECT id FROM webhook_configs WHERE id = :id" rows = _execute_raw(db, check_sql, {"id": webhook_id}) if not rows: raise HTTPException(status_code=404, detail="Webhook not found") db.execute(text("DELETE FROM webhook_configs WHERE id = :id"), {"id": webhook_id}) db.commit() return {"code": 0, "message": "Webhook deleted"} @router.post("/webhooks/{webhook_id}/test", summary="Test webhook delivery") async def test_webhook( webhook_id: str, body: WebhookTest = None, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """Send a test payload to the webhook endpoint.""" select_sql = "SELECT * FROM webhook_configs WHERE id = :id" rows = _execute_raw(db, select_sql, {"id": webhook_id}) if not rows: raise HTTPException(status_code=404, detail="Webhook not found") wh = rows[0] url = wh["url"] secret = wh.get("secret", "") test_event = body.event if body else "test" test_payload = body.payload if body else {"message": "Test webhook delivery"} full_payload = { "event": test_event, "timestamp": cst_now().isoformat(), "webhook_id": webhook_id, "data": test_payload, } payload_bytes = json.dumps(full_payload).encode("utf-8") # Build headers headers = {"Content-Type": "application/json"} headers_raw = wh.get("headers") if isinstance(headers_raw, str): try: headers.update(json.loads(headers_raw)) except Exception: pass elif isinstance(headers_raw, dict): headers.update(headers_raw) # HMAC signature if secret: signature = hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest() headers["X-Webhook-Signature"] = signature # Send request delivery_id = "dlv_" + secrets.token_hex(12) now = cst_now() response_code = None response_body = None response_time_ms = None status = "failed" error_message = None try: req = urllib.request.Request(url, data=payload_bytes, headers=headers, method="POST") start_time = datetime.now() resp = urllib.request.urlopen(req, timeout=10) end_time = datetime.now() response_code = resp.getcode() response_body = resp.read().decode("utf-8", errors="replace")[:2000] response_time_ms = int((end_time - start_time).total_seconds() * 1000) if 200 <= response_code < 300: status = "success" except urllib.error.HTTPError as e: response_code = e.code try: response_body = e.read().decode("utf-8", errors="replace")[:2000] except Exception: response_body = str(e) error_message = f"HTTP {e.code}: {str(e)[:200]}" except Exception as e: error_message = str(e)[:500] # Record delivery insert_sql = """INSERT INTO webhook_deliveries (id, webhook_id, event, payload, status, response_code, response_body, response_time_ms, attempts, error_message, created_at) VALUES (:id, :wid, :event, :payload, :status, :rcode, :rbody, :rtime, 1, :err, :now)""" db.execute(text(insert_sql), { "id": delivery_id, "wid": webhook_id, "event": test_event, "payload": json.dumps(full_payload), "status": status, "rcode": response_code, "rbody": response_body, "rtime": response_time_ms, "err": error_message, "now": now, }) # Update webhook stats total_deliveries = (wh.get("total_deliveries") or 0) + 1 success_count = (wh.get("success_count") or 0) + (1 if status == "success" else 0) fail_count = (wh.get("fail_count") or 0) + (1 if status != "success" else 0) db.execute(text("""UPDATE webhook_configs SET total_deliveries = :td, success_count = :sc, fail_count = :fc, last_triggered_at = :now, last_status = :ls, updated_at = :now WHERE id = :id"""), { "td": total_deliveries, "sc": success_count, "fc": fail_count, "now": now, "ls": status, "id": webhook_id, }) db.commit() return { "code": 0, "data": { "delivery_id": delivery_id, "status": status, "response_code": response_code, "response_time_ms": response_time_ms, "response_body": response_body[:500] if response_body else None, "error_message": error_message, } } @router.get("/webhooks/{webhook_id}/deliveries", summary="Webhook delivery history") async def webhook_deliveries( webhook_id: str, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """Get delivery history for a specific webhook.""" count_sql = "SELECT COUNT(*) as cnt FROM webhook_deliveries WHERE webhook_id = :wid" count_rows = _execute_raw(db, count_sql, {"wid": webhook_id}) total = count_rows[0]["cnt"] if count_rows else 0 offset = (page - 1) * page_size list_sql = """SELECT id, webhook_id, event, payload, status, response_code, response_body, response_time_ms, attempts, error_message, created_at FROM webhook_deliveries WHERE webhook_id = :wid ORDER BY created_at DESC LIMIT :limit OFFSET :offset""" rows = _execute_raw(db, list_sql, {"wid": webhook_id, "limit": page_size, "offset": offset}) items = [] for r in rows: payload_raw = r.get("payload") if isinstance(payload_raw, str): try: payload_val = json.loads(payload_raw) except Exception: payload_val = payload_raw else: payload_val = payload_raw items.append({ "id": r["id"], "event": r["event"], "status": r["status"], "response_code": r.get("response_code"), "response_time_ms": r.get("response_time_ms"), "attempts": r.get("attempts", 0), "error_message": r.get("error_message"), "payload": payload_val, "response_body": (r.get("response_body") or "")[:500], "created_at": fmt_time(r.get("created_at")), }) return {"code": 0, "data": {"total": total, "page": page, "page_size": page_size, "items": items}} # ============================================================ # SDK Download Center # ============================================================ SDK_PACKAGES = [ { "language": "python", "name": "Python SDK", "version": "1.2.0", "description": "Official Python SDK for Qihu-AIFlow platform. Supports chat completions, knowledge base, agent execution, and file upload.", "install_cmd": "pip install qihu-aiflow", "download_url": "https://pypi.org/project/qihu-aiflow/", "github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-python", "min_version": "Python 3.8+", "features": ["Chat completions (streaming)", "Knowledge base queries", "Agent execution", "File upload", "Webhook verification", "Async support"], "code_example": '''from qihu_aiflow import QihuClient client = QihuClient(api_key="sk-your-api-key", base_url="http://210.16.161.218:18080") # Chat completion response = client.chat.create( model="deepseek-ai/DeepSeek-V4-Flash", messages=[{"role": "user", "content": "Hello!"}] ) print(response.choices[0].message.content) # Streaming for chunk in client.chat.create( model="deepseek-ai/DeepSeek-V4-Flash", messages=[{"role": "user", "content": "Tell me a story"}], stream=True ): print(chunk.choices[0].delta.content, end="")''', }, { "language": "javascript", "name": "JavaScript SDK", "version": "1.1.0", "description": "Official JavaScript/TypeScript SDK for browser and Node.js. Full TypeScript support with type definitions.", "install_cmd": "npm install @qihu/aiflow", "download_url": "https://www.npmjs.com/package/@qihu/aiflow", "github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-js", "min_version": "Node.js 16+ / Browser", "features": ["Chat completions (streaming)", "Knowledge base queries", "Agent execution", "File upload", "WebSocket support", "TypeScript types"], "code_example": '''import { QihuClient } from "@qihu/aiflow"; const client = new QihuClient({ apiKey: "sk-your-api-key", baseUrl: "http://210.16.161.218:18080", }); // Chat completion const response = await client.chat.create({ model: "deepseek-ai/DeepSeek-V4-Flash", messages: [{ role: "user", content: "Hello!" }], }); console.log(response.choices[0].message.content); // Streaming const stream = await client.chat.create({ model: "deepseek-ai/DeepSeek-V4-Flash", messages: [{ role: "user", content: "Tell me a story" }], stream: true, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0].delta.content || ""); }''', }, { "language": "go", "name": "Go SDK", "version": "1.0.0", "description": "Official Go SDK for Qihu-AIFlow. Lightweight and fast, suitable for microservices and CLI tools.", "install_cmd": "go get github.com/qihuyun/qihu-aiflow-sdk-go", "download_url": "https://github.com/qihuyun/qihu-aiflow-sdk-go/releases", "github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-go", "min_version": "Go 1.19+", "features": ["Chat completions", "Knowledge base queries", "Agent execution", "Context cancellation", "Retry logic", "Connection pooling"], "code_example": '''package main import ( "context" "fmt" "github.com/qihuyun/qihu-aiflow-sdk-go/qihu" ) func main() { client := qihu.NewClient("sk-your-api-key", "http://210.16.161.218:18080") resp, err := client.Chat.Create(context.Background(), &qihu.ChatRequest{ Model: "deepseek-ai/DeepSeek-V4-Flash", Messages: []qihu.Message{ {Role: "user", Content: "Hello!"}, }, }) if err != nil { panic(err) } fmt.Println(resp.Choices[0].Message.Content) }''', }, { "language": "curl", "name": "REST API (cURL)", "version": "1.0.0", "description": "Direct REST API access without SDK. Use any HTTP client to interact with the platform.", "install_cmd": "# No installation needed - use curl directly", "download_url": "https://docs.qihuyun.com/api-reference", "github_url": "", "min_version": "Any HTTP client", "features": ["Chat completions", "Knowledge base queries", "Agent execution", "File upload", "Webhook management", "No dependencies"], "code_example": '''# Chat completion curl -X POST http://210.16.161.218:18080/app/api/chat/completions \\ -H "Authorization: Bearer sk-your-api-key" \\ -H "Content-Type: application/json" \\ -d '{ "model": "deepseek-ai/DeepSeek-V4-Flash", "messages": [{"role": "user", "content": "Hello!"}] }' # List knowledge bases curl http://210.16.161.218:18080/app/api/knowledge-bases \\ -H "Authorization: Bearer sk-your-api-key"''', }, ] @router.get("/sdk/packages", summary="List SDK packages") async def list_sdk_packages( admin_user=Depends(get_current_admin), ): """List all available SDK packages with download info and code examples.""" return {"code": 0, "data": {"packages": SDK_PACKAGES}} # ============================================================ # Interactive API Documentation # ============================================================ API_DOC_SECTIONS = [ { "section": "Authentication", "icon": "lock", "description": "All API requests require authentication via Bearer token.", "endpoints": [ { "method": "POST", "path": "/app/api/auth/login", "title": "User Login", "description": "Authenticate and obtain an access token.", "params": [ {"name": "username", "type": "string", "required": True, "desc": "Username"}, {"name": "password", "type": "string", "required": True, "desc": "Password"}, ], "example_request": '{"username": "admin", "password": "admin123"}', "example_response": '{"code": 0, "data": {"access_token": "eyJ...", "token_type": "bearer", "user": {"id": "admin-001", "username": "admin"}}}', }, ], }, { "section": "Chat & Completions", "icon": "chat", "description": "AI chat completion endpoints for conversational AI.", "endpoints": [ { "method": "POST", "path": "/app/api/chat/completions", "title": "Chat Completion", "description": "Create a chat completion with the specified model.", "params": [ {"name": "model", "type": "string", "required": True, "desc": "Model ID (e.g. deepseek-ai/DeepSeek-V4-Flash)"}, {"name": "messages", "type": "array", "required": True, "desc": "Array of message objects"}, {"name": "stream", "type": "boolean", "required": False, "desc": "Enable streaming response"}, {"name": "temperature", "type": "number", "required": False, "desc": "Sampling temperature (0-2)"}, {"name": "max_tokens", "type": "integer", "required": False, "desc": "Maximum tokens to generate"}, ], "example_request": '{"model": "deepseek-ai/DeepSeek-V4-Flash", "messages": [{"role": "user", "content": "Hello"}]}', "example_response": '{"code": 0, "data": {"id": "chat-xxx", "choices": [{"message": {"role": "assistant", "content": "Hi there!"}}]}}', }, ], }, { "section": "Knowledge Base", "icon": "book", "description": "Manage knowledge bases and documents for RAG.", "endpoints": [ { "method": "GET", "path": "/app/api/knowledge-bases", "title": "List Knowledge Bases", "description": "Get all knowledge bases for the current user.", "params": [], "example_request": "", "example_response": '{"code": 0, "data": [{"id": "kb-001", "name": "Product Docs", "document_count": 15}]}', }, { "method": "POST", "path": "/app/api/knowledge-bases/{kb_id}/documents", "title": "Upload Document", "description": "Upload a document to a knowledge base for processing.", "params": [ {"name": "file", "type": "file", "required": True, "desc": "Document file (PDF, TXT, DOCX, MD)"}, ], "example_request": "multipart/form-data: file=@document.pdf", "example_response": '{"code": 0, "data": {"document_id": "doc-001", "status": "processing"}}', }, ], }, { "section": "Agents", "icon": "robot", "description": "Agent management and execution endpoints.", "endpoints": [ { "method": "GET", "path": "/app/api/agents", "title": "List Agents", "description": "Get all available agents.", "params": [], "example_request": "", "example_response": '{"code": 0, "data": [{"id": "agent-001", "name": "Code Assistant", "type": "agent"}]}', }, { "method": "POST", "path": "/app/api/agents/{agent_id}/execute", "title": "Execute Agent", "description": "Execute an agent with a given input.", "params": [ {"name": "input", "type": "string", "required": True, "desc": "Input text for the agent"}, {"name": "context", "type": "object", "required": False, "desc": "Additional context"}, ], "example_request": '{"input": "Help me write a Python function"}', "example_response": '{"code": 0, "data": {"output": "Here is a Python function...", "tool_calls": []}}', }, ], }, { "section": "Workflows", "icon": "workflow", "description": "DAG workflow orchestration endpoints.", "endpoints": [ { "method": "GET", "path": "/app/api/admin/workflows/templates", "title": "List Workflow Templates", "description": "Get predefined workflow templates.", "params": [], "example_request": "", "example_response": '{"code": 0, "data": [{"id": "tpl-001", "name": "Q&A Pipeline", "nodes": [...]}]}', }, { "method": "POST", "path": "/app/api/admin/workflows/{id}/execute", "title": "Execute Workflow", "description": "Execute a workflow with input data.", "params": [ {"name": "input", "type": "string", "required": True, "desc": "Input for the workflow"}, ], "example_request": '{"input": "Summarize this article"}', "example_response": '{"code": 0, "data": {"execution_id": "exec-001", "status": "completed", "result": "..."}}', }, ], }, { "section": "File Storage", "icon": "storage", "description": "File upload and storage management.", "endpoints": [ { "method": "POST", "path": "/app/api/files/upload", "title": "Upload File", "description": "Upload a file to storage (MinIO/OSS).", "params": [ {"name": "file", "type": "file", "required": True, "desc": "File to upload"}, {"name": "category", "type": "string", "required": False, "desc": "File category"}, ], "example_request": "multipart/form-data: file=@image.png", "example_response": '{"code": 0, "data": {"url": "https://...", "size": 102400, "file_type": "image"}}', }, ], }, { "section": "Webhooks", "icon": "webhook", "description": "Webhook configuration and management.", "endpoints": [ { "method": "GET", "path": "/app/api/admin/developer-tools/webhooks", "title": "List Webhooks", "description": "List all configured webhooks.", "params": [], "example_request": "", "example_response": '{"code": 0, "data": {"total": 2, "items": [{"id": "wh_xxx", "name": "My Webhook", "url": "https://...", "status": "active"}]}}', }, { "method": "POST", "path": "/app/api/admin/developer-tools/webhooks/", "title": "Create Webhook", "description": "Create a new webhook endpoint.", "params": [ {"name": "name", "type": "string", "required": True, "desc": "Webhook name"}, {"name": "url", "type": "string", "required": True, "desc": "Webhook URL"}, {"name": "events", "type": "array", "required": True, "desc": "Events to subscribe"}, ], "example_request": '{"name": "My Webhook", "url": "https://example.com/webhook", "events": ["app.created"]}', "example_response": '{"code": 0, "data": {"id": "wh_xxx", "secret": "whsec_..."}}', }, ], }, ] @router.get("/api-docs", summary="Interactive API documentation") async def get_api_docs( admin_user=Depends(get_current_admin), ): """Get structured API documentation for the interactive docs viewer.""" total_endpoints = sum(len(s["endpoints"]) for s in API_DOC_SECTIONS) return { "code": 0, "data": { "sections": API_DOC_SECTIONS, "total_sections": len(API_DOC_SECTIONS), "total_endpoints": total_endpoints, "base_url": "http://210.16.161.218:18080", "api_version": "v1", } } # ============================================================ # Enhanced Usage Dashboard # ============================================================ @router.get("/usage/dashboard", summary="Enhanced usage dashboard") async def usage_dashboard( days: int = Query(7, ge=1, le=90), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """Enhanced usage statistics dashboard with trend data.""" now = cst_now() start_date = now - timedelta(days=days) # API call trends (daily) trend_sql = """SELECT DATE(created_at) as date, COUNT(*) as calls, SUM(CASE WHEN error_count > 0 THEN 1 ELSE 0 END) as errors, SUM(CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) as avg_time FROM api_call_stats WHERE created_at >= :start GROUP BY DATE(created_at) ORDER BY date""" trend_rows = _execute_raw(db, trend_sql, {"start": start_date}) trend = [] for r in trend_rows: trend.append({ "date": r["date"].strftime("%Y-%m-%d") if r.get("date") else "", "calls": r.get("calls", 0), "errors": r.get("errors", 0), "avg_time": round(r.get("avg_time", 0), 2) if r.get("avg_time") else 0, }) # Top endpoints by call count top_sql = """SELECT endpoint, method, call_count, error_count, last_called_at, total_response_time FROM api_call_stats ORDER BY call_count DESC LIMIT 10""" top_rows = _execute_raw(db, top_sql) top_endpoints = [] for r in top_rows: top_endpoints.append({ "endpoint": r.get("endpoint", ""), "method": r.get("method", ""), "call_count": r.get("call_count", 0), "error_count": r.get("error_count", 0), "error_rate": round(r.get("error_count", 0) / max(r.get("call_count", 1), 1) * 100, 2) if r.get("call_count", 0) > 0 else 0, "avg_response_time": round(r.get("total_response_time", 0) / max(r.get("call_count", 1), 1), 2) if r.get("call_count", 0) > 0 else 0, "last_called_at": fmt_time(r.get("last_called_at")), }) # Error distribution by status code error_sql = """SELECT CASE WHEN error_count > 0 THEN 'has_errors' ELSE 'success' END as status_group, COUNT(*) as count, SUM(call_count) as total_calls FROM api_call_stats GROUP BY status_group""" error_rows = _execute_raw(db, error_sql) error_distribution = {"success": 0, "has_errors": 0, "success_calls": 0, "error_calls": 0} for r in error_rows: if r["status_group"] == "success": error_distribution["success"] = r["count"] error_distribution["success_calls"] = r.get("total_calls", 0) else: error_distribution["has_errors"] = r["count"] error_distribution["error_calls"] = r.get("total_calls", 0) # Response time distribution latency_sql = """SELECT CASE WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 100 THEN 'fast' WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 500 THEN 'normal' WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 1000 THEN 'slow' ELSE 'very_slow' END as latency_bucket, COUNT(*) as count, SUM(CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) as avg_time FROM api_call_stats WHERE total_response_time IS NOT NULL AND call_count > 0 GROUP BY latency_bucket""" latency_rows = _execute_raw(db, latency_sql) latency_distribution = {"fast": 0, "normal": 0, "slow": 0, "very_slow": 0} latency_avg = {"fast": 0, "normal": 0, "slow": 0, "very_slow": 0} for r in latency_rows: bucket = r["latency_bucket"] latency_distribution[bucket] = r["count"] latency_avg[bucket] = round(r.get("avg_time", 0), 2) if r.get("avg_time") else 0 # API key usage distribution key_sql = """SELECT k.name, k.status, COUNT(s.id) as endpoint_count, COALESCE(SUM(s.call_count), 0) as total_calls, COALESCE(SUM(s.error_count), 0) as total_errors FROM api_keys k LEFT JOIN api_call_stats s ON s.endpoint LIKE CONCAT('%api-key%', k.id, '%') GROUP BY k.id, k.name, k.status ORDER BY total_calls DESC LIMIT 10""" key_rows = _execute_raw(db, key_sql) key_usage = [] for r in key_rows: key_usage.append({ "name": r.get("name", "Unknown"), "status": r.get("status", "unknown"), "total_calls": r.get("total_calls", 0), "total_errors": r.get("total_errors", 0), }) # Summary total_calls = sum(t["calls"] for t in trend) total_errors = sum(t["errors"] for t in trend) avg_response = sum(t["avg_time"] for t in trend) / len(trend) if trend else 0 error_rate = round(total_errors / total_calls * 100, 2) if total_calls > 0 else 0 return { "code": 0, "data": { "summary": { "total_calls": total_calls, "total_errors": total_errors, "error_rate": error_rate, "avg_response_time": round(avg_response, 2), "active_api_keys": error_distribution["success"] + error_distribution["has_errors"], }, "trend": trend, "top_endpoints": top_endpoints, "error_distribution": error_distribution, "latency_distribution": latency_distribution, "latency_avg": latency_avg, "key_usage": key_usage, } }