localgreenchain/lib/logging/middleware.ts
Claude 5ea8bab5c3
Add production deployment infrastructure (Agent 4)
- 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/*
2025-11-23 03:54:03 +00:00

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 };