165 lines
4.3 KiB
TypeScript
165 lines
4.3 KiB
TypeScript
/**
|
|
* 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<LoggingPluginConfig> = {
|
|
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<string, unknown> = {
|
|
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: <T>(context: PluginContext, response: PluginResponse<T>) => {
|
|
if (!pluginConfig.logResponses) {
|
|
return response;
|
|
}
|
|
|
|
const duration = Date.now() - context.request.startTime;
|
|
|
|
const logData: Record<string, unknown> = {
|
|
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<string, unknown> = {};
|
|
|
|
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;
|
|
}
|