request-logger.ts•5.79 kB
/**
* Request logging middleware for debugging and monitoring
*/
import { logger } from './logger.js';
import { randomUUID } from 'crypto';
export interface RequestContext {
requestId: string;
startTime: number;
method: string;
path: string;
userId: string | undefined;
userAgent: string | undefined;
ip: string | undefined;
}
export class RequestLogger {
private activeRequests = new Map<string, RequestContext>();
/**
* Start request logging
*/
public startRequest(
method: string,
path: string,
headers?: Record<string, string>,
userId?: string
): string {
const requestId = randomUUID();
const startTime = Date.now();
const context: RequestContext = {
requestId,
startTime,
method,
path,
userId,
userAgent: headers?.['user-agent'],
ip: headers?.['x-forwarded-for'] || headers?.['x-real-ip']
};
this.activeRequests.set(requestId, context);
// Log request start
logger.logRequest(requestId, method, path, userId);
// Log request details in debug mode
logger.debug('Request details', {
requestId,
headers: this.sanitizeHeaders(headers),
userAgent: context.userAgent,
ip: context.ip
});
return requestId;
}
/**
* End request logging
*/
public endRequest(
requestId: string,
statusCode: number,
responseSize?: number,
error?: Error
): void {
const context = this.activeRequests.get(requestId);
if (!context) {
logger.warn('Attempted to end unknown request', { requestId });
return;
}
const duration = Date.now() - context.startTime;
// Log response
logger.logResponse(requestId, statusCode, duration, context.userId);
// Log additional response details in debug mode
logger.debug('Response details', {
requestId,
statusCode,
duration,
responseSize,
success: statusCode < 400
});
// Log error if present
if (error) {
logger.error('Request failed with error', error, {
requestId,
statusCode,
duration,
method: context.method,
path: context.path
});
}
// Clean up
this.activeRequests.delete(requestId);
}
/**
* Log request body (for debugging)
*/
public logRequestBody(requestId: string, body: any): void {
const context = this.activeRequests.get(requestId);
if (!context) {
return;
}
logger.debug('Request body', {
requestId,
bodyType: typeof body,
bodySize: JSON.stringify(body).length,
body: this.sanitizeBody(body)
});
}
/**
* Log response body (for debugging)
*/
public logResponseBody(requestId: string, body: any): void {
const context = this.activeRequests.get(requestId);
if (!context) {
return;
}
logger.debug('Response body', {
requestId,
bodyType: typeof body,
bodySize: JSON.stringify(body).length,
body: this.sanitizeBody(body)
});
}
/**
* Log API operation within a request
*/
public logOperation(
requestId: string,
operation: string,
success: boolean,
duration: number,
details?: Record<string, any>
): void {
const context = this.activeRequests.get(requestId);
logger.logOperation(operation, success, duration, {
requestId,
userId: context?.userId,
...details
});
}
/**
* Get active request count
*/
public getActiveRequestCount(): number {
return this.activeRequests.size;
}
/**
* Get active requests
*/
public getActiveRequests(): RequestContext[] {
return Array.from(this.activeRequests.values());
}
/**
* Clean up stale requests (older than 5 minutes)
*/
public cleanupStaleRequests(): void {
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
for (const [requestId, context] of this.activeRequests.entries()) {
if (context.startTime < fiveMinutesAgo) {
logger.warn('Cleaning up stale request', {
requestId,
age: Date.now() - context.startTime,
method: context.method,
path: context.path
});
this.activeRequests.delete(requestId);
}
}
}
/**
* Sanitize headers for logging (remove sensitive data)
*/
private sanitizeHeaders(headers?: Record<string, string>): Record<string, string> {
if (!headers) return {};
const sanitized = { ...headers };
const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token'];
for (const header of sensitiveHeaders) {
if (sanitized[header]) {
sanitized[header] = '[REDACTED]';
}
}
return sanitized;
}
/**
* Sanitize body for logging (remove sensitive data and limit size)
*/
private sanitizeBody(body: any): any {
if (!body) return body;
try {
const bodyStr = JSON.stringify(body);
// Limit body size in logs
if (bodyStr.length > 1000) {
return `[TRUNCATED - ${bodyStr.length} chars] ${bodyStr.substring(0, 1000)}...`;
}
// Remove sensitive fields
if (typeof body === 'object') {
const sanitized = { ...body };
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
}
return body;
} catch (error) {
return '[UNPARSEABLE BODY]';
}
}
}
// Export singleton instance
export const requestLogger = new RequestLogger();
// Cleanup stale requests every minute
setInterval(() => {
requestLogger.cleanupStaleRequests();
}, 60000);