341 lines
7.5 KiB
Markdown
341 lines
7.5 KiB
Markdown
# Logging and Monitoring
|
|
|
|
This document covers structured logging, error tracking with Sentry, and observability patterns.
|
|
|
|
## Critical Rules
|
|
|
|
### NO `console.log` - Use Structured Logger
|
|
|
|
Never use `console.log` in production code. Always use the structured logger from `@your-app/logs`.
|
|
|
|
```typescript
|
|
// BAD - Unstructured console logging
|
|
console.log("Order created:", orderId);
|
|
console.error("Failed to process:", error);
|
|
|
|
// GOOD - Structured logging
|
|
import { logger } from "@your-app/logs";
|
|
|
|
logger.info("Order created", {
|
|
orderId,
|
|
userId,
|
|
total: order.total,
|
|
});
|
|
|
|
logger.error("Failed to process order", {
|
|
orderId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
```
|
|
|
|
## Logger API
|
|
|
|
```typescript
|
|
import { logger } from "@your-app/logs";
|
|
|
|
// Log levels
|
|
logger.debug("Debug message", { context: "value" });
|
|
logger.info("Info message", { orderId, status });
|
|
logger.warn("Warning message", { userId, reason: "quota exceeded" });
|
|
logger.error("Error message", { error: err.message, stack: err.stack });
|
|
```
|
|
|
|
## Sentry Integration
|
|
|
|
### Span Tracing
|
|
|
|
Use the tracing system to monitor performance and track operations.
|
|
|
|
```typescript
|
|
import { SpanPrefix, span } from "../../../lib/tracer";
|
|
|
|
// Database operations
|
|
const orders = await span(
|
|
`${SpanPrefix.DB}GetUserOrders`,
|
|
() => db.select().from(orderTable).where(eq(orderTable.userId, userId)),
|
|
{ userId, limit: 20 }
|
|
);
|
|
|
|
// External API calls
|
|
const response = await span(
|
|
`${SpanPrefix.Http}FetchInventory`,
|
|
() => inventoryClient.getStock(productIds),
|
|
{ productCount: productIds.length }
|
|
);
|
|
|
|
// Redis cache operations
|
|
const cached = await span(
|
|
`${SpanPrefix.Redis}GetSession`,
|
|
() => redis.get(sessionKey),
|
|
{ sessionKey }
|
|
);
|
|
```
|
|
|
|
### SpanPrefix Constants
|
|
|
|
Use standardized prefixes for consistent Sentry categorization:
|
|
|
|
```typescript
|
|
import { SpanPrefix } from "../../../lib/tracer";
|
|
|
|
const SpanPrefix = {
|
|
/** Database operations - maps to Sentry op: db.query */
|
|
DB: "DB.",
|
|
|
|
/** External HTTP API calls - maps to Sentry op: http.client */
|
|
Http: "Http.",
|
|
|
|
/** Redis cache operations - maps to Sentry op: db.redis */
|
|
Redis: "Redis.",
|
|
|
|
/** AI model invocations - maps to Sentry op: ai.run */
|
|
AI: "AI.",
|
|
|
|
/** Generic cache operations - maps to Sentry op: cache */
|
|
Cache: "Cache.",
|
|
|
|
/** Queue/message operations - maps to Sentry op: queue */
|
|
Queue: "Queue.",
|
|
} as const;
|
|
```
|
|
|
|
### Naming Convention
|
|
|
|
```typescript
|
|
// Pattern: ${SpanPrefix.Type}${Action}${Resource}
|
|
|
|
// Database
|
|
`${SpanPrefix.DB}GetUserOrders`
|
|
`${SpanPrefix.DB}BatchUpdateProducts`
|
|
`${SpanPrefix.DB}CreateOrder`
|
|
|
|
// External APIs
|
|
`${SpanPrefix.Http}FetchPaymentStatus`
|
|
`${SpanPrefix.Http}SendNotification`
|
|
|
|
// Redis
|
|
`${SpanPrefix.Redis}GetSession`
|
|
`${SpanPrefix.Redis}SetCache`
|
|
|
|
// AI
|
|
`${SpanPrefix.AI}ClassifyContent`
|
|
`${SpanPrefix.AI}GenerateResponse`
|
|
```
|
|
|
|
### Error Capture
|
|
|
|
```typescript
|
|
import { captureError } from "../../../lib/tracer";
|
|
|
|
try {
|
|
await processOrder(orderId);
|
|
} catch (error) {
|
|
captureError(error, {
|
|
tags: {
|
|
operation: "processOrder",
|
|
orderId,
|
|
},
|
|
extra: {
|
|
userId: context.user.id,
|
|
orderStatus: order.status,
|
|
},
|
|
});
|
|
|
|
throw error; // Re-throw if needed
|
|
}
|
|
```
|
|
|
|
### Trace Context
|
|
|
|
For complex operations, use trace context to correlate logs:
|
|
|
|
```typescript
|
|
import { runWithTrace, getLogId } from "../../../lib/tracer";
|
|
|
|
export async function processOrderBatch(orderIds: string[]) {
|
|
return runWithTrace(`batch-${Date.now()}`, async () => {
|
|
const logId = getLogId();
|
|
|
|
logger.info("Starting batch processing", {
|
|
logId,
|
|
orderCount: orderIds.length
|
|
});
|
|
|
|
for (const orderId of orderIds) {
|
|
await span(
|
|
`${SpanPrefix.DB}ProcessOrder`,
|
|
() => processSingleOrder(orderId),
|
|
{ orderId }
|
|
);
|
|
}
|
|
|
|
logger.info("Batch processing complete", { logId });
|
|
});
|
|
}
|
|
```
|
|
|
|
## AI SDK Telemetry
|
|
|
|
When using the Vercel AI SDK, enable telemetry for token tracking:
|
|
|
|
```typescript
|
|
import { generateText } from "ai";
|
|
import { openai } from "@ai-sdk/openai";
|
|
|
|
const result = await generateText({
|
|
model: openai("gpt-4o"),
|
|
prompt: userPrompt,
|
|
experimental_telemetry: {
|
|
isEnabled: true,
|
|
functionId: "classify-content",
|
|
metadata: {
|
|
userId,
|
|
contentLength: content.length,
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
### Telemetry Metadata
|
|
|
|
Include relevant context in telemetry:
|
|
|
|
```typescript
|
|
experimental_telemetry: {
|
|
isEnabled: true,
|
|
functionId: "generate-response", // Unique identifier for this AI function
|
|
metadata: {
|
|
// User context
|
|
userId: context.user.id,
|
|
|
|
// Input metrics
|
|
promptTokens: estimatedTokens,
|
|
|
|
// Business context
|
|
feature: "auto-reply",
|
|
priority: "high",
|
|
},
|
|
}
|
|
```
|
|
|
|
## Error Handling Patterns
|
|
|
|
### Structured Error Logging
|
|
|
|
```typescript
|
|
async function processPayment(orderId: string) {
|
|
try {
|
|
const result = await paymentGateway.charge(orderId);
|
|
|
|
logger.info("Payment processed", {
|
|
orderId,
|
|
transactionId: result.transactionId,
|
|
amount: result.amount,
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
logger.error("Payment processing failed", {
|
|
orderId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
errorCode: (error as any).code,
|
|
});
|
|
|
|
// Capture to Sentry with context
|
|
captureError(error, {
|
|
tags: { service: "payment", operation: "charge" },
|
|
extra: { orderId },
|
|
});
|
|
|
|
throw new ORPCError("INTERNAL_SERVER_ERROR", {
|
|
message: "Payment processing failed",
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### Batch Operation Logging
|
|
|
|
```typescript
|
|
async function batchUpdateInventory(updates: InventoryUpdate[]) {
|
|
const results: ProcessResult[] = [];
|
|
|
|
logger.info("Starting batch inventory update", {
|
|
updateCount: updates.length,
|
|
});
|
|
|
|
const processed = await Promise.allSettled(
|
|
updates.map(update => processUpdate(update))
|
|
);
|
|
|
|
const successful = processed.filter(r => r.status === "fulfilled").length;
|
|
const failed = processed.filter(r => r.status === "rejected").length;
|
|
|
|
logger.info("Batch inventory update complete", {
|
|
total: updates.length,
|
|
successful,
|
|
failed,
|
|
});
|
|
|
|
if (failed > 0) {
|
|
logger.warn("Some inventory updates failed", {
|
|
failedCount: failed,
|
|
errors: processed
|
|
.filter((r): r is PromiseRejectedResult => r.status === "rejected")
|
|
.map(r => r.reason?.message || "Unknown error"),
|
|
});
|
|
}
|
|
|
|
return { successful, failed };
|
|
}
|
|
```
|
|
|
|
## Logging Best Practices
|
|
|
|
### What to Log
|
|
|
|
**Always log:**
|
|
- Request/response for external API calls
|
|
- Database write operations (create, update, delete)
|
|
- Authentication events
|
|
- Business-critical operations
|
|
- Errors and exceptions
|
|
|
|
**Log with care (avoid sensitive data):**
|
|
- User inputs (sanitize PII)
|
|
- Request payloads (redact secrets)
|
|
|
|
**Never log:**
|
|
- Passwords or tokens
|
|
- Credit card numbers
|
|
- Personal identification numbers
|
|
- API keys or secrets
|
|
|
|
### Log Levels Guide
|
|
|
|
| Level | Use Case | Example |
|
|
|-------|----------|---------|
|
|
| `debug` | Development diagnostics | Variable values, flow tracing |
|
|
| `info` | Normal operations | Order created, user logged in |
|
|
| `warn` | Recoverable issues | Rate limit approaching, retry attempted |
|
|
| `error` | Failures requiring attention | API call failed, database error |
|
|
|
|
### Structured Context
|
|
|
|
Always include relevant context as structured data:
|
|
|
|
```typescript
|
|
// BAD - String interpolation
|
|
logger.info(`User ${userId} created order ${orderId} for $${total}`);
|
|
|
|
// GOOD - Structured context
|
|
logger.info("Order created", {
|
|
userId,
|
|
orderId,
|
|
total,
|
|
currency: "USD",
|
|
itemCount: items.length,
|
|
});
|
|
```
|