Overview
API logging captures every API request with full context - method, path, status, latency, and the API key used. This enables usage analytics, debugging, and security monitoring.
What it is
An append-only record of all API requests made through the platform, stored in the ApiLog table with aggregation capabilities.
Why we use it
Usage analytics, debugging failed requests, security monitoring, and billing/quota enforcement.
When to use
Automatically logged by the API middleware - no manual intervention needed. Query logs via the dashboard or metrics API.
Key Features
- Automatic logging of all API requests via middleware
- Rich metadata: method, path, status, latency, response size
- Linkage to API keys for per-key analytics
- Aggregated metrics for dashboard visualization
Quick Start
Querying API Logs
Use the repository functions to fetch logs with filtering and pagination.
// Every API route MUST use the withApiLogging wrapper
// ESLint rule 'centercode/require-api-logging' enforces this
import { NextRequest, NextResponse } from 'next/server';
import { withApiLogging } from '@/lib/api-logging';
// Simple route (no params)
export const GET = withApiLogging(async (request: NextRequest) => {
// Your handler code - logging happens automatically
return NextResponse.json({ ok: true, data: {} });
});
// Route with params
export const GET = withApiLogging(async (
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) => {
const { id } = await params;
return NextResponse.json({ ok: true, data: { id } });
});
// Unused request param - prefix with underscore
export const DELETE = withApiLogging(async (
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) => {
const { id } = await params;
// ...
});Patterns
Logging Middleware
The API middleware automatically logs all requests passing through.
// What withApiLogging captures automatically:
// - method: GET, POST, PATCH, DELETE
// - path: /api/v1/resources/123
// - query: ?page=1&limit=10 (sensitive params filtered)
// - status: 200, 400, 401, 404, 500
// - latencyMs: Response time in milliseconds
// - apiKeyId: From X-API-Key header (if present)
// - programId/projectId: Denormalized from API key
// - ip: Client IP address
// - userAgent: Browser/client identifier
// - errorCode/errorMessage: From error responses
// Log entry stored in api_log table:
interface ApiLogEntry {
method: string;
path: string;
query?: string;
status: number;
latencyMs: number;
apiKeyId?: string;
sessionId?: string;
programId?: string;
projectId?: string;
ip?: string;
userAgent?: string;
errorCode?: string;
errorMessage?: string;
}Metrics Aggregation
Aggregate logs into metrics for dashboards - requests over time, error rates, latency percentiles.
// Metrics aggregation for dashboards
import { getApiMetrics } from '@/features/api-logs';
const metrics = await getApiMetrics({
programId: 'prog_123',
startDate: subDays(new Date(), 7),
endDate: new Date(),
granularity: 'day', // 'hour' | 'day' | 'week'
});
// Summary metrics
const { totalRequests, successfulRequests, failedRequests, errorRate, avgLatencyMs, p95LatencyMs } = metrics.summary;
// Requests over time (for line charts)
metrics.requestsOverTime.forEach(({ date, requests, errors, avgLatency }) => {
// Plot on chart
});
// Status breakdown (for donut/pie charts)
metrics.statusBreakdown.forEach(({ status, count, percentage }) => {
// '2xx': 850, '4xx': 45, '5xx': 5
});
// Top endpoints by traffic
metrics.topEndpoints.forEach(({ path, method, count, avgLatency, errorRate }) => {
// '/api/v1/projects': 500 requests, 45ms avg
});
// Top API keys by usage
metrics.topKeys.forEach(({ keyId, keyName, keyPrefix, count, errorRate }) => {
// Track which keys are most active
});Log Filtering
Filter logs by date range, status code, method, path, or API key.
// Log filtering patterns
import { listApiLogs } from '@/features/api-logs';
// Filter by date range
const recentLogs = await listApiLogs({
programId: 'prog_123',
startDate: subHours(new Date(), 1),
endDate: new Date(),
});
// Filter by status code range
const errorLogs = await listApiLogs({
programId: 'prog_123',
statusMin: 400,
statusMax: 600,
});
// Filter by HTTP method
const writeLogs = await listApiLogs({
programId: 'prog_123',
method: 'POST', // or 'PUT', 'DELETE', etc.
});
// Filter by API key
const keyLogs = await listApiLogs({
programId: 'prog_123',
apiKeyId: 'key_abc123',
});
// Filter by path pattern (contains search)
const projectLogs = await listApiLogs({
programId: 'prog_123',
path: '/api/v1/projects',
});Build-Time Enforcement
Zero drift guarantee: The ESLint rule centercode/require-api-logging ensures every API route uses the wrapper. Builds fail if any route is missing it.
// ESLint rule: centercode/require-api-logging
// Configured in eslint.config.mjs
// This rule checks ALL files in src/app/api/v1/**/*.ts
// and ensures every exported handler uses withApiLogging
// Build will FAIL if any route is missing the wrapper:
// error: API route handlers must use withApiLogging wrapper
// The rule detects these patterns:
// ✗ export async function GET() { }
// ✗ export const GET = async () => { }
// ✓ export const GET = withApiLogging(async () => { })Watch Out
Never log request bodies that may contain sensitive data
Don't
// Missing the wrapper (DON'T - ESLint will fail)
export async function GET(request: NextRequest) {
// No logging! This will be flagged by ESLint rule
// 'centercode/require-api-logging'
return NextResponse.json({ ok: true, data: {} });
}
// Or manually logging (DON'T - inconsistent)
export async function POST(request: NextRequest) {
console.log('API call:', request.url); // Wrong!
return NextResponse.json({ ok: true, data: {} });
}Do
// Correct: Use withApiLogging wrapper (DO)
import { withApiLogging } from '@/lib/api-logging';
export const GET = withApiLogging(async (request: NextRequest) => {
// Logging is automatic and consistent
// - Captures method, path, status, latency
// - Filters sensitive query params
// - Extracts API key info safely
return NextResponse.json({ ok: true, data: {} });
});
export const POST = withApiLogging(async (request: NextRequest) => {
// Same automatic logging for all methods
const body = await request.json();
// Request body is NOT logged (security)
return NextResponse.json({ ok: true, data: body });
});Index frequently queried columns (timestamp for time-based queries)
Don't
// Missing indexes (DON'T)
// schema.prisma
model ApiLog {
id String @id @default(cuid())
programId String
timestamp DateTime @default(now())
// No indexes - queries will be slow!
}Do
// Properly indexed (DO)
// schema.prisma
model ApiLog {
id String @id @default(cuid())
programId String
projectId String?
apiKeyId String?
timestamp DateTime @default(now())
// Index on timestamp for time-based queries
@@index([timestamp(sort: Desc)])
}- Implement log retention policies for high-traffic APIs
- Ensure API key ID is captured for key-based analytics