- 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/*
188 lines
4.6 KiB
TypeScript
188 lines
4.6 KiB
TypeScript
/**
|
|
* Structured Logging System
|
|
* Agent 4: Production Deployment
|
|
*
|
|
* Provides structured JSON logging with support for different log levels,
|
|
* context enrichment, and production-ready formatting.
|
|
*/
|
|
|
|
import { env, LogLevel } from '../config';
|
|
|
|
interface LogContext {
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface LogEntry {
|
|
timestamp: string;
|
|
level: LogLevel;
|
|
message: string;
|
|
service: string;
|
|
environment: string;
|
|
context?: LogContext;
|
|
error?: {
|
|
name: string;
|
|
message: string;
|
|
stack?: string;
|
|
};
|
|
}
|
|
|
|
type LogMethod = (message: string, context?: LogContext) => void;
|
|
|
|
interface Logger {
|
|
error: (message: string, errorOrContext?: Error | LogContext, context?: LogContext) => void;
|
|
warn: LogMethod;
|
|
info: LogMethod;
|
|
debug: LogMethod;
|
|
trace: LogMethod;
|
|
child: (context: LogContext) => Logger;
|
|
}
|
|
|
|
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
error: 0,
|
|
warn: 1,
|
|
info: 2,
|
|
debug: 3,
|
|
trace: 4,
|
|
};
|
|
|
|
const LOG_LEVEL_COLORS: Record<LogLevel, string> = {
|
|
error: '\x1b[31m', // Red
|
|
warn: '\x1b[33m', // Yellow
|
|
info: '\x1b[36m', // Cyan
|
|
debug: '\x1b[35m', // Magenta
|
|
trace: '\x1b[90m', // Gray
|
|
};
|
|
|
|
const RESET_COLOR = '\x1b[0m';
|
|
|
|
class LoggerImpl implements Logger {
|
|
private baseContext: LogContext;
|
|
private minLevel: number;
|
|
private format: 'json' | 'pretty';
|
|
private serviceName: string;
|
|
|
|
constructor(context: LogContext = {}) {
|
|
this.baseContext = context;
|
|
this.minLevel = LOG_LEVEL_PRIORITY[env.logLevel];
|
|
this.format = env.logFormat;
|
|
this.serviceName = env.appName;
|
|
}
|
|
|
|
private shouldLog(level: LogLevel): boolean {
|
|
return LOG_LEVEL_PRIORITY[level] <= this.minLevel;
|
|
}
|
|
|
|
private formatEntry(entry: LogEntry): string {
|
|
if (this.format === 'pretty') {
|
|
return this.formatPretty(entry);
|
|
}
|
|
return JSON.stringify(entry);
|
|
}
|
|
|
|
private formatPretty(entry: LogEntry): string {
|
|
const color = LOG_LEVEL_COLORS[entry.level];
|
|
const timestamp = new Date(entry.timestamp).toLocaleTimeString();
|
|
const level = entry.level.toUpperCase().padEnd(5);
|
|
|
|
let output = `${color}[${timestamp}] ${level}${RESET_COLOR} ${entry.message}`;
|
|
|
|
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
output += ` ${JSON.stringify(entry.context)}`;
|
|
}
|
|
|
|
if (entry.error) {
|
|
output += `\n ${color}Error: ${entry.error.message}${RESET_COLOR}`;
|
|
if (entry.error.stack) {
|
|
output += `\n${entry.error.stack.split('\n').slice(1).join('\n')}`;
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
private log(level: LogLevel, message: string, context?: LogContext, error?: Error): void {
|
|
if (!this.shouldLog(level)) {
|
|
return;
|
|
}
|
|
|
|
const entry: LogEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
level,
|
|
message,
|
|
service: this.serviceName,
|
|
environment: env.nodeEnv,
|
|
};
|
|
|
|
// Merge base context with provided context
|
|
const mergedContext = { ...this.baseContext, ...context };
|
|
if (Object.keys(mergedContext).length > 0) {
|
|
entry.context = mergedContext;
|
|
}
|
|
|
|
// Add error details if present
|
|
if (error) {
|
|
entry.error = {
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
};
|
|
}
|
|
|
|
const output = this.formatEntry(entry);
|
|
|
|
// Use appropriate console method
|
|
switch (level) {
|
|
case 'error':
|
|
console.error(output);
|
|
break;
|
|
case 'warn':
|
|
console.warn(output);
|
|
break;
|
|
case 'debug':
|
|
case 'trace':
|
|
console.debug(output);
|
|
break;
|
|
default:
|
|
console.log(output);
|
|
}
|
|
}
|
|
|
|
error(message: string, errorOrContext?: Error | LogContext, context?: LogContext): void {
|
|
if (errorOrContext instanceof Error) {
|
|
this.log('error', message, context, errorOrContext);
|
|
} else {
|
|
this.log('error', message, errorOrContext);
|
|
}
|
|
}
|
|
|
|
warn(message: string, context?: LogContext): void {
|
|
this.log('warn', message, context);
|
|
}
|
|
|
|
info(message: string, context?: LogContext): void {
|
|
this.log('info', message, context);
|
|
}
|
|
|
|
debug(message: string, context?: LogContext): void {
|
|
this.log('debug', message, context);
|
|
}
|
|
|
|
trace(message: string, context?: LogContext): void {
|
|
this.log('trace', message, context);
|
|
}
|
|
|
|
child(context: LogContext): Logger {
|
|
return new LoggerImpl({ ...this.baseContext, ...context });
|
|
}
|
|
}
|
|
|
|
// Create and export default logger instance
|
|
export const logger = new LoggerImpl();
|
|
|
|
// Export for creating child loggers with context
|
|
export function createLogger(context: LogContext): Logger {
|
|
return new LoggerImpl(context);
|
|
}
|
|
|
|
// Export types
|
|
export type { Logger, LogContext, LogEntry };
|