UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

651 lines (594 loc) 16 kB
/** * @fileoverview OrdoJS CLI - Monitoring and observability generator */ import { CLIError, ErrorType } from '../index.js'; /** * Monitoring and observability generator for OrdoJS applications */ export class MonitoringGenerator { defaultConfig = { structuredLogging: true, logLevel: 'info', metricsCollection: true, distributedTracing: true, errorReporting: true, healthChecks: true, performanceMonitoring: true, alerting: true, integrations: { datadog: false, newrelic: false, sentry: false, prometheus: true, grafana: true } }; /** * Generate monitoring configuration */ generateMonitoringConfig(config, options = {}) { const monitoringConfig = this.mergeConfig(options); try { return { logger: this.generateLoggerConfig(monitoringConfig), metrics: this.generateMetricsConfig(monitoringConfig), tracing: this.generateTracingConfig(monitoringConfig), healthCheck: this.generateHealthCheckConfig(monitoringConfig), errorHandler: this.generateErrorHandlerConfig(monitoringConfig), dockerCompose: this.generateDockerComposeMonitoring(monitoringConfig), kubernetes: this.generateKubernetesMonitoring(monitoringConfig) }; } catch (error) { throw new CLIError(`Failed to generate monitoring configuration: ${error instanceof Error ? error.message : String(error)}`, ErrorType.DEPLOYMENT, 'MONITORING-001'); } } /** * Generate structured logging configuration */ generateLoggerConfig(config) { return `import winston from 'winston'; import { format } from 'winston'; // Configure structured logging const logger = winston.createLogger({ level: '${config.logLevel}', format: format.combine( format.timestamp(), format.errors({ stack: true }), format.json() ), defaultMeta: { service: 'ordojs-app' }, transports: [ new winston.transports.Console({ format: format.combine( format.colorize(), format.simple() ) }), new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }) ] }); // Add request logging middleware export const requestLogger = (req: any, res: any, next: any) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.info('HTTP Request', { method: req.method, url: req.url, statusCode: res.statusCode, duration, userAgent: req.get('User-Agent'), ip: req.ip }); }); next(); }; export default logger; `; } /** * Generate metrics collection configuration */ generateMetricsConfig(config) { return `import prometheus from 'prom-client'; import { register, collectDefaultMetrics } from 'prom-client'; // Initialize Prometheus metrics collectDefaultMetrics({ register }); // Custom metrics export const httpRequestDuration = new prometheus.Histogram({ name: 'http_request_duration_seconds', help: 'Duration of HTTP requests in seconds', labelNames: ['method', 'route', 'status_code'], buckets: [0.1, 0.5, 1, 2, 5] }); export const httpRequestTotal = new prometheus.Counter({ name: 'http_requests_total', help: 'Total number of HTTP requests', labelNames: ['method', 'route', 'status_code'] }); export const activeConnections = new prometheus.Gauge({ name: 'active_connections', help: 'Number of active connections' }); export const memoryUsage = new prometheus.Gauge({ name: 'memory_usage_bytes', help: 'Memory usage in bytes', labelNames: ['type'] }); // Metrics middleware export const metricsMiddleware = (req: any, res: any, next: any) => { const start = Date.now(); res.on('finish', () => { const duration = (Date.now() - start) / 1000; const labels = { method: req.method, route: req.route?.path || req.path, status_code: res.statusCode }; httpRequestDuration.observe(labels, duration); httpRequestTotal.inc(labels); }); next(); }; // Metrics endpoint export const metricsEndpoint = async (req: any, res: any) => { try { res.set('Content-Type', register.contentType); res.end(await register.metrics()); } catch (error) { res.status(500).end(error); } }; // Memory usage monitoring setInterval(() => { const memUsage = process.memoryUsage(); memoryUsage.set({ type: 'rss' }, memUsage.rss); memoryUsage.set({ type: 'heapTotal' }, memUsage.heapTotal); memoryUsage.set({ type: 'heapUsed' }, memUsage.heapUsed); memoryUsage.set({ type: 'external' }, memUsage.external); }, 30000); `; } /** * Generate distributed tracing configuration */ generateTracingConfig(config) { return `import { trace, context, SpanStatusCode } from '@opentelemetry/api'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; // Initialize tracing const provider = new NodeTracerProvider(); const exporter = new JaegerExporter({ endpoint: process.env.JAEGER_ENDPOINT || 'http://localhost:14268/api/traces' }); provider.addSpanProcessor(new BatchSpanProcessor(exporter)); provider.register(); registerInstrumentations({ instrumentations: [getNodeAutoInstrumentations()] }); const tracer = trace.getTracer('ordojs-app'); // Tracing middleware export const tracingMiddleware = (req: any, res: any, next: any) => { const span = tracer.startSpan('HTTP Request', { attributes: { 'http.method': req.method, 'http.url': req.url, 'http.user_agent': req.get('User-Agent'), 'http.client_ip': req.ip } }); const ctx = trace.setSpan(context.active(), span); context.with(ctx, () => { res.on('finish', () => { span.setAttributes({ 'http.status_code': res.statusCode, 'http.status_text': res.statusMessage }); if (res.statusCode >= 400) { span.setStatus({ code: SpanStatusCode.ERROR }); } else { span.setStatus({ code: SpanStatusCode.OK }); } span.end(); }); next(); }); }; // Custom span creation export const createSpan = (name: string, attributes?: Record<string, any>) => { return tracer.startSpan(name, { attributes }); }; export { tracer }; `; } /** * Generate health check configuration */ generateHealthCheckConfig(config) { return `import { createHealthCheck } from './health-check.js'; // Health check configuration const healthChecks = { database: async () => { try { // Add your database health check logic here return { status: 'healthy', details: 'Database connection OK' }; } catch (error) { return { status: 'unhealthy', details: error.message }; } }, redis: async () => { try { // Add your Redis health check logic here return { status: 'healthy', details: 'Redis connection OK' }; } catch (error) { return { status: 'unhealthy', details: error.message }; } }, external: async () => { try { // Add external service health check logic here return { status: 'healthy', details: 'External services OK' }; } catch (error) { return { status: 'unhealthy', details: error.message }; } } }; export const healthCheckMiddleware = createHealthCheck(healthChecks); // Health check endpoint export const healthCheckEndpoint = async (req: any, res: any) => { const results = await healthCheckMiddleware(req, res); const isHealthy = Object.values(results).every( (result: any) => result.status === 'healthy' ); res.status(isHealthy ? 200 : 503).json({ status: isHealthy ? 'healthy' : 'unhealthy', timestamp: new Date().toISOString(), checks: results }); }; `; } /** * Generate error handler configuration */ generateErrorHandlerConfig(config) { return `import logger from './logger.js'; import { tracer } from './tracing.js'; // Global error handler export const errorHandler = (error: any, req: any, res: any, next: any) => { const span = tracer.startSpan('Error Handler'); // Log error with structured logging logger.error('Application Error', { error: { message: error.message, stack: error.stack, name: error.name }, request: { method: req.method, url: req.url, headers: req.headers, body: req.body, userAgent: req.get('User-Agent'), ip: req.ip }, user: req.user?.id, timestamp: new Date().toISOString() }); // Set span attributes span.setAttributes({ 'error.message': error.message, 'error.type': error.name, 'error.stack': error.stack }); span.setStatus({ code: 2 }); // ERROR span.end(); // Send error response const statusCode = error.statusCode || 500; const message = statusCode === 500 ? 'Internal Server Error' : error.message; res.status(statusCode).json({ error: { message, statusCode, timestamp: new Date().toISOString() } }); }; // Async error wrapper export const asyncHandler = (fn: Function) => { return (req: any, res: any, next: any) => { Promise.resolve(fn(req, res, next)).catch(next); }; }; // Unhandled rejection handler process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection', { reason: reason instanceof Error ? reason.message : reason, stack: reason instanceof Error ? reason.stack : undefined, promise: promise.toString() }); }); // Uncaught exception handler process.on('uncaughtException', (error) => { logger.error('Uncaught Exception', { error: { message: error.message, stack: error.stack, name: error.name } }); // Graceful shutdown process.exit(1); }); `; } /** * Generate Docker Compose monitoring services */ generateDockerComposeMonitoring(config) { return `# Monitoring services for Docker Compose version: '3.8' services: # Prometheus for metrics collection prometheus: image: prom/prometheus:latest container_name: prometheus ports: - "9090:9090" volumes: - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml - prometheus_data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/etc/prometheus/console_libraries' - '--web.console.templates=/etc/prometheus/consoles' - '--storage.tsdb.retention.time=200h' - '--web.enable-lifecycle' restart: unless-stopped # Grafana for visualization grafana: image: grafana/grafana:latest container_name: grafana ports: - "3001:3000" volumes: - grafana_data:/var/lib/grafana - ./monitoring/grafana/provisioning:/etc/grafana/provisioning environment: - GF_SECURITY_ADMIN_PASSWORD=admin - GF_USERS_ALLOW_SIGN_UP=false restart: unless-stopped # Jaeger for distributed tracing jaeger: image: jaegertracing/all-in-one:latest container_name: jaeger ports: - "16686:16686" - "14268:14268" environment: - COLLECTOR_OTLP_ENABLED=true restart: unless-stopped # Redis for caching and session storage redis: image: redis:alpine container_name: redis ports: - "6379:6379" volumes: - redis_data:/data restart: unless-stopped volumes: prometheus_data: grafana_data: redis_data: `; } /** * Generate Kubernetes monitoring manifests */ generateKubernetesMonitoring(config) { return `# Prometheus ConfigMap apiVersion: v1 kind: ConfigMap metadata: name: prometheus-config data: prometheus.yml: | global: scrape_interval: 15s scrape_configs: - job_name: 'ordojs-app' static_configs: - targets: ['ordojs-app-service:3000'] metrics_path: '/metrics' scrape_interval: 5s --- # Prometheus Deployment apiVersion: apps/v1 kind: Deployment metadata: name: prometheus spec: replicas: 1 selector: matchLabels: app: prometheus template: metadata: labels: app: prometheus spec: containers: - name: prometheus image: prom/prometheus:latest ports: - containerPort: 9090 volumeMounts: - name: prometheus-config mountPath: /etc/prometheus - name: prometheus-storage mountPath: /prometheus command: - /bin/prometheus - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --web.console.libraries=/etc/prometheus/console_libraries - --web.console.templates=/etc/prometheus/consoles - --storage.tsdb.retention.time=200h - --web.enable-lifecycle volumes: - name: prometheus-config configMap: name: prometheus-config - name: prometheus-storage persistentVolumeClaim: claimName: prometheus-pvc --- # Prometheus Service apiVersion: v1 kind: Service metadata: name: prometheus-service spec: selector: app: prometheus ports: - port: 9090 targetPort: 9090 type: ClusterIP --- # Grafana Deployment apiVersion: apps/v1 kind: Deployment metadata: name: grafana spec: replicas: 1 selector: matchLabels: app: grafana template: metadata: labels: app: grafana spec: containers: - name: grafana image: grafana/grafana:latest ports: - containerPort: 3000 env: - name: GF_SECURITY_ADMIN_PASSWORD value: "admin" - name: GF_USERS_ALLOW_SIGN_UP value: "false" volumeMounts: - name: grafana-storage mountPath: /var/lib/grafana volumes: - name: grafana-storage persistentVolumeClaim: claimName: grafana-pvc --- # Grafana Service apiVersion: v1 kind: Service metadata: name: grafana-service spec: selector: app: grafana ports: - port: 3000 targetPort: 3000 type: ClusterIP --- # Jaeger Deployment apiVersion: apps/v1 kind: Deployment metadata: name: jaeger spec: replicas: 1 selector: matchLabels: app: jaeger template: metadata: labels: app: jaeger spec: containers: - name: jaeger image: jaegertracing/all-in-one:latest ports: - containerPort: 16686 - containerPort: 14268 env: - name: COLLECTOR_OTLP_ENABLED value: "true" --- # Jaeger Service apiVersion: v1 kind: Service metadata: name: jaeger-service spec: selector: app: jaeger ports: - name: ui port: 16686 targetPort: 16686 - name: collector port: 14268 targetPort: 14268 type: ClusterIP --- # Persistent Volume Claims apiVersion: v1 kind: PersistentVolumeClaim metadata: name: prometheus-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: grafana-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi `; } mergeConfig(options) { return { ...this.defaultConfig, ...options, integrations: { ...this.defaultConfig.integrations, ...options.integrations } }; } } //# sourceMappingURL=monitoring-generator.js.map