- Docker: Multi-stage Dockerfile with security hardening, docker-compose for production and development environments - Environment: Comprehensive .env.example with all config options, lib/config/env.ts for typed environment validation - Logging: Structured JSON logging with request/response middleware - Monitoring: Prometheus metrics endpoint, Grafana dashboard, health checks (liveness/readiness probes) - Security: Security headers, rate limiting, CORS middleware - CI/CD: GitHub Actions workflows for CI, production deploy, and preview deployments - Error tracking: Sentry integration foundation Files created: - Docker: Dockerfile, docker-compose.yml, docker-compose.dev.yml, .dockerignore - Config: lib/config/env.ts, lib/config/index.ts - Logging: lib/logging/logger.ts, lib/logging/middleware.ts - Monitoring: lib/monitoring/sentry.ts, lib/monitoring/metrics.ts, lib/monitoring/health.ts - Security: lib/security/headers.ts, lib/security/rateLimit.ts, lib/security/cors.ts - API: pages/api/health/*, pages/api/metrics.ts - Infra: infra/prometheus/prometheus.yml, infra/grafana/*
158 lines
4.3 KiB
TypeScript
158 lines
4.3 KiB
TypeScript
/**
|
|
* Logging Middleware for API Routes
|
|
* Agent 4: Production Deployment
|
|
*
|
|
* Provides request/response logging middleware for Next.js API routes.
|
|
*/
|
|
|
|
import type { NextApiRequest, NextApiResponse, NextApiHandler } from 'next';
|
|
import { createLogger, Logger } from './logger';
|
|
|
|
interface RequestLogContext {
|
|
requestId: string;
|
|
method: string;
|
|
path: string;
|
|
query?: Record<string, string | string[]>;
|
|
userAgent?: string;
|
|
ip?: string;
|
|
}
|
|
|
|
interface ResponseLogContext extends RequestLogContext {
|
|
statusCode: number;
|
|
duration: number;
|
|
}
|
|
|
|
/**
|
|
* Generate a unique request ID
|
|
*/
|
|
function generateRequestId(): string {
|
|
return `req_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
|
|
}
|
|
|
|
/**
|
|
* Get client IP from request headers
|
|
*/
|
|
function getClientIp(req: NextApiRequest): string {
|
|
const forwarded = req.headers['x-forwarded-for'];
|
|
if (typeof forwarded === 'string') {
|
|
return forwarded.split(',')[0].trim();
|
|
}
|
|
if (Array.isArray(forwarded)) {
|
|
return forwarded[0];
|
|
}
|
|
return req.socket?.remoteAddress || 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Sanitize headers for logging (remove sensitive data)
|
|
*/
|
|
function sanitizeHeaders(headers: Record<string, string | string[] | undefined>): Record<string, string> {
|
|
const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token'];
|
|
const sanitized: Record<string, string> = {};
|
|
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
if (sensitiveHeaders.includes(key.toLowerCase())) {
|
|
sanitized[key] = '[REDACTED]';
|
|
} else if (typeof value === 'string') {
|
|
sanitized[key] = value;
|
|
} else if (Array.isArray(value)) {
|
|
sanitized[key] = value.join(', ');
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Request logging middleware
|
|
*/
|
|
export function withLogging(handler: NextApiHandler): NextApiHandler {
|
|
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
const startTime = Date.now();
|
|
const requestId = generateRequestId();
|
|
const logger = createLogger({ requestId });
|
|
|
|
// Extract request information
|
|
const requestContext: RequestLogContext = {
|
|
requestId,
|
|
method: req.method || 'UNKNOWN',
|
|
path: req.url || '/',
|
|
query: req.query as Record<string, string | string[]>,
|
|
userAgent: req.headers['user-agent'],
|
|
ip: getClientIp(req),
|
|
};
|
|
|
|
// Log incoming request
|
|
logger.info('Incoming request', {
|
|
...requestContext,
|
|
headers: sanitizeHeaders(req.headers as Record<string, string | string[] | undefined>),
|
|
});
|
|
|
|
// Add request ID to response headers
|
|
res.setHeader('X-Request-Id', requestId);
|
|
|
|
// Capture the original end method
|
|
const originalEnd = res.end;
|
|
let responseLogged = false;
|
|
|
|
// Override end to log response
|
|
res.end = function (this: NextApiResponse, ...args: Parameters<typeof originalEnd>) {
|
|
if (!responseLogged) {
|
|
responseLogged = true;
|
|
const duration = Date.now() - startTime;
|
|
|
|
const responseContext: ResponseLogContext = {
|
|
...requestContext,
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
};
|
|
|
|
// Log based on status code
|
|
if (res.statusCode >= 500) {
|
|
logger.error('Request completed with server error', responseContext);
|
|
} else if (res.statusCode >= 400) {
|
|
logger.warn('Request completed with client error', responseContext);
|
|
} else {
|
|
logger.info('Request completed', responseContext);
|
|
}
|
|
}
|
|
|
|
return originalEnd.apply(this, args);
|
|
} as typeof originalEnd;
|
|
|
|
try {
|
|
// Execute the handler
|
|
await handler(req, res);
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
|
|
// Log error
|
|
logger.error(
|
|
'Request failed with exception',
|
|
error instanceof Error ? error : new Error(String(error)),
|
|
{
|
|
...requestContext,
|
|
duration,
|
|
}
|
|
);
|
|
|
|
// Re-throw to let Next.js handle the error
|
|
throw error;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a logger with request context for use within API handlers
|
|
*/
|
|
export function getRequestLogger(req: NextApiRequest): Logger {
|
|
const requestId = (req.headers['x-request-id'] as string) || generateRequestId();
|
|
return createLogger({
|
|
requestId,
|
|
method: req.method,
|
|
path: req.url,
|
|
});
|
|
}
|
|
|
|
// Export types
|
|
export type { RequestLogContext, ResponseLogContext };
|