From 86ed48fff2067349ce7f89fce59f62be7e06deb9 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 10 Feb 2026 00:18:25 -0800 Subject: [PATCH] feat: wire observability-guide pages to persistent data --- data/observability-guides.json | 83 ++ src/app/api/observability-guide/route.ts | 101 +++ src/app/observability-guide/logging/page.tsx | 741 ++---------------- src/app/observability-guide/metrics/page.tsx | 757 +----------------- src/app/observability-guide/page.tsx | 686 +++-------------- src/app/observability-guide/tracing/page.tsx | 767 ++----------------- src/lib/observability-guides.ts | 102 +++ 7 files changed, 533 insertions(+), 2704 deletions(-) create mode 100644 data/observability-guides.json create mode 100644 src/app/api/observability-guide/route.ts create mode 100644 src/lib/observability-guides.ts diff --git a/data/observability-guides.json b/data/observability-guides.json new file mode 100644 index 00000000..130c6b80 --- /dev/null +++ b/data/observability-guides.json @@ -0,0 +1,83 @@ +[ + { + "id": "obs-logging-structured", + "title": "Adopt Structured Logging for Every Agent Action", + "content": "Emit JSON logs for prompts, tool calls, retries, and final outcomes so your team can query behavior reliably in production.", + "category": "logging", + "difficulty": "beginner", + "tags": ["json", "pino", "audit", "operations"], + "updatedAt": "2026-02-10T08:00:00.000Z" + }, + { + "id": "obs-logging-correlation", + "title": "Propagate Correlation IDs Across Requests", + "content": "Generate a request correlation ID at ingress and include it in every downstream log, metric label, and trace span to speed up incident triage.", + "category": "logging", + "difficulty": "intermediate", + "tags": ["correlation-id", "debugging", "incident-response"], + "updatedAt": "2026-02-10T08:05:00.000Z" + }, + { + "id": "obs-metrics-latency", + "title": "Track Latency Percentiles, Not Just Averages", + "content": "Measure p50/p95/p99 request latency for agent orchestration and tool calls to uncover tail latency and user-impacting slow paths.", + "category": "metrics", + "difficulty": "beginner", + "tags": ["prometheus", "latency", "slo"], + "updatedAt": "2026-02-10T08:10:00.000Z" + }, + { + "id": "obs-metrics-token-cost", + "title": "Monitor Token and Cost Burn Rate", + "content": "Capture prompt/completion token usage per model and operation, then alert when daily or hourly spend deviates from baseline.", + "category": "metrics", + "difficulty": "intermediate", + "tags": ["llm", "cost", "token-usage", "finops"], + "updatedAt": "2026-02-10T08:15:00.000Z" + }, + { + "id": "obs-tracing-root", + "title": "Create Root Spans for Every User Request", + "content": "Start one root span per incoming request and attach key metadata so each workflow can be replayed end-to-end across services.", + "category": "tracing", + "difficulty": "beginner", + "tags": ["opentelemetry", "spans", "request-flow"], + "updatedAt": "2026-02-10T08:20:00.000Z" + }, + { + "id": "obs-tracing-delegation", + "title": "Trace Multi-Agent Delegation Boundaries", + "content": "Inject and extract W3C trace context when one agent delegates to another so cross-agent bottlenecks are visible.", + "category": "tracing", + "difficulty": "advanced", + "tags": ["delegation", "tracecontext", "distributed-systems"], + "updatedAt": "2026-02-10T08:25:00.000Z" + }, + { + "id": "obs-alerting-error-budget", + "title": "Alert on Error Budget Burn, Not Raw Noise", + "content": "Use SLO-driven alerts based on error-budget burn rate to reduce pager fatigue and focus incidents on meaningful user impact.", + "category": "alerting", + "difficulty": "advanced", + "tags": ["slo", "error-budget", "on-call"], + "updatedAt": "2026-02-10T08:30:00.000Z" + }, + { + "id": "obs-alerting-runbooks", + "title": "Pair Alerts with Actionable Runbooks", + "content": "Every alert should include owner, probable causes, and remediation steps so responders can resolve incidents faster and consistently.", + "category": "alerting", + "difficulty": "intermediate", + "tags": ["runbooks", "incident-management", "reliability"], + "updatedAt": "2026-02-10T08:35:00.000Z" + }, + { + "id": "obs-metrics-quality", + "title": "Measure Agent Output Quality with Business KPIs", + "content": "Add task success, retry rate, and human override metrics so observability captures both technical health and user-facing quality.", + "category": "metrics", + "difficulty": "advanced", + "tags": ["kpi", "quality", "product-metrics"], + "updatedAt": "2026-02-10T08:40:00.000Z" + } +] diff --git a/src/app/api/observability-guide/route.ts b/src/app/api/observability-guide/route.ts new file mode 100644 index 00000000..8edd5f41 --- /dev/null +++ b/src/app/api/observability-guide/route.ts @@ -0,0 +1,101 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + filterObservabilityGuides, + normalizeObservabilityGuideCategory, + normalizeObservabilityGuideDifficulty, + readObservabilityGuides, + type ObservabilityGuide, + type ObservabilityGuideCategory, + type ObservabilityGuideDifficulty, + writeObservabilityGuides, +} from "@/lib/observability-guides"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +function createGuideId(): string { + const suffix = Math.random().toString(36).slice(2, 10); + return `observability-${Date.now()}-${suffix}`; +} + +export async function GET(request: NextRequest) { + try { + const categoryParam = request.nextUrl.searchParams.get("category"); + const search = request.nextUrl.searchParams.get("search"); + + if (categoryParam && !normalizeObservabilityGuideCategory(categoryParam)) { + return NextResponse.json( + { error: "Invalid category. Use logging, metrics, tracing, or alerting." }, + { status: 400 } + ); + } + + const guides = await readObservabilityGuides(); + const filteredGuides = filterObservabilityGuides(guides, { + category: categoryParam, + search, + }); + + return NextResponse.json( + { + guides: filteredGuides, + total: filteredGuides.length, + }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); + } catch (error) { + console.error("Failed to load observability guides", error); + return NextResponse.json({ error: "Failed to load observability guides" }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const body = (await request.json()) as Record; + + const title = typeof body.title === "string" ? body.title.trim() : ""; + const content = typeof body.content === "string" ? body.content.trim() : ""; + const categoryRaw = typeof body.category === "string" ? body.category : ""; + const difficultyRaw = typeof body.difficulty === "string" ? body.difficulty : ""; + const tags = Array.isArray(body.tags) + ? body.tags.filter((tag): tag is string => typeof tag === "string" && tag.trim().length > 0) + : []; + + const category = normalizeObservabilityGuideCategory(categoryRaw) as ObservabilityGuideCategory | null; + const difficulty = normalizeObservabilityGuideDifficulty(difficultyRaw) as ObservabilityGuideDifficulty | null; + + if (!title || !content || !category || !difficulty) { + return NextResponse.json( + { + error: + "title, content, category (logging|metrics|tracing|alerting), and difficulty (beginner|intermediate|advanced) are required", + }, + { status: 400 } + ); + } + + const guides = await readObservabilityGuides(); + + const newGuide: ObservabilityGuide = { + id: createGuideId(), + title, + content, + category, + difficulty, + tags, + updatedAt: new Date().toISOString(), + }; + + const updatedGuides = [...guides, newGuide]; + await writeObservabilityGuides(updatedGuides); + + return NextResponse.json({ guide: newGuide }, { status: 201 }); + } catch (error) { + console.error("Failed to create observability guide", error); + return NextResponse.json({ error: "Failed to create observability guide" }, { status: 500 }); + } +} diff --git a/src/app/observability-guide/logging/page.tsx b/src/app/observability-guide/logging/page.tsx index 1572527c..bb3d5ee1 100644 --- a/src/app/observability-guide/logging/page.tsx +++ b/src/app/observability-guide/logging/page.tsx @@ -1,714 +1,59 @@ -"use client"; +/* eslint-disable react/no-unescaped-entities */ -import { useState } from "react"; import Link from "next/link"; -import { FileText, Info, AlertTriangle, XOctagon } from "lucide-react"; +import { FileText } from "lucide-react"; +import { readObservabilityGuides, type ObservabilityGuideDifficulty } from "@/lib/observability-guides"; -interface LogExample { - title: string; - description: string; - level: "debug" | "info" | "warn" | "error"; - good: string; - bad?: string; -} - -const LOG_EXAMPLES: LogExample[] = [ - { - title: "Tool Call Execution", - description: "Log every tool invocation with parameters, duration, and result status", - level: "info", - good: `logger.info({ - correlationId: "req-abc123", - userId: "user-456", - action: "tool_call", - tool: "web_search", - params: { query: "best practices", maxResults: 5 }, - durationMs: 234, - status: "success", - resultSize: 1024, -}, "Tool call completed");`, - bad: `console.log("web search done");`, - }, - { - title: "Agent Decision Point", - description: "Log reasoning steps and why specific tools or approaches were chosen", - level: "info", - good: `logger.info({ - correlationId: "req-abc123", - action: "reasoning", - thought: "User wants weather, need location first", - selectedTool: "get_location", - alternatives: ["web_search", "ask_user"], - confidence: 0.92, -}, "Agent decision");`, - bad: `console.log("Choosing get_location tool");`, - }, - { - title: "Error Handling", - description: "Capture full error context including stack, retry attempts, and impact", - level: "error", - good: `logger.error({ - correlationId: "req-abc123", - action: "tool_call", - tool: "database_query", - error: { - name: error.name, - message: error.message, - stack: error.stack, - code: error.code, - }, - retryAttempt: 2, - maxRetries: 3, - userImpact: "Request failed, fallback to cached data", -}, "Tool execution failed");`, - bad: `console.error("DB query failed:", error);`, - }, - { - title: "Context Switch", - description: "Log when agent changes conversation context or workspace", - level: "info", - good: `logger.info({ - correlationId: "req-abc123", - action: "context_switch", - from: { workspace: "/project-a", sessionId: "sess-111" }, - to: { workspace: "/project-b", sessionId: "sess-222" }, - reason: "User explicitly requested project switch", - preservedContext: ["user_preferences", "auth_token"], -}, "Context switched");`, - bad: `console.log("Switched to project-b");`, - }, - { - title: "Token Usage", - description: "Track LLM token consumption for cost and performance analysis", - level: "info", - good: `logger.info({ - correlationId: "req-abc123", - action: "llm_call", - model: "gpt-4-turbo", - operation: "completion", - tokens: { - prompt: 1234, - completion: 567, - total: 1801, - }, - cost: 0.0234, // USD - durationMs: 1820, - cached: false, -}, "LLM request completed");`, - bad: `console.log("GPT-4 call done");`, - }, - { - title: "Security Event", - description: "Always log potential security violations for audit trails", - level: "warn", - good: `logger.warn({ - correlationId: "req-abc123", - userId: "user-789", - action: "security_violation", - type: "prompt_injection_detected", - input: sanitizedInput.substring(0, 200), // Truncated for privacy - patterns: ["ignore previous instructions"], - blocked: true, - severity: "high", -}, "Potential prompt injection blocked");`, - bad: `console.log("Blocked suspicious input");`, - }, -]; - -const LOG_LEVELS = [ - { - level: "DEBUG", - icon: Info, - color: "text-gray-600 bg-gray-100 border-gray-300", - when: "Development/troubleshooting only", - examples: [ - "Variable state changes", - "Loop iterations", - "Detailed function entry/exit", - "Interim computation results", - ], - production: false, - }, - { - level: "INFO", - icon: Info, - color: "text-blue-600 bg-blue-100 border-blue-300", - when: "Normal operations worth recording", - examples: [ - "Tool calls started/completed", - "Agent decisions and reasoning", - "Context switches", - "Session start/end", - ], - production: true, - }, - { - level: "WARN", - icon: AlertTriangle, - color: "text-yellow-600 bg-yellow-100 border-yellow-300", - when: "Unexpected but recoverable situations", - examples: [ - "Retry attempts after transient failures", - "Deprecated API usage", - "Approaching rate limits", - "Potential security issues (blocked)", - ], - production: true, - }, - { - level: "ERROR", - icon: XOctagon, - color: "text-red-600 bg-red-100 border-red-300", - when: "Failures that prevent operation completion", - examples: [ - "Unhandled exceptions", - "Tool execution failures", - "External API errors", - "Data validation failures", - ], - production: true, - }, -]; - -const CORRELATION_STRATEGIES = [ - { - strategy: "Request ID Propagation", - description: "Generate a unique ID for each user request and propagate it through all logs, traces, and metrics", - code: `import { randomUUID } from 'crypto'; -import { AsyncLocalStorage } from 'async_hooks'; - -const asyncLocalStorage = new AsyncLocalStorage(); - -// Middleware to create correlation context -app.use((req, res, next) => { - const correlationId = req.headers['x-correlation-id'] || randomUUID(); - asyncLocalStorage.run({ correlationId }, () => { - req.correlationId = correlationId; - res.setHeader('x-correlation-id', correlationId); - next(); - }); -}); - -// Logger automatically includes correlation ID -function createLogger() { - return { - info: (data, msg) => { - const context = asyncLocalStorage.getStore(); - console.log(JSON.stringify({ - ...data, - correlationId: context?.correlationId, - timestamp: new Date().toISOString(), - level: 'info', - message: msg, - })); - }, - }; -}`, - }, - { - strategy: "Trace Context Injection", - description: "Use OpenTelemetry to automatically inject trace IDs into logs for correlation", - code: `import { trace } from '@opentelemetry/api'; -import pino from 'pino'; - -const logger = pino({ - mixin: () => { - const span = trace.getActiveSpan(); - if (!span) return {}; - - const spanContext = span.spanContext(); - return { - traceId: spanContext.traceId, - spanId: spanContext.spanId, - traceFlags: spanContext.traceFlags, - }; - }, -}); - -// Now all logs automatically include trace/span IDs -logger.info({ action: 'tool_call', tool: 'search' }, 'Executing tool'); -// Output includes: { ..., traceId: "abc123...", spanId: "def456..." }`, - }, - { - strategy: "Hierarchical Context", - description: "Maintain parent-child relationships in logs for multi-agent or nested operations", - code: `class ExecutionContext { - constructor(parentId = null) { - this.id = randomUUID(); - this.parentId = parentId; - this.startTime = Date.now(); - } - - createChild() { - return new ExecutionContext(this.id); - } - - toLogContext() { - return { - executionId: this.id, - parentExecutionId: this.parentId, - executionDepth: this.getDepth(), - executionDurationMs: Date.now() - this.startTime, - }; - } -} - -// Usage -const rootCtx = new ExecutionContext(); -logger.info({ ...rootCtx.toLogContext(), action: 'main_task' }, 'Starting task'); - -const childCtx = rootCtx.createChild(); -logger.info({ ...childCtx.toLogContext(), action: 'sub_task' }, 'Delegated to sub-agent');`, - }, -]; - -const LOG_AGGREGATION = [ - { - name: "Structured Format", - description: "Always use JSON for logs. Never plain text.", - example: { - good: `{ "timestamp": "2026-02-09T21:00:00Z", "level": "info", "correlationId": "req-123", "action": "tool_call", "tool": "search", "durationMs": 234 }`, - bad: `[INFO] 2026-02-09 21:00:00 - Tool search completed in 234ms`, - }, - why: "JSON logs are machine-parseable, filterable, and can be queried in log aggregation tools. Plain text requires regex parsing.", - }, - { - name: "Standard Fields", - description: "Use consistent field names across all logs", - fields: [ - { name: "timestamp", type: "ISO8601 string", required: true }, - { name: "level", type: "debug|info|warn|error", required: true }, - { name: "correlationId", type: "string (UUID)", required: true }, - { name: "action", type: "string", required: false, description: "What happened (tool_call, reasoning, etc.)" }, - { name: "userId", type: "string", required: false }, - { name: "sessionId", type: "string", required: false }, - { name: "message", type: "string", required: true }, - ], - }, - { - name: "Sampling & Volume Control", - description: "High-volume logs (like debug) should be sampled in production", - code: `const shouldSample = (level: string, sampleRate: number) => { - if (level === 'error') return true; // Always log errors - if (level === 'debug' && process.env.NODE_ENV === 'production') { - return Math.random() < sampleRate; // Sample 1% of debug logs - } - return true; -}; - -logger.debug = (data, msg) => { - if (!shouldSample('debug', 0.01)) return; - // ... log normally -};`, - }, -]; - -const BEST_PRACTICES = [ - { - practice: "Log at Boundaries", - description: "Always log when crossing system boundaries (API calls, database queries, external tool invocations)", - why: "These are the most common failure points and debugging blind spots.", - }, - { - practice: "Avoid Logging Secrets", - description: "Never log API keys, passwords, tokens, or PII. Redact sensitive fields.", - code: `const redact = (obj: any) => { - const sensitive = ['password', 'apiKey', 'token', 'secret', 'ssn', 'creditCard']; - const redacted = { ...obj }; - for (const key of Object.keys(redacted)) { - if (sensitive.some(s => key.toLowerCase().includes(s))) { - redacted[key] = '[REDACTED]'; - } - } - return redacted; +const difficultyStyles: Record = { + beginner: "bg-emerald-100 text-emerald-800 border-emerald-300", + intermediate: "bg-amber-100 text-amber-800 border-amber-300", + advanced: "bg-rose-100 text-rose-800 border-rose-300", }; -logger.info(redact(userData), "User authenticated");`, - }, - { - practice: "Include Error Context", - description: "When logging errors, include the full context needed to reproduce the issue", - code: `try { - const result = await tool.execute(params); -} catch (error) { - logger.error({ - correlationId, - action: 'tool_execution', - tool: tool.name, - params: redact(params), // Params that caused the error - error: { - name: error.name, - message: error.message, - stack: error.stack, - }, - retryable: isRetryable(error), - userImpact: 'Request failed', - }, 'Tool execution failed'); - throw error; -}`, - }, - { - practice: "Use Log Levels Consistently", - description: "Don't use ERROR for warnings or INFO for errors. Be consistent across your codebase.", - why: "Alerts and dashboards depend on accurate log levels. Mis-leveled logs cause alert fatigue or missed incidents.", - }, - { - practice: "Make Logs Searchable", - description: "Use consistent field names and values that you can query in log aggregation tools", - examples: [ - "action: 'tool_call' (not 'executing tool', 'tool exec', 'calling tool')", - "status: 'success'|'failure' (not 'ok', 'done', 'error')", - ], - }, -]; - -export default function LoggingPage() { - const [selectedLevel, setSelectedLevel] = useState("all"); - - const filteredExamples = - selectedLevel === "all" ? LOG_EXAMPLES : LOG_EXAMPLES.filter((ex) => ex.level === selectedLevel); +export default async function LoggingGuidePage() { + const guides = (await readObservabilityGuides()).filter((guide) => guide.category === "logging"); return ( -
- {/* Header */} -
- - ← Back to Observability Guide - -
- -

Logging Best Practices

-
-

- Structured logging strategies for AI agents. Learn what to log, how to format it, and how to make logs - actionable. -

+
+ + ← Back to Observability Guide + + +
+ +

Logging for Agent Systems

- - {/* What to Log */} -
-

What to Log in AI Agents

-
-

- Golden Rule: Log everything needed to answer these questions: -

-
    -
  • - - What did the agent decide to do? -
  • -
  • - - Why did it make that decision? -
  • -
  • - - Which tools were called and with what parameters? -
  • -
  • - - What was the outcome (success, failure, partial)? -
  • -
  • - - How long did each operation take? -
  • -
  • - - What was the cost (tokens, API calls)? -
  • -
-
- -
-
-

Log Examples by Category

-
- - - - +

+ Persistent, category-filtered guidance for structured logging, auditability, and rapid incident debugging. +

+ +
+ {guides.map((guide) => ( +
+
+

{guide.title}

+ + {guide.difficulty} +
-
- -
- {filteredExamples.map((example) => ( -
-
-

{example.title}

- - {example.level} - -
-

{example.description}

- -
- {example.bad && ( -
-
❌ Bad:
-
-                        {example.bad}
-                      
-
- )} -
-
✅ Good:
-
-                      {example.good}
-                    
-
-
-
- ))} -
-
-
- - {/* Log Levels */} -
-

Log Levels: When to Use Each

-
- {LOG_LEVELS.map((level) => { - const Icon = level.icon; - return ( -
-
- -
-
-

{level.level}

- {level.production ? ( - - Production enabled - - ) : ( - Dev only - )} -
-

- When to use: {level.when} -

-
-

Examples:

-
    - {level.examples.map((ex) => ( -
  • - - {ex} -
  • - ))} -
-
-
-
-
- ); - })} -
-
- - {/* Correlation IDs */} -
-

- Correlation IDs: Tracing Requests End-to-End -

-
-

- Why correlation IDs matter: Without them, you can't answer "What happened during this specific - user request?" when you have thousands of concurrent requests. -

-

- A correlation ID (or request ID) is a unique identifier that connects all logs, metrics, and traces for a - single user request, even across multiple services or agents. -

-
- -
- {CORRELATION_STRATEGIES.map((strategy) => ( -
-

{strategy.strategy}

-

{strategy.description}

-
-                {strategy.code}
-              
+

{guide.content}

+
+ {guide.tags.map((tag) => ( + + #{tag} + + ))}
- ))} -
-
- - {/* Log Aggregation */} -
-

Log Aggregation & Structured Formats

- - {LOG_AGGREGATION.map((topic, idx) => ( -
0 ? "mt-8" : ""}`}> -

{topic.name}

-

{topic.description}

- - {topic.example && ( -
-
-
✅ Good (JSON):
-
-                    {topic.example.good}
-                  
-
-
-
❌ Bad (Plain text):
-
-                    {topic.example.bad}
-                  
-
- {topic.why && ( -
-

- Why: {topic.why} -

-
- )} -
- )} - - {topic.fields && ( -
- - - - - - - - - - - {topic.fields.map((field) => ( - - - - - - - ))} - -
FieldTypeRequiredDescription
{field.name}{field.type} - {field.required ? ( - Yes - ) : ( - No - )} - {field.description || "—"}
-
- )} - - {topic.code && ( -
-
-                  {topic.code}
-                
-
- )} -
+

Updated: {new Date(guide.updatedAt).toLocaleString()}

+ ))} -
- - {/* Best Practices */} -
-

Best Practices Checklist

-
- {BEST_PRACTICES.map((item) => ( -
-

{item.practice}

-

{item.description}

- {item.why && ( -
-

- Why: {item.why} -

-
- )} - {item.code && ( -
-                  {item.code}
-                
- )} - {item.examples && ( -
-

Examples:

-
    - {item.examples.map((ex) => ( -
  • - - {ex} -
  • - ))} -
-
- )} -
- ))} -
-
+
- {/* Related Guides */} -
-

Related Guides

-
- -

📊 Metrics & Dashboards

-

- Track agent performance with key metrics, instrumentation patterns, and alerting -

- - -

🔍 Distributed Tracing

-

- Trace multi-agent workflows with OpenTelemetry and span instrumentation -

- + {guides.length === 0 && ( +
+ No logging guides found.
-
+ )}
); } diff --git a/src/app/observability-guide/metrics/page.tsx b/src/app/observability-guide/metrics/page.tsx index df597a13..101b4d8c 100644 --- a/src/app/observability-guide/metrics/page.tsx +++ b/src/app/observability-guide/metrics/page.tsx @@ -1,732 +1,59 @@ -"use client"; +/* eslint-disable react/no-unescaped-entities */ -import { useState } from "react"; import Link from "next/link"; import { LineChart } from "lucide-react"; +import { readObservabilityGuides, type ObservabilityGuideDifficulty } from "@/lib/observability-guides"; -interface Metric { - name: string; - category: "performance" | "cost" | "reliability" | "business"; - description: string; - type: "counter" | "gauge" | "histogram"; - labels?: string[]; - alertThreshold?: string; - code: string; -} - -const KEY_METRICS: Metric[] = [ - { - name: "agent_requests_total", - category: "performance", - description: "Total number of agent requests (success + failure)", - type: "counter", - labels: ["status", "userId", "agentType"], - code: `import { Counter } from 'prom-client'; - -const requestCounter = new Counter({ - name: 'agent_requests_total', - help: 'Total agent requests', - labelNames: ['status', 'userId', 'agentType'], -}); - -// Instrument -requestCounter.inc({ status: 'success', userId: 'user-123', agentType: 'assistant' });`, - }, - { - name: "agent_request_duration_seconds", - category: "performance", - description: "Agent request latency (p50, p95, p99)", - type: "histogram", - labels: ["agentType", "operation"], - alertThreshold: "p95 > 5s", - code: `import { Histogram } from 'prom-client'; - -const requestDuration = new Histogram({ - name: 'agent_request_duration_seconds', - help: 'Agent request latency', - labelNames: ['agentType', 'operation'], - buckets: [0.1, 0.5, 1, 2, 5, 10, 30], // seconds -}); - -// Instrument -const start = Date.now(); -const result = await agent.execute(request); -requestDuration.observe( - { agentType: 'assistant', operation: 'chat' }, - (Date.now() - start) / 1000 -);`, - }, - { - name: "agent_llm_tokens_total", - category: "cost", - description: "Total LLM tokens consumed (prompt + completion)", - type: "counter", - labels: ["model", "operation", "cached"], - code: `const tokenCounter = new Counter({ - name: 'agent_llm_tokens_total', - help: 'Total LLM tokens consumed', - labelNames: ['model', 'operation', 'cached'], -}); - -// After LLM call -tokenCounter.inc( - { model: 'gpt-4-turbo', operation: 'completion', cached: 'false' }, - response.usage.total_tokens -);`, - }, - { - name: "agent_llm_cost_usd", - category: "cost", - description: "Estimated LLM cost in USD", - type: "counter", - labels: ["model", "userId"], - alertThreshold: "Daily cost > $100", - code: `const costCounter = new Counter({ - name: 'agent_llm_cost_usd', - help: 'Estimated LLM cost', - labelNames: ['model', 'userId'], -}); - -// Calculate cost based on token pricing -const cost = calculateCost(response.usage, 'gpt-4-turbo'); -costCounter.inc({ model: 'gpt-4-turbo', userId: 'user-123' }, cost);`, - }, - { - name: "agent_tool_calls_total", - category: "performance", - description: "Tool invocations by tool name and status", - type: "counter", - labels: ["tool", "status"], - code: `const toolCallCounter = new Counter({ - name: 'agent_tool_calls_total', - help: 'Tool invocations', - labelNames: ['tool', 'status'], -}); - -try { - await tools[toolName].execute(params); - toolCallCounter.inc({ tool: toolName, status: 'success' }); -} catch (error) { - toolCallCounter.inc({ tool: toolName, status: 'failure' }); - throw error; -}`, - }, - { - name: "agent_tool_duration_seconds", - category: "performance", - description: "Tool execution latency", - type: "histogram", - labels: ["tool"], - alertThreshold: "p99 > 10s", - code: `const toolDuration = new Histogram({ - name: 'agent_tool_duration_seconds', - help: 'Tool execution latency', - labelNames: ['tool'], - buckets: [0.05, 0.1, 0.5, 1, 2, 5, 10], -}); - -const start = Date.now(); -const result = await tools[toolName].execute(params); -toolDuration.observe({ tool: toolName }, (Date.now() - start) / 1000);`, - }, - { - name: "agent_errors_total", - category: "reliability", - description: "Total errors by type and severity", - type: "counter", - labels: ["errorType", "severity", "recoverable"], - alertThreshold: "Error rate > 5%", - code: `const errorCounter = new Counter({ - name: 'agent_errors_total', - help: 'Total errors', - labelNames: ['errorType', 'severity', 'recoverable'], -}); - -catch (error) { - errorCounter.inc({ - errorType: error.name, - severity: getSeverity(error), - recoverable: isRetryable(error) ? 'true' : 'false', - }); -}`, - }, - { - name: "agent_queue_depth", - category: "performance", - description: "Current number of queued agent requests", - type: "gauge", - labels: ["priority"], - alertThreshold: "Queue depth > 100", - code: `import { Gauge } from 'prom-client'; - -const queueDepth = new Gauge({ - name: 'agent_queue_depth', - help: 'Queued agent requests', - labelNames: ['priority'], -}); - -// Update as queue changes -queueDepth.set({ priority: 'high' }, highPriorityQueue.length); -queueDepth.set({ priority: 'normal' }, normalPriorityQueue.length);`, - }, - { - name: "agent_success_rate", - category: "reliability", - description: "Percentage of successful requests (business metric)", - type: "gauge", - labels: ["agentType"], - alertThreshold: "Success rate < 95%", - code: `// Derived from agent_requests_total -// Calculate in your monitoring tool (Prometheus, Datadog, etc.) -// Prometheus query: -// rate(agent_requests_total{status="success"}[5m]) / -// rate(agent_requests_total[5m])`, - }, - { - name: "agent_context_switches_total", - category: "business", - description: "Number of times agent switches context (workspace, project, etc.)", - type: "counter", - labels: ["fromContext", "toContext"], - code: `const contextSwitchCounter = new Counter({ - name: 'agent_context_switches_total', - help: 'Context switches', - labelNames: ['fromContext', 'toContext'], -}); - -contextSwitchCounter.inc({ - fromContext: 'project-a', - toContext: 'project-b', -});`, - }, -]; - -const DASHBOARD_PATTERNS = [ - { - name: "Agent Health Overview", - description: "Single-pane view of overall agent health", - panels: [ - { metric: "Request Rate", query: "rate(agent_requests_total[5m])", viz: "Line chart" }, - { metric: "Success Rate", query: "success / total requests", viz: "Single stat (95%+)" }, - { metric: "Error Rate", query: "rate(agent_errors_total[5m])", viz: "Line chart (alert if > 5%)" }, - { metric: "p95 Latency", query: "histogram_quantile(0.95, agent_request_duration_seconds)", viz: "Line chart" }, - { metric: "Queue Depth", query: "agent_queue_depth", viz: "Gauge" }, - { metric: "Active Users", query: "count(distinct userId)", viz: "Single stat" }, - ], - }, - { - name: "Cost & Token Usage", - description: "Track LLM costs and optimize spend", - panels: [ - { metric: "Daily Cost", query: "increase(agent_llm_cost_usd[1d])", viz: "Bar chart by model" }, - { metric: "Tokens/Request", query: "avg(tokens) by model", viz: "Line chart" }, - { metric: "Cost per User", query: "sum(cost) by userId", viz: "Table (top 10)" }, - { metric: "Cache Hit Rate", query: "cached / total tokens", viz: "Single stat" }, - { metric: "Model Distribution", query: "count by model", viz: "Pie chart" }, - ], - }, - { - name: "Tool Performance", - description: "Analyze tool usage and bottlenecks", - panels: [ - { metric: "Tool Call Volume", query: "rate(agent_tool_calls_total[5m]) by tool", viz: "Stacked area" }, - { metric: "Tool Latency", query: "histogram_quantile(0.95, tool_duration) by tool", viz: "Bar chart" }, - { metric: "Tool Error Rate", query: "errors / total by tool", viz: "Heatmap" }, - { metric: "Slowest Tools", query: "topk(5, p95 latency by tool)", viz: "Table" }, - ], - }, - { - name: "User Experience", - description: "User-centric metrics for product teams", - panels: [ - { metric: "Session Duration", query: "avg(session_duration_seconds)", viz: "Line chart" }, - { metric: "Messages per Session", query: "avg(messages)", viz: "Histogram" }, - { metric: "User Satisfaction", query: "thumbs_up / (thumbs_up + thumbs_down)", viz: "Single stat" }, - { metric: "Retry Rate", query: "retries / total requests", viz: "Line chart" }, - ], - }, -]; - -const ALERT_RULES = [ - { - name: "High Error Rate", - severity: "critical", - condition: "Error rate > 5% for 5 minutes", - query: "rate(agent_errors_total[5m]) / rate(agent_requests_total[5m]) > 0.05", - action: "Page on-call engineer", - why: "Users are experiencing failures. Immediate investigation required.", - }, - { - name: "Slow Response Time", - severity: "warning", - condition: "p95 latency > 5 seconds for 10 minutes", - query: "histogram_quantile(0.95, rate(agent_request_duration_seconds_bucket[5m])) > 5", - action: "Notify team Slack channel", - why: "Degraded user experience. May indicate scaling issues or slow LLM responses.", - }, - { - name: "Queue Buildup", - severity: "warning", - condition: "Queue depth > 100 for 5 minutes", - query: "agent_queue_depth > 100", - action: "Auto-scale workers or notify ops team", - why: "Requests are backing up. May need to add capacity or shed load.", - }, - { - name: "Cost Spike", - severity: "warning", - condition: "Hourly cost > 2x baseline", - query: "increase(agent_llm_cost_usd[1h]) > 2 * avg_over_time(increase(agent_llm_cost_usd[1h])[7d])", - action: "Notify finance/eng leads", - why: "Unexpected cost increase. Could indicate a runaway agent, attack, or usage spike.", - }, - { - name: "Tool Failure Spike", - severity: "high", - condition: "Tool error rate > 10% for any tool", - query: "rate(agent_tool_calls_total{status='failure'}[5m]) / rate(agent_tool_calls_total[5m]) > 0.10", - action: "Alert on-call, investigate tool health", - why: "A critical tool is failing. May cascade to agent failures.", - }, - { - name: "Zero Traffic", - severity: "critical", - condition: "No requests for 5 minutes", - query: "rate(agent_requests_total[5m]) == 0", - action: "Page on-call (potential outage)", - why: "Complete service outage. Users cannot reach the agent.", - }, -]; - -const INSTRUMENTATION_CODE = { - expressMiddleware: `import express from 'express'; -import { register, collectDefaultMetrics } from 'prom-client'; - -collectDefaultMetrics(); // CPU, memory, etc. - -const app = express(); - -// Metrics endpoint -app.get('/metrics', async (req, res) => { - res.set('Content-Type', register.contentType); - res.end(await register.metrics()); -}); - -// Request metrics middleware -app.use((req, res, next) => { - const start = Date.now(); - res.on('finish', () => { - const duration = (Date.now() - start) / 1000; - requestDuration.observe( - { method: req.method, route: req.route?.path, status: res.statusCode }, - duration - ); - requestCounter.inc({ method: req.method, status: res.statusCode }); - }); - next(); -});`, - - llmInstrumentation: `async function callLLM(prompt: string, model: string) { - const start = Date.now(); - const operationId = randomUUID(); - - try { - const response = await openai.chat.completions.create({ - model, - messages: [{ role: 'user', content: prompt }], - }); - - // Record metrics - const durationSec = (Date.now() - start) / 1000; - llmLatency.observe({ model, operation: 'completion' }, durationSec); - - const tokens = response.usage; - tokenCounter.inc({ model, type: 'prompt' }, tokens.prompt_tokens); - tokenCounter.inc({ model, type: 'completion' }, tokens.completion_tokens); - - const cost = calculateCost(tokens, model); - costCounter.inc({ model }, cost); - - llmCallCounter.inc({ model, status: 'success' }); - - return response; - } catch (error) { - llmCallCounter.inc({ model, status: 'failure' }); - throw error; - } -}`, - - customBusinessMetrics: `// Track business-specific metrics -const userFeedbackGauge = new Gauge({ - name: 'agent_user_satisfaction', - help: 'User satisfaction score (1-5)', - labelNames: ['agentType'], -}); - -const sessionLengthHistogram = new Histogram({ - name: 'agent_session_duration_seconds', - help: 'User session duration', - buckets: [60, 300, 600, 1800, 3600], // 1min to 1hr -}); - -// Instrument -userFeedbackGauge.set({ agentType: 'assistant' }, averageRating); -sessionLengthHistogram.observe(sessionDurationSec);`, +const difficultyStyles: Record = { + beginner: "bg-emerald-100 text-emerald-800 border-emerald-300", + intermediate: "bg-amber-100 text-amber-800 border-amber-300", + advanced: "bg-rose-100 text-rose-800 border-rose-300", }; -export default function MetricsPage() { - const [selectedCategory, setSelectedCategory] = useState("all"); - - const filteredMetrics = - selectedCategory === "all" ? KEY_METRICS : KEY_METRICS.filter((m) => m.category === selectedCategory); +export default async function MetricsGuidePage() { + const guides = (await readObservabilityGuides()).filter((guide) => guide.category === "metrics"); return ( -
- {/* Header */} -
- - ← Back to Observability Guide - -
- -

Metrics & Dashboards

-
-

- Key performance indicators for AI agents. Learn what to measure, how to instrument, and how to build - actionable dashboards. -

+
+ + ← Back to Observability Guide + + +
+ +

Metrics for Agent Reliability and Cost

- - {/* Why Metrics Matter */} -
-

Why Metrics Matter

-
-

- Logs tell you what happened. Metrics tell you how often, how - fast, and how much. -

-
-
-

Metrics enable:

-
    -
  • • Real-time dashboards and alerts
  • -
  • • Performance regression detection
  • -
  • • Capacity planning and autoscaling
  • -
  • • Cost tracking and optimization
  • -
  • • SLA/SLO compliance monitoring
  • -
+

+ Persistent, category-filtered guidance for latency, success rate, queue depth, and token economics. +

+ +
+ {guides.map((guide) => ( +
+
+

{guide.title}

+ + {guide.difficulty} +
-
-

Without metrics, you can't answer:

-
    -
  • • Is the agent slower than yesterday?
  • -
  • • Are we within budget for LLM costs?
  • -
  • • Which tool is the bottleneck?
  • -
  • • Can we handle 10x traffic?
  • -
-
-
-
-
- - {/* Metric Types */} -
-

Metric Types

-
-
-

Counter

-

- Monotonically increasing value. Only goes up (or resets to 0). -

-

- Examples: Total requests, total errors, total tokens -

-

- Query: rate(counter[5m]) for per-second rate -

-
-
-

Gauge

-

Current value that can go up or down.

-

- Examples: Queue depth, active users, memory usage -

-

- Query: gauge (current value) -

-
-
-

Histogram

-

- Distribution of values, calculates percentiles (p50, p95, p99). -

-

- Examples: Request latency, tool duration, token count per request -

-

- Query: histogram_quantile(0.95, ...) -

-
-
-
- - {/* Key Metrics */} -
-
-

Key Agent Metrics

-
- - - - - -
-
- -
- {filteredMetrics.map((metric) => ( -
-
-
-

{metric.name}

-

{metric.description}

-
-
- - {metric.type} - - - {metric.category} - -
-
- - {metric.labels && ( -
- Labels: - {metric.labels.join(", ")} -
- )} - - {metric.alertThreshold && ( -
- ⚠️ Alert Threshold: - {metric.alertThreshold} -
- )} - -
-                {metric.code}
-              
-
- ))} -
-
- - {/* Dashboard Patterns */} -
-

Dashboard Design Patterns

-
- {DASHBOARD_PATTERNS.map((dashboard) => ( -
-

{dashboard.name}

-

{dashboard.description}

-
- - - - - - - - - - {dashboard.panels.map((panel, idx) => ( - - - - - - ))} - -
PanelQuery/MetricVisualization
{panel.metric}{panel.query}{panel.viz}
-
-
- ))} -
-
- - {/* Alert Rules */} -
-

Alerting Best Practices

-
-

- Golden Rule: Only alert on symptoms users care about (errors, latency, downtime). -

-

- Don't alert on causes (high CPU, memory) unless they directly impact user experience. -

-
- -
- {ALERT_RULES.map((rule) => ( -
-
-
-

{rule.name}

-

{rule.condition}

-
- - {rule.severity} +

{guide.content}

+
+ {guide.tags.map((tag) => ( + + #{tag} -
- -
- Prometheus Query: -
-                  {rule.query}
-                
-
- -
-
- Action: -

{rule.action}

-
-
- Why it matters: -

{rule.why}

-
-
+ ))}
- ))} -
-
- - {/* Instrumentation Code */} -
-

Instrumentation Code Examples

- -
-
-

Express.js Middleware (Automatic Instrumentation)

-
-              {INSTRUMENTATION_CODE.expressMiddleware}
-            
-
- -
-

- LLM Call Instrumentation -

-
-              {INSTRUMENTATION_CODE.llmInstrumentation}
-            
-
- -
-

Custom Business Metrics

-
-              {INSTRUMENTATION_CODE.customBusinessMetrics}
-            
-
-
-
+

Updated: {new Date(guide.updatedAt).toLocaleString()}

+ + ))} +
- {/* Related Guides */} -
-

Related Guides

-
- -

📝 Logging Best Practices

-

- Structured logging, correlation IDs, and what to log for debugging -

- - -

🔍 Distributed Tracing

-

- Trace multi-agent workflows with OpenTelemetry and span instrumentation -

- + {guides.length === 0 && ( +
+ No metrics guides found.
-
+ )}
); } diff --git a/src/app/observability-guide/page.tsx b/src/app/observability-guide/page.tsx index 98e679ee..41163f72 100644 --- a/src/app/observability-guide/page.tsx +++ b/src/app/observability-guide/page.tsx @@ -1,608 +1,160 @@ -"use client"; +/* eslint-disable react/no-unescaped-entities */ -import { useState } from "react"; import Link from "next/link"; -import { Eye, CheckCircle, XCircle } from "lucide-react"; - -interface ChecklistItem { - id: string; - title: string; - priority: "critical" | "high" | "medium" | "low"; - category: string; - description: string; - guide: string; -} - -const CHECKLIST: ChecklistItem[] = [ - { - id: "structured-logging", - title: "Structured Logging", - priority: "critical", - category: "Logging", - description: "Implement JSON-formatted logs with correlation IDs for all agent decisions and tool calls", - guide: "/observability-guide/logging", - }, - { - id: "error-tracking", - title: "Error Tracking & Alerts", - priority: "critical", - category: "Metrics", - description: "Track error rates, failures, and set up alerts for degraded agent performance", - guide: "/observability-guide/metrics", - }, - { - id: "distributed-tracing", - title: "Distributed Tracing", - priority: "high", - category: "Tracing", - description: "Instrument multi-agent workflows with OpenTelemetry for end-to-end visibility", - guide: "/observability-guide/tracing", - }, - { - id: "token-usage-metrics", - title: "Token Usage Tracking", - priority: "high", - category: "Metrics", - description: "Monitor token consumption, costs, and model latency across all LLM calls", - guide: "/observability-guide/metrics#token-tracking", - }, - { - id: "correlation-ids", - title: "Request Correlation", - priority: "high", - category: "Logging", - description: "Propagate correlation IDs through all log statements for request tracing", - guide: "/observability-guide/logging#correlation", - }, - { - id: "decision-logging", - title: "Decision Audit Trail", - priority: "medium", - category: "Logging", - description: "Log all agent reasoning steps, tool selections, and parameter choices", - guide: "/observability-guide/logging#decisions", - }, - { - id: "latency-percentiles", - title: "Latency Percentiles", - priority: "medium", - category: "Metrics", - description: "Track p50, p95, p99 latencies for agent responses and tool executions", - guide: "/observability-guide/metrics#latency", - }, - { - id: "span-instrumentation", - title: "Span Instrumentation", - priority: "medium", - category: "Tracing", - description: "Create spans for all tool calls, LLM requests, and context switches", - guide: "/observability-guide/tracing#spans", - }, -]; - -const OBSERVABILITY_PILLARS = [ - { - name: "Logs", - icon: "📝", - color: "blue", - description: "Timestamped records of discrete events", - useCases: [ - "Debugging agent decisions and failures", - "Audit trails for compliance", - "Root cause analysis", - "Security incident investigation", - ], - keyMetrics: ["Log volume", "Error log rate", "Log ingestion lag"], - guide: "/observability-guide/logging", - }, - { - name: "Metrics", - icon: "📊", - color: "green", - description: "Numerical measurements aggregated over time", - useCases: [ - "Performance monitoring and alerting", - "Capacity planning", - "Cost tracking (tokens, API calls)", - "SLA/SLO tracking", - ], - keyMetrics: ["Latency percentiles", "Error rates", "Token usage", "Success rates"], - guide: "/observability-guide/metrics", - }, - { - name: "Traces", - icon: "🔍", - color: "purple", - description: "End-to-end request flow through distributed systems", - useCases: [ - "Multi-agent workflow visibility", - "Latency bottleneck identification", - "Dependency mapping", - "Tool call chain analysis", - ], - keyMetrics: ["Trace completion rate", "Span count", "Critical path latency"], - guide: "/observability-guide/tracing", - }, -]; - -const MATURITY_LEVELS = [ - { - level: 1, - name: "Reactive", - description: "Basic logging to console/files, manual debugging", - characteristics: [ - "Print statements and console.log debugging", - "No structured logging", - "Manual log grepping", - "No metrics or dashboards", - ], - limitations: "Can't answer: Why did the agent fail? What's the p95 latency? Which tool call is slow?", - }, - { - level: 2, - name: "Aware", - description: "Centralized logging, basic metrics", - characteristics: [ - "Logs aggregated in central system (e.g., CloudWatch, Datadog)", - "JSON structured logging with some context", - "Basic metrics (error count, request count)", - "Manual dashboard creation", - ], - limitations: "Limited correlation between logs and metrics. No distributed tracing. Reactive debugging.", - }, - { - level: 3, - name: "Proactive", - description: "Full observability with logs, metrics, traces", - characteristics: [ - "Structured logging with correlation IDs", - "Comprehensive metrics and alerting", - "Distributed tracing with OpenTelemetry", - "Pre-built dashboards for agent health", - ], - capabilities: "Can answer: What failed? Where? Why? How often? What's the user impact?", - }, - { - level: 4, - name: "Predictive", - description: "AI-powered observability, anomaly detection", - characteristics: [ - "Automated anomaly detection on metrics", - "Predictive alerting (issues before they impact users)", - "Auto-remediation for known failure patterns", - "Continuous profiling and optimization", - ], - capabilities: - "System self-heals and auto-scales. Proactive alerts before user impact. Agent performance continuously optimized.", - }, -]; - -const TOOL_LANDSCAPE = [ - { - category: "All-in-One Platforms", - tools: [ - { name: "Datadog", features: ["Logs", "Metrics", "Traces", "APM"], agentFriendly: true }, - { name: "New Relic", features: ["Logs", "Metrics", "Traces", "AI Monitoring"], agentFriendly: true }, - { name: "Dynatrace", features: ["Full-stack observability", "AI ops"], agentFriendly: false }, - ], - }, - { - category: "Open Source", - tools: [ - { name: "Grafana Stack", features: ["Loki (logs)", "Prometheus (metrics)", "Tempo (traces)"], agentFriendly: true }, - { name: "Elastic Stack", features: ["Elasticsearch", "Logstash", "Kibana"], agentFriendly: true }, - { name: "OpenTelemetry", features: ["Vendor-neutral instrumentation"], agentFriendly: true }, - ], - }, - { - category: "Specialized", - tools: [ - { name: "Langfuse", features: ["LLM observability", "Token tracking", "Prompt management"], agentFriendly: true }, - { name: "LangSmith", features: ["LangChain tracing", "Eval workflows"], agentFriendly: true }, - { name: "Helicone", features: ["LLM proxy", "Cost tracking", "Caching"], agentFriendly: true }, - ], - }, -]; - -const QUICK_START_CODE = [ - { - step: 1, - title: "Structured Logging Setup", - code: `// Use a structured logging library -import pino from 'pino'; - -const logger = pino({ - level: process.env.LOG_LEVEL || 'info', - formatters: { - level: (label) => ({ level: label }), - }, -}); - -// Log with context and correlation -logger.info({ - correlationId: req.id, - userId: req.user.id, - action: 'tool_call', - tool: 'web_search', - query: 'observability best practices', - durationMs: 234, -}, 'Tool call completed');`, - }, - { - step: 2, - title: "Basic Metrics with Prometheus", - code: `import { Counter, Histogram } from 'prom-client'; - -// Define metrics -const toolCallCounter = new Counter({ - name: 'agent_tool_calls_total', - help: 'Total number of tool calls', - labelNames: ['tool', 'status'], -}); - -const llmLatency = new Histogram({ - name: 'agent_llm_latency_seconds', - help: 'LLM request latency', - labelNames: ['model', 'operation'], - buckets: [0.1, 0.5, 1, 2, 5, 10], -}); - -// Instrument your code -const start = Date.now(); -const result = await callLLM(prompt); -llmLatency.observe({ model: 'gpt-4', operation: 'completion' }, (Date.now() - start) / 1000);`, - }, - { - step: 3, - title: "Distributed Tracing with OpenTelemetry", - code: `import { trace, SpanStatusCode } from '@opentelemetry/api'; - -const tracer = trace.getTracer('agent-runtime'); - -async function executeToolCall(toolName: string, params: any) { - return tracer.startActiveSpan(\`tool.\${toolName}\`, async (span) => { - try { - span.setAttribute('tool.name', toolName); - span.setAttribute('tool.params', JSON.stringify(params)); - - const result = await tools[toolName](params); - - span.setStatus({ code: SpanStatusCode.OK }); - span.setAttribute('tool.result.size', JSON.stringify(result).length); - - return result; - } catch (error) { - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - span.recordException(error); - throw error; - } finally { - span.end(); +import { Eye, BellRing } from "lucide-react"; +import { + readObservabilityGuides, + type ObservabilityGuideCategory, + type ObservabilityGuideDifficulty, +} from "@/lib/observability-guides"; + +const categoryLabels: Record = { + logging: "Logging", + metrics: "Metrics", + tracing: "Tracing", + alerting: "Alerting", +}; + +const difficultyStyles: Record = { + beginner: "bg-emerald-100 text-emerald-800 border-emerald-300", + intermediate: "bg-amber-100 text-amber-800 border-amber-300", + advanced: "bg-rose-100 text-rose-800 border-rose-300", +}; + +export default async function ObservabilityGuidePage() { + const guides = await readObservabilityGuides(); + + const categoryCounts = guides.reduce>( + (acc, guide) => { + acc[guide.category] = (acc[guide.category] ?? 0) + 1; + return acc; + }, + { + logging: 0, + metrics: 0, + tracing: 0, + alerting: 0, } - }); -}`, - }, -]; + ); -export default function ObservabilityGuidePage() { - const [selectedPriority, setSelectedPriority] = useState("all"); - const [completedItems, setCompletedItems] = useState>(new Set()); + const difficultyCounts = guides.reduce>( + (acc, guide) => { + acc[guide.difficulty] = (acc[guide.difficulty] ?? 0) + 1; + return acc; + }, + { + beginner: 0, + intermediate: 0, + advanced: 0, + } + ); - const priorityColors = { - critical: "text-red-600 bg-red-50 border-red-200", - high: "text-orange-600 bg-orange-50 border-orange-200", - medium: "text-yellow-600 bg-yellow-50 border-yellow-200", - low: "text-blue-600 bg-blue-50 border-blue-200", - }; + const latestGuides = [...guides] + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) + .slice(0, 8); - const filteredChecklist = - selectedPriority === "all" - ? CHECKLIST - : CHECKLIST.filter((item) => item.priority === selectedPriority); - - const toggleComplete = (id: string) => { - const newCompleted = new Set(completedItems); - if (newCompleted.has(id)) { - newCompleted.delete(id); - } else { - newCompleted.add(id); - } - setCompletedItems(newCompleted); - }; + const alertingGuides = guides.filter((guide) => guide.category === "alerting"); return (
- {/* Header */} -
+
- -

Agent Observability & Monitoring

+ +

Agent Observability Guide

- Comprehensive observability guide for AI agents. Monitor, debug, and optimize agent behavior with logs, - metrics, and distributed tracing. + Persistent guidance for logging, metrics, tracing, and alerting so teams can monitor agent systems with + confidence.

- {/* Three Pillars */} -
-

The Three Pillars of Observability

-
- {OBSERVABILITY_PILLARS.map((pillar) => ( -
-
- {pillar.icon} -

{pillar.name}

-
-

{pillar.description}

-
-

Use Cases:

-
    - {pillar.useCases.map((useCase) => ( -
  • - - {useCase} -
  • - ))} -
-
-
-

Key Metrics:

-
- {pillar.keyMetrics.map((metric) => ( - - {metric} - - ))} -
-
- - Deep Dive → - +
+

Coverage by Category

+
+ {(Object.keys(categoryLabels) as ObservabilityGuideCategory[]).map((category) => ( +
+

{categoryLabels[category]}

+

{categoryCounts[category]}

+

guides

))}
- {/* Why Observability Matters for Agents */} -
-

Why Observability Matters for AI Agents

-
-

- AI agents are non-deterministic black boxes that make autonomous decisions, call external - tools, and chain complex reasoning steps. Without observability, you're flying blind: -

-
-
-

❌ Without Observability

-
    -
  • • "The agent failed" — but why?
  • -
  • • Debugging requires code changes & redeployment
  • -
  • • Can't measure performance improvements
  • -
  • • Users report issues before you know they exist
  • -
  • • No visibility into cost (tokens, API calls)
  • -
-
-
-

✅ With Observability

-
    -
  • • Real-time dashboards show agent health
  • -
  • • Trace requests end-to-end across tools
  • -
  • • Debug production issues without redeployment
  • -
  • • Alerts trigger before users notice degradation
  • -
  • • Optimize costs with token/latency metrics
  • -
-
-
-

- Rule of thumb: If you can't answer "Why did this agent request fail?" in under 2 minutes, - your observability is insufficient. -

-
-
- - {/* Maturity Model */} -
-

Observability Maturity Model

-
- {MATURITY_LEVELS.map((level) => ( -
-
-
- {level.level} -
-
-

{level.name}

-

{level.description}

-
-
-

Characteristics:

-
    - {level.characteristics.map((char) => ( -
  • - - {char} -
  • - ))} -
-
-
-

- {level.capabilities ? "Capabilities:" : "Limitations:"} -

-

- {level.capabilities || level.limitations} -

-
-
-
-
-
- ))} -
-
- - {/* Tool Landscape */} -
-

Observability Tool Landscape

-
- {TOOL_LANDSCAPE.map((category) => ( -
-

{category.category}

-
- {category.tools.map((tool) => ( -
-
-

{tool.name}

- {tool.agentFriendly && ( - Agent-friendly - )} -
-
- {tool.features.map((feature) => ( - - {feature} - - ))} -
-
- ))} -
+
+

Difficulty Distribution

+
+ {(["beginner", "intermediate", "advanced"] as ObservabilityGuideDifficulty[]).map((difficulty) => ( +
+

{difficulty}

+

{difficultyCounts[difficulty]}

))}
- {/* Quick Start Checklist */} -
-
-

Quick Start Checklist

-
- - - - -
-
- +
+

Recently Updated Guides

- {filteredChecklist.map((item) => ( -
-
- -
-
-

{item.title}

- - {item.priority} - - {item.category} -
-

{item.description}

- - View Guide → - -
+ {latestGuides.map((guide) => ( +
+
+

{guide.title}

+ + {guide.difficulty} +
-
+

{guide.content}

+

+ Category: {categoryLabels[guide.category]} • Updated: {new Date(guide.updatedAt).toLocaleDateString()} +

+ ))} -
-
- - {/* Quick Start Code Examples */} -
-

Quick Start: 3 Essential Steps

-
- {QUICK_START_CODE.map((item) => ( -
-
-
- {item.step} -
-
-

{item.title}

-
-                    {item.code}
-                  
-
-
+ {latestGuides.length === 0 && ( +
+ No observability guides found in persistent storage.
- ))} + )}
- {/* Deep Dive Links */} -
-

Deep Dive Guides

+
+

Category Deep Dives

- -

📝 Logging Best Practices

-

- Structured logging, correlation IDs, log levels, what to log, aggregation strategies -

+ +

📝 Logging

+

Structured logs, correlation IDs, and auditing patterns.

- -

📊 Metrics & Dashboards

-

- Key agent metrics, instrumentation patterns, dashboard design, alerting thresholds -

+ +

📊 Metrics

+

Latency, cost, success-rate, and queue telemetry.

- -

🔍 Distributed Tracing

-

- Multi-agent tracing, OpenTelemetry setup, span design, trace visualization -

+ +

🔍 Tracing

+

Span modeling and context propagation across agents.

+ +
+
+ +

Alerting Highlights

+
+
+ {alertingGuides.map((guide) => ( +
+

{guide.title}

+

{guide.content}

+

Tags: {guide.tags.join(", ")}

+
+ ))} +
+
+ +
+ Data source: data/observability-guides.json. API endpoint: /api/observability-guide. +
); } diff --git a/src/app/observability-guide/tracing/page.tsx b/src/app/observability-guide/tracing/page.tsx index 081b04a7..067856d7 100644 --- a/src/app/observability-guide/tracing/page.tsx +++ b/src/app/observability-guide/tracing/page.tsx @@ -1,740 +1,59 @@ -"use client"; +/* eslint-disable react/no-unescaped-entities */ -import { useState } from "react"; import Link from "next/link"; -import { Network, GitBranch, Layers, Zap } from "lucide-react"; +import { Network } from "lucide-react"; +import { readObservabilityGuides, type ObservabilityGuideDifficulty } from "@/lib/observability-guides"; -interface SpanExample { - name: string; - description: string; - spanKind: "CLIENT" | "SERVER" | "INTERNAL" | "PRODUCER" | "CONSUMER"; - attributes: { key: string; value: string; description: string }[]; - code: string; -} - -const SPAN_EXAMPLES: SpanExample[] = [ - { - name: "Agent Request (Root Span)", - description: "Top-level span representing the entire user request", - spanKind: "SERVER", - attributes: [ - { key: "agent.id", value: "agent-123", description: "Unique agent identifier" }, - { key: "user.id", value: "user-456", description: "User making the request" }, - { key: "agent.type", value: "assistant", description: "Agent type (assistant, analyst, etc.)" }, - { key: "request.type", value: "chat", description: "Request operation type" }, - ], - code: `import { trace, SpanStatusCode } from '@opentelemetry/api'; - -const tracer = trace.getTracer('agent-runtime'); - -async function handleRequest(req: Request) { - return tracer.startActiveSpan('agent.request', async (span) => { - try { - span.setAttribute('agent.id', 'agent-123'); - span.setAttribute('user.id', req.userId); - span.setAttribute('agent.type', 'assistant'); - span.setAttribute('request.type', 'chat'); - - const response = await processRequest(req); - - span.setStatus({ code: SpanStatusCode.OK }); - return response; - } catch (error) { - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - span.recordException(error); - throw error; - } finally { - span.end(); - } - }); -}`, - }, - { - name: "LLM API Call", - description: "Span for LLM completion requests (OpenAI, Anthropic, etc.)", - spanKind: "CLIENT", - attributes: [ - { key: "llm.model", value: "gpt-4-turbo", description: "Model name" }, - { key: "llm.operation", value: "completion", description: "API operation type" }, - { key: "llm.prompt.tokens", value: "1234", description: "Input token count" }, - { key: "llm.completion.tokens", value: "567", description: "Output token count" }, - { key: "llm.cached", value: "false", description: "Whether response was cached" }, - ], - code: `async function callLLM(prompt: string, model: string) { - return tracer.startActiveSpan('llm.completion', async (span) => { - span.setAttributes({ - 'llm.model': model, - 'llm.operation': 'completion', - 'llm.prompt.length': prompt.length, - }); - - try { - const response = await openai.chat.completions.create({ - model, - messages: [{ role: 'user', content: prompt }], - }); - - span.setAttributes({ - 'llm.prompt.tokens': response.usage.prompt_tokens, - 'llm.completion.tokens': response.usage.completion_tokens, - 'llm.total.tokens': response.usage.total_tokens, - 'llm.cached': false, - }); - - span.setStatus({ code: SpanStatusCode.OK }); - return response; - } catch (error) { - span.recordException(error); - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - throw error; - } finally { - span.end(); - } - }); -}`, - }, - { - name: "Tool Call Execution", - description: "Span for individual tool invocations", - spanKind: "INTERNAL", - attributes: [ - { key: "tool.name", value: "web_search", description: "Tool identifier" }, - { key: "tool.params", value: "{...}", description: "Serialized parameters" }, - { key: "tool.result.size", value: "2048", description: "Result size in bytes" }, - { key: "tool.cache.hit", value: "false", description: "Whether tool result was cached" }, - ], - code: `async function executeTool(toolName: string, params: any) { - return tracer.startActiveSpan(\`tool.\${toolName}\`, async (span) => { - span.setAttributes({ - 'tool.name': toolName, - 'tool.params': JSON.stringify(params), - }); - - try { - const result = await tools[toolName].execute(params); - - span.setAttributes({ - 'tool.result.size': JSON.stringify(result).length, - 'tool.cache.hit': false, - }); - - span.setStatus({ code: SpanStatusCode.OK }); - return result; - } catch (error) { - span.recordException(error); - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - throw error; - } finally { - span.end(); - } - }); -}`, - }, - { - name: "Agent Reasoning Step", - description: "Span for agent decision-making and planning", - spanKind: "INTERNAL", - attributes: [ - { key: "reasoning.type", value: "plan", description: "Type of reasoning (plan, decide, reflect)" }, - { key: "reasoning.thought", value: "User wants...", description: "Agent's internal thought" }, - { key: "reasoning.selected_tool", value: "web_search", description: "Tool chosen by reasoning" }, - { key: "reasoning.confidence", value: "0.92", description: "Confidence score (0-1)" }, - ], - code: `async function agentReasoning(context: Context) { - return tracer.startActiveSpan('agent.reasoning', async (span) => { - span.setAttribute('reasoning.type', 'plan'); - - const thought = await generateThought(context); - span.setAttribute('reasoning.thought', thought); - - const selectedTool = selectBestTool(thought, context); - span.setAttributes({ - 'reasoning.selected_tool': selectedTool.name, - 'reasoning.confidence': selectedTool.confidence, - 'reasoning.alternatives': JSON.stringify(selectedTool.alternatives), - }); - - span.setStatus({ code: SpanStatusCode.OK }); - span.end(); - - return selectedTool; - }); -}`, - }, - { - name: "Multi-Agent Delegation", - description: "Span when delegating to another agent", - spanKind: "CLIENT", - attributes: [ - { key: "delegation.from_agent", value: "orchestrator", description: "Delegating agent" }, - { key: "delegation.to_agent", value: "specialist", description: "Receiving agent" }, - { key: "delegation.task", value: "analyze_data", description: "Task being delegated" }, - { key: "delegation.reason", value: "Requires domain expertise", description: "Why delegated" }, - ], - code: `async function delegateToAgent(task: Task, targetAgent: string) { - return tracer.startActiveSpan('agent.delegation', async (span) => { - span.setAttributes({ - 'delegation.from_agent': 'orchestrator', - 'delegation.to_agent': targetAgent, - 'delegation.task': task.type, - 'delegation.reason': task.delegationReason, - }); - - // Propagate trace context to the other agent - const result = await callAgent(targetAgent, task, { - headers: { - 'traceparent': getCurrentTraceContext(), - }, - }); - - span.setAttribute('delegation.result.status', result.status); - span.setStatus({ code: SpanStatusCode.OK }); - span.end(); - - return result; - }); -}`, - }, -]; - -const TRACE_PROPAGATION = { - httpHeaders: `// W3C Trace Context (standard) -// https://www.w3.org/TR/trace-context/ - -import { propagation, context } from '@opentelemetry/api'; - -// SENDING side (Agent A → Agent B) -async function callExternalAgent(url: string, data: any) { - const headers = {}; - - // Inject current trace context into headers - propagation.inject(context.active(), headers); - - // Headers now contain: traceparent, tracestate - // traceparent format: 00-{trace-id}-{parent-span-id}-{flags} - - return fetch(url, { - method: 'POST', - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); -} - -// RECEIVING side (Agent B) -function handleIncomingRequest(req: Request) { - // Extract trace context from headers - const ctx = propagation.extract(context.active(), req.headers); - - // Continue trace in new span - return context.with(ctx, () => { - return tracer.startActiveSpan('agent.handle_request', async (span) => { - // This span is now a child of the remote span! - const result = await processRequest(req); - span.end(); - return result; - }); - }); -}`, - - messageQueue: `// Propagating trace context through message queues - -import { TextMapPropagator } from '@opentelemetry/api'; - -// PRODUCER: Inject trace context into message metadata -async function publishMessage(topic: string, payload: any) { - return tracer.startActiveSpan('queue.publish', async (span) => { - const metadata = {}; - propagation.inject(context.active(), metadata); - - await messageQueue.publish({ - topic, - payload, - metadata, // Contains trace context - }); - - span.setAttribute('queue.topic', topic); - span.end(); - }); -} - -// CONSUMER: Extract trace context from message metadata -async function consumeMessage(message: Message) { - const ctx = propagation.extract(context.active(), message.metadata); - - return context.with(ctx, () => { - return tracer.startActiveSpan('queue.consume', async (span) => { - span.setAttribute('queue.topic', message.topic); - - const result = await handleMessage(message.payload); - - span.end(); - return result; - }); - }); -}`, - - localStorage: `// For single-process multi-agent systems (AsyncLocalStorage) - -import { AsyncLocalStorage } from 'async_hooks'; - -const traceStorage = new AsyncLocalStorage(); - -// Store trace context -function executeWithTrace(traceId: string, fn: () => Promise) { - return traceStorage.run({ traceId }, fn); -} - -// Retrieve trace context anywhere in the call stack -function getCurrentTraceId() { - return traceStorage.getStore()?.traceId; -} - -// Usage -await executeWithTrace('trace-abc123', async () => { - await agentA.process(); // Can access traceId - await agentB.delegate(); // Inherits same traceId - await tool.execute(); // All share the same trace -});`, -}; - -const OPENTELEMETRY_SETUP = { - basic: `// 1. Install dependencies -// npm install @opentelemetry/api @opentelemetry/sdk-node \\ -// @opentelemetry/auto-instrumentations-node - -// 2. instrumentation.ts (run BEFORE your app code) -import { NodeSDK } from '@opentelemetry/sdk-node'; -import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; -import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; -import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; - -const sdk = new NodeSDK({ - traceExporter: new OTLPTraceExporter({ - url: 'http://localhost:4318/v1/traces', // Jaeger, Tempo, etc. - }), - instrumentations: [ - getNodeAutoInstrumentations({ - // Automatically instruments http, express, fetch, etc. - '@opentelemetry/instrumentation-fs': { enabled: false }, - }), - ], -}); - -sdk.start(); - -process.on('SIGTERM', () => { - sdk.shutdown().then(() => console.log('Tracing terminated')); -});`, - - custom: `// 3. Custom instrumentation in your agent code -import { trace, SpanStatusCode } from '@opentelemetry/api'; - -const tracer = trace.getTracer('my-agent', '1.0.0'); - -export async function myAgentFunction(input: string) { - return tracer.startActiveSpan('my_agent.process', async (span) => { - span.setAttribute('input.length', input.length); - - try { - const result = await doWork(input); - span.setStatus({ code: SpanStatusCode.OK }); - return result; - } catch (error) { - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - span.recordException(error); - throw error; - } finally { - span.end(); - } - }); -}`, - - backends: `// Popular trace backends (choose one) - -// Option 1: Jaeger (open-source, easy local setup) -// docker run -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest -// UI: http://localhost:16686 - -// Option 2: Grafana Tempo (open-source, scalable) -// Integrated with Grafana for visualization - -// Option 3: Datadog APM (commercial, full observability platform) -traceExporter: new DatadogSpanExporter({ - agentUrl: 'http://localhost:8126', -}), - -// Option 4: Honeycomb (commercial, powerful querying) -traceExporter: new OTLPTraceExporter({ - url: 'https://api.honeycomb.io/v1/traces', - headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY }, -}), - -// Option 5: AWS X-Ray (AWS-native) -import { AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray'; -import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';`, +const difficultyStyles: Record = { + beginner: "bg-emerald-100 text-emerald-800 border-emerald-300", + intermediate: "bg-amber-100 text-amber-800 border-amber-300", + advanced: "bg-rose-100 text-rose-800 border-rose-300", }; -const VISUALIZATION_PATTERNS = [ - { - name: "Waterfall View", - description: "Shows the chronological sequence of spans and their durations", - useCases: ["Identify slow operations", "See which tool calls block others", "Find critical path"], - image: "Shows spans as horizontal bars on a timeline, with parent-child nesting", - }, - { - name: "Dependency Graph", - description: "Visualizes relationships between agents and services", - useCases: ["Understand multi-agent architecture", "Identify chatty communication", "Detect circular dependencies"], - image: "Node-edge graph showing which agents/services call which", - }, - { - name: "Latency Heatmap", - description: "Color-coded view of span durations across many traces", - useCases: ["Spot performance regressions", "Compare latencies over time", "Identify outliers"], - image: "Grid where color intensity represents duration (green=fast, red=slow)", - }, - { - name: "Trace Comparison", - description: "Side-by-side view of two traces (e.g., fast vs slow)", - useCases: ["Debug why some requests are slow", "Compare before/after optimization", "Understand variation"], - image: "Two waterfall views stacked vertically for comparison", - }, -]; - -const BEST_PRACTICES = [ - { - practice: "Keep Spans Focused", - description: "Each span should represent a single logical operation", - good: "Separate spans for: LLM call, tool execution, database query", - bad: "One giant span for the entire agent workflow", - }, - { - practice: "Use Semantic Attributes", - description: "Follow OpenTelemetry semantic conventions for common attributes", - good: "http.method, http.status_code, db.statement, messaging.destination", - bad: "method, status, sql, queue (non-standard names)", - link: "https://opentelemetry.io/docs/specs/semconv/", - }, - { - practice: "Propagate Trace Context Everywhere", - description: "Always pass trace context when crossing boundaries (HTTP, queues, agents)", - good: "Inject traceparent header in all outbound requests", - bad: "Starting a new trace for each service (loses correlation)", - }, - { - practice: "Sample Strategically", - description: "In high-traffic systems, sample traces to reduce overhead", - good: "Sample 10% of normal requests, 100% of errors and slow requests", - bad: "Sample randomly without considering error/latency (lose debugging data)", - }, - { - practice: "Add Business Context", - description: "Include user/session/request IDs as span attributes", - good: "user.id, session.id, request.id, tenant.id", - bad: "Only technical attributes (no way to correlate to user reports)", - }, - { - practice: "Record Exceptions Properly", - description: "Use span.recordException() instead of just setting error status", - good: "span.recordException(error) includes stack trace and message", - bad: "span.setStatus(ERROR) without exception details", - }, -]; +export default async function TracingGuidePage() { + const guides = (await readObservabilityGuides()).filter((guide) => guide.category === "tracing"); -export default function TracingPage() { return ( -
- {/* Header */} -
- - ← Back to Observability Guide - -
- -

Distributed Tracing

-
-

- End-to-end visibility for multi-agent systems. Learn how to instrument, propagate, and visualize traces with - OpenTelemetry. -

+
+ + ← Back to Observability Guide + + +
+ +

Tracing Across Agent Workflows

- - {/* Why Tracing Matters */} -
-

Why Distributed Tracing?

-
-

- The Problem: When a user request touches 5 agents, 10 LLM calls, and 20 tool executions, how - do you debug a slow request? -

-
-
-

❌ Without Tracing

-
    -
  • • Logs scattered across services
  • -
  • • Can't correlate requests across agents
  • -
  • • No visibility into call chains
  • -
  • • Debugging requires code changes
  • -
  • • Can't identify bottlenecks
  • -
+

+ Persistent, category-filtered guidance for root spans, context propagation, and multi-agent trace continuity. +

+ +
+ {guides.map((guide) => ( +
+
+

{guide.title}

+ + {guide.difficulty} +
-
-

✅ With Tracing

-
    -
  • • See entire request flow in one view
  • -
  • • Drill down into any operation
  • -
  • • Instantly spot the slowest span
  • -
  • • Correlate logs/metrics by trace ID
  • -
  • • Understand agent dependencies
  • -
-
-
-
-

- Example: "Request took 8 seconds" → Trace shows: LLM call (6s), tool execution (1.5s), - reasoning (0.5s). Now you know where to optimize. -

-
-
-
- - {/* Core Concepts */} -
-

Core Concepts

-
-
- -

Trace

-

- A trace represents one user request's journey through your entire system. It has a unique trace ID. -

-

- Example: User asks "What's the weather?" → Trace ID: abc123 -

-
-
- -

Span

-

- A span represents a single operation within a trace (LLM call, tool execution, etc.). Spans nest to form a - tree. -

-

- Example: Span "llm.completion" (2.3s) → child of "agent.request" -

-
-
- -

Context Propagation

-

- Passing trace ID and parent span ID across service boundaries (HTTP, queues, agents) to maintain the trace. -

-

- Standard: W3C Trace Context (traceparent header) -

-
-
-
- - {/* Span Examples */} -
-

Span Instrumentation Patterns

-
- {SPAN_EXAMPLES.map((example) => ( -
-
-
-

{example.name}

-

{example.description}

-
- - {example.spanKind} +

{guide.content}

+
+ {guide.tags.map((tag) => ( + + #{tag} -
- -
-

Standard Attributes:

-
- - - - - - - - - - {example.attributes.map((attr) => ( - - - - - - ))} - -
KeyExample ValueDescription
{attr.key}{attr.value}{attr.description}
-
-
- -
-                {example.code}
-              
+ ))}
- ))} -
-
- - {/* Trace Propagation */} -
-

Trace Context Propagation

-
-

- Critical: Without propagation, each service starts a new trace. You lose end-to-end - visibility. -

-

- Always inject trace context when calling external services, agents, or queues. -

-
- -
-
-

HTTP Headers (Agent-to-Agent)

-
-              {TRACE_PROPAGATION.httpHeaders}
-            
-
- -
-

Message Queues (Async Communication)

-
-              {TRACE_PROPAGATION.messageQueue}
-            
-
- -
-

In-Process Multi-Agent (AsyncLocalStorage)

-
-              {TRACE_PROPAGATION.localStorage}
-            
-
-
-
- - {/* OpenTelemetry Setup */} -
-

OpenTelemetry Setup Guide

- -
-
-

Step 1: Install & Configure SDK

-
-              {OPENTELEMETRY_SETUP.basic}
-            
-
- -
-

Step 2: Custom Instrumentation

-
-              {OPENTELEMETRY_SETUP.custom}
-            
-
- -
-

Step 3: Choose a Backend

-
-              {OPENTELEMETRY_SETUP.backends}
-            
-
-
-
- - {/* Visualization Patterns */} -
-

Trace Visualization Patterns

-
- {VISUALIZATION_PATTERNS.map((pattern) => ( -
-

{pattern.name}

-

{pattern.description}

-
-

{pattern.image}

-
-
-

Use Cases:

-
    - {pattern.useCases.map((useCase) => ( -
  • - - {useCase} -
  • - ))} -
-
-
- ))} -
-
- - {/* Best Practices */} -
-

Best Practices

-
- {BEST_PRACTICES.map((practice) => ( -
-

{practice.practice}

-

{practice.description}

- {practice.good && practice.bad && ( -
-
-
✅ Good:
-

- {practice.good} -

-
-
-
❌ Bad:
-

{practice.bad}

-
-
- )} - {practice.link && ( - - Learn more → - - )} -
- ))} -
-
+

Updated: {new Date(guide.updatedAt).toLocaleString()}

+ + ))} +
- {/* Related Guides */} -
-

Related Guides

-
- -

📝 Logging Best Practices

-

- Structured logging with correlation IDs to complement your traces -

- - -

📊 Metrics & Dashboards

-

- Track agent performance with metrics that complement trace data -

- + {guides.length === 0 && ( +
+ No tracing guides found.
-
+ )}
); } diff --git a/src/lib/observability-guides.ts b/src/lib/observability-guides.ts new file mode 100644 index 00000000..675cb3de --- /dev/null +++ b/src/lib/observability-guides.ts @@ -0,0 +1,102 @@ +import { promises as fs } from "fs"; +import path from "path"; + +export type ObservabilityGuideCategory = "logging" | "metrics" | "tracing" | "alerting"; +export type ObservabilityGuideDifficulty = "beginner" | "intermediate" | "advanced"; + +export interface ObservabilityGuide { + id: string; + title: string; + content: string; + category: ObservabilityGuideCategory; + difficulty: ObservabilityGuideDifficulty; + tags: string[]; + updatedAt: string; +} + +interface ObservabilityGuideFilters { + category?: string | null; + difficulty?: string | null; + search?: string | null; +} + +const OBSERVABILITY_GUIDES_PATH = path.join(process.cwd(), "data", "observability-guides.json"); + +const guideCategories: ObservabilityGuideCategory[] = ["logging", "metrics", "tracing", "alerting"]; +const guideDifficulties: ObservabilityGuideDifficulty[] = ["beginner", "intermediate", "advanced"]; + +function isGuideCategory(value: string): value is ObservabilityGuideCategory { + return guideCategories.includes(value as ObservabilityGuideCategory); +} + +function isGuideDifficulty(value: string): value is ObservabilityGuideDifficulty { + return guideDifficulties.includes(value as ObservabilityGuideDifficulty); +} + +function isObservabilityGuide(item: unknown): item is ObservabilityGuide { + if (!item || typeof item !== "object") return false; + + const candidate = item as Record; + + return ( + typeof candidate.id === "string" && + typeof candidate.title === "string" && + typeof candidate.content === "string" && + typeof candidate.category === "string" && + isGuideCategory(candidate.category) && + typeof candidate.difficulty === "string" && + isGuideDifficulty(candidate.difficulty) && + Array.isArray(candidate.tags) && + candidate.tags.every((tag) => typeof tag === "string") && + typeof candidate.updatedAt === "string" + ); +} + +export async function readObservabilityGuides(): Promise { + try { + const raw = await fs.readFile(OBSERVABILITY_GUIDES_PATH, "utf-8"); + const parsed = JSON.parse(raw) as unknown; + + if (!Array.isArray(parsed)) return []; + return parsed.filter(isObservabilityGuide); + } catch { + return []; + } +} + +export async function writeObservabilityGuides(guides: ObservabilityGuide[]): Promise { + const dir = path.dirname(OBSERVABILITY_GUIDES_PATH); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(OBSERVABILITY_GUIDES_PATH, `${JSON.stringify(guides, null, 2)}\n`, "utf-8"); +} + +export function filterObservabilityGuides( + guides: ObservabilityGuide[], + filters: ObservabilityGuideFilters +): ObservabilityGuide[] { + const category = filters.category?.trim().toLowerCase(); + const difficulty = filters.difficulty?.trim().toLowerCase(); + const search = filters.search?.trim().toLowerCase(); + + return guides.filter((guide) => { + if (category && isGuideCategory(category) && guide.category !== category) return false; + if (difficulty && isGuideDifficulty(difficulty) && guide.difficulty !== difficulty) return false; + + if (!search) return true; + + return [guide.title, guide.content, guide.category, guide.difficulty, ...guide.tags] + .join(" ") + .toLowerCase() + .includes(search); + }); +} + +export function normalizeObservabilityGuideCategory(value: string): ObservabilityGuideCategory | null { + const normalized = value.trim().toLowerCase(); + return isGuideCategory(normalized) ? normalized : null; +} + +export function normalizeObservabilityGuideDifficulty(value: string): ObservabilityGuideDifficulty | null { + const normalized = value.trim().toLowerCase(); + return isGuideDifficulty(normalized) ? normalized : null; +}