Files
elasticsearch/ts/core/plugins/built-in/logging-plugin.ts

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