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, 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
});Log Filtering
Filter logs by date range, status code, method, path, or API key.
// Log filtering patterns
import { getApiLogs } from '@/features/api-logs';
// Filter by date range
const recentLogs = await getApiLogs({
programId: 'prog_123',
startDate: subHours(new Date(), 1),
endDate: new Date(),
});
// Filter by status code range
const errorLogs = await getApiLogs({
programId: 'prog_123',
minStatus: 400,
maxStatus: 599,
});
// Filter by HTTP method
const writeLogs = await getApiLogs({
programId: 'prog_123',
method: 'POST', // or 'PUT', 'DELETE', etc.
});
// Filter by API key
const keyLogs = await getApiLogs({
programId: 'prog_123',
apiKeyId: 'key_abc123',
});
// Filter by path pattern
const projectLogs = await getApiLogs({
programId: 'prog_123',
pathPrefix: '/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 (programId, projectId, apiKeyId, createdAt)
Don't
// Missing indexes (DON'T)
// schema.prisma
model ApiLog {
id String @id @default(cuid())
programId String
createdAt 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?
createdAt DateTime @default(now())
// Index frequently queried columns
@@index([programId, createdAt(sort: Desc)])
@@index([projectId, createdAt(sort: Desc)])
@@index([apiKeyId, createdAt(sort: Desc)])
}- Implement log retention policies for high-traffic APIs
- Ensure API key ID is captured for key-based analytics