BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes
This commit is contained in:
164
ts/core/plugins/built-in/logging-plugin.ts
Normal file
164
ts/core/plugins/built-in/logging-plugin.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user