/** * Logging Plugin * * Automatically logs requests, responses, and errors */ import { defaultLogger } from '../../observability/logger.js'; import type { Plugin, PluginContext, PluginResponse, LoggingPluginConfig } from '../types.js'; /** * Default configuration */ const DEFAULT_CONFIG: Required = { logRequests: true, logResponses: true, logErrors: true, logRequestBody: false, logResponseBody: false, maxBodySize: 1024, // 1KB sensitiveFields: ['password', 'token', 'secret', 'authorization', 'api_key'], }; /** * Create logging plugin */ export function createLoggingPlugin(config: LoggingPluginConfig = {}): Plugin { const pluginConfig = { ...DEFAULT_CONFIG, ...config }; const logger = defaultLogger; return { name: 'logging', version: '1.0.0', priority: 10, // Execute early beforeRequest: (context: PluginContext) => { if (!pluginConfig.logRequests) { return context; } const logData: Record = { requestId: context.request.requestId, method: context.request.method, path: context.request.path, }; // Add querystring if present if (context.request.querystring) { logData.querystring = context.request.querystring; } // Add request body if enabled if (pluginConfig.logRequestBody && context.request.body) { const bodyStr = JSON.stringify(context.request.body); if (bodyStr.length <= pluginConfig.maxBodySize) { logData.body = sanitizeObject(context.request.body, pluginConfig.sensitiveFields); } else { logData.bodySize = bodyStr.length; logData.bodyTruncated = true; } } logger.debug('Elasticsearch request', logData); return context; }, afterResponse: (context: PluginContext, response: PluginResponse) => { if (!pluginConfig.logResponses) { return response; } const duration = Date.now() - context.request.startTime; const logData: Record = { requestId: context.request.requestId, method: context.request.method, path: context.request.path, statusCode: response.statusCode, duration, }; // Add warnings if present if (response.warnings && response.warnings.length > 0) { logData.warnings = response.warnings; } // Add response body if enabled if (pluginConfig.logResponseBody && response.body) { const bodyStr = JSON.stringify(response.body); if (bodyStr.length <= pluginConfig.maxBodySize) { logData.body = response.body; } else { logData.bodySize = bodyStr.length; logData.bodyTruncated = true; } } logger.info('Elasticsearch response', logData); return response; }, onError: (context) => { if (!pluginConfig.logErrors) { return null; } const duration = Date.now() - context.request.startTime; logger.error('Elasticsearch error', { requestId: context.request.requestId, method: context.request.method, path: context.request.path, duration, attempts: context.attempts, error: { name: context.error.name, message: context.error.message, stack: context.error.stack, }, statusCode: context.response?.statusCode, }); // Don't handle error, just log it return null; }, }; } /** * Sanitize object by removing sensitive fields */ function sanitizeObject(obj: unknown, sensitiveFields: string[]): unknown { if (obj === null || obj === undefined) { return obj; } if (typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map((item) => sanitizeObject(item, sensitiveFields)); } const sanitized: Record = {}; for (const [key, value] of Object.entries(obj)) { const lowerKey = key.toLowerCase(); // Check if key is sensitive const isSensitive = sensitiveFields.some((field) => lowerKey.includes(field.toLowerCase())); if (isSensitive) { sanitized[key] = '[REDACTED]'; } else if (typeof value === 'object' && value !== null) { sanitized[key] = sanitizeObject(value, sensitiveFields); } else { sanitized[key] = value; } } return sanitized; }