improved logger
This commit is contained in:
parent
e8485dd140
commit
0f510bfa33
9 changed files with 110 additions and 1250 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Data Service - Combined live and historical data ingestion
|
* Data Service - Combined live and historical data ingestion
|
||||||
*/
|
*/
|
||||||
import { createLogger, GracefulShutdownManager } from '@stock-bot/logger';
|
import { createLogger } from '@stock-bot/logger';
|
||||||
import { loadEnvVariables } from '@stock-bot/config';
|
import { loadEnvVariables } from '@stock-bot/config';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { serve } from '@hono/node-server';
|
import { serve } from '@hono/node-server';
|
||||||
|
|
@ -11,7 +11,6 @@ loadEnvVariables();
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const logger = createLogger('data-service');
|
const logger = createLogger('data-service');
|
||||||
const shutdownManager = new GracefulShutdownManager(logger);
|
|
||||||
|
|
||||||
const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002');
|
const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002');
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,8 @@ async function demonstrateCustomProxySource() {
|
||||||
sourceCount: customSources.length
|
sourceCount: customSources.length
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Custom source scraping failed', error, {
|
logger.error('❌ Custom source scraping failed',{
|
||||||
|
error: error as Error,
|
||||||
sourceUrls: customSources.map(s => s.url)
|
sourceUrls: customSources.map(s => s.url)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ export class ProxyService {
|
||||||
try {
|
try {
|
||||||
await this.scrapeProxies();
|
await this.scrapeProxies();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error in periodic proxy refresh', { error });
|
this.logger.error('Error in periodic proxy refresh', {error});
|
||||||
}
|
}
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +169,7 @@ export class ProxyService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error scraping from source', {
|
this.logger.error('Error scraping from source', {
|
||||||
url: source.url,
|
url: source.url,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error
|
||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -318,7 +318,7 @@ export class ProxyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error updating proxy status', { error });
|
this.logger.error('Error updating proxy status', {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|
@ -346,7 +346,7 @@ export class ProxyService {
|
||||||
this.logger.warn('No working proxies available');
|
this.logger.warn('No working proxies available');
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error getting working proxy', { error });
|
this.logger.error('Error getting working proxy', {error});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +371,7 @@ export class ProxyService {
|
||||||
|
|
||||||
return workingProxies;
|
return workingProxies;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error getting working proxies', { error });
|
this.logger.error('Error getting working proxies', {error});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -391,7 +391,7 @@ export class ProxyService {
|
||||||
this.logger.warn('getAllProxies not fully implemented - Redis cache provider limitations');
|
this.logger.warn('getAllProxies not fully implemented - Redis cache provider limitations');
|
||||||
return proxies;
|
return proxies;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error getting all proxies', { error });
|
this.logger.error('Error getting all proxies', {error});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -424,7 +424,7 @@ export class ProxyService {
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error getting proxy stats', { error });
|
this.logger.error('Error getting proxy stats', {error});
|
||||||
return {
|
return {
|
||||||
total: 0,
|
total: 0,
|
||||||
working: 0,
|
working: 0,
|
||||||
|
|
@ -485,7 +485,7 @@ export class ProxyService {
|
||||||
stillWorking: successCount
|
stillWorking: successCount
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error in health check', { error });
|
this.logger.error('Error in health check', {error});
|
||||||
}
|
}
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
}
|
}
|
||||||
|
|
@ -502,7 +502,7 @@ export class ProxyService {
|
||||||
this.logger.info('Cleared proxy stats from cache');
|
this.logger.info('Cleared proxy stats from cache');
|
||||||
this.logger.warn('Full proxy data clearing not implemented due to cache provider limitations');
|
this.logger.warn('Full proxy data clearing not implemented due to cache provider limitations');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error clearing proxy data', { error });
|
this.logger.error('Error clearing proxy data', {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import pino from 'pino';
|
|
||||||
|
|
||||||
interface ShutdownHandler {
|
|
||||||
name: string;
|
|
||||||
handler: () => Promise<void> | void;
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GracefulShutdownManager {
|
|
||||||
private logger: pino.Logger;
|
|
||||||
private handlers: ShutdownHandler[] = [];
|
|
||||||
private isShuttingDown = false;
|
|
||||||
private startTime = Date.now();
|
|
||||||
|
|
||||||
constructor(logger: pino.Logger) {
|
|
||||||
this.logger = logger;
|
|
||||||
this.setupSignalHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a shutdown handler
|
|
||||||
*/
|
|
||||||
addHandler(handler: ShutdownHandler): void {
|
|
||||||
this.handlers.push(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup signal handlers for graceful shutdown
|
|
||||||
*/
|
|
||||||
private setupSignalHandlers(): void {
|
|
||||||
// Graceful shutdown signals
|
|
||||||
process.on('SIGTERM', () => this.shutdown('SIGTERM', 0));
|
|
||||||
process.on('SIGINT', () => this.shutdown('SIGINT', 0));
|
|
||||||
process.on('SIGUSR2', () => this.shutdown('SIGUSR2', 0)); // nodemon
|
|
||||||
|
|
||||||
// Fatal error handlers
|
|
||||||
process.on('uncaughtException', (error) => {
|
|
||||||
this.logger.error('Uncaught exception', error, { fatal: true });
|
|
||||||
this.shutdown('uncaughtException', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
|
||||||
this.logger.error('Unhandled promise rejection', {
|
|
||||||
reason: reason instanceof Error ? reason.message : reason,
|
|
||||||
stack: reason instanceof Error ? reason.stack : undefined,
|
|
||||||
fatal: true
|
|
||||||
});
|
|
||||||
this.shutdown('unhandledRejection', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process warnings
|
|
||||||
process.on('warning', (warning) => {
|
|
||||||
this.logger.warn('Process warning', {
|
|
||||||
name: warning.name,
|
|
||||||
message: warning.message,
|
|
||||||
stack: warning.stack
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Final exit
|
|
||||||
process.on('exit', (code) => {
|
|
||||||
const uptime = Date.now() - this.startTime;
|
|
||||||
console.log(`Process exiting with code ${code} after ${uptime}ms`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute graceful shutdown
|
|
||||||
*/
|
|
||||||
private async shutdown(signal: string, exitCode: number): Promise<void> {
|
|
||||||
if (this.isShuttingDown) {
|
|
||||||
this.logger.warn('Force shutdown - multiple signals received');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShuttingDown = true;
|
|
||||||
const shutdownStart = Date.now();
|
|
||||||
|
|
||||||
this.logger.info('Graceful shutdown initiated', {
|
|
||||||
signal,
|
|
||||||
exitCode,
|
|
||||||
uptime: Date.now() - this.startTime,
|
|
||||||
memoryUsage: process.memoryUsage(),
|
|
||||||
handlersCount: this.handlers.length
|
|
||||||
});
|
|
||||||
|
|
||||||
// Execute shutdown handlers
|
|
||||||
for (const handler of this.handlers) {
|
|
||||||
try {
|
|
||||||
this.logger.info(`Executing shutdown handler: ${handler.name}`);
|
|
||||||
const timeout = handler.timeout || 5000;
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
Promise.resolve(handler.handler()),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.logger.info(`Shutdown handler completed: ${handler.name}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Shutdown handler failed: ${handler.name}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shutdownDuration = Date.now() - shutdownStart;
|
|
||||||
this.logger.info('Graceful shutdown completed', {
|
|
||||||
signal,
|
|
||||||
shutdownDuration,
|
|
||||||
totalUptime: Date.now() - this.startTime
|
|
||||||
});
|
|
||||||
|
|
||||||
// Give logs time to flush
|
|
||||||
setTimeout(() => process.exit(exitCode), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +1,14 @@
|
||||||
/**
|
/**
|
||||||
* @stock-bot/logger - Enhanced logging library with Loki integration
|
* @stock-bot/logger - Simplified logging library
|
||||||
*
|
*
|
||||||
* Main exports for the logger library
|
* Main exports for the logger library
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Core logger classes and functions
|
// Core logger classes and functions
|
||||||
export { Logger, createLogger, getLogger, shutdownLoggers } from './logger';
|
export { Logger, createLogger, getLogger } from './logger';
|
||||||
|
|
||||||
// Utility functions
|
// Type definitions
|
||||||
export {
|
export type { LogLevel, LogContext, LogMetadata } from './types';
|
||||||
createTimer,
|
|
||||||
formatError,
|
|
||||||
sanitizeMetadata,
|
|
||||||
generateCorrelationId,
|
|
||||||
extractHttpMetadata,
|
|
||||||
createBusinessEvent,
|
|
||||||
createSecurityEvent,
|
|
||||||
maskSensitiveData,
|
|
||||||
calculateLogSize,
|
|
||||||
LogThrottle
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
// Hono middleware
|
// Default export
|
||||||
export {
|
export { getLogger as default } from './logger';
|
||||||
loggingMiddleware,
|
|
||||||
errorLoggingMiddleware,
|
|
||||||
createRequestLogger,
|
|
||||||
performanceMiddleware,
|
|
||||||
securityMiddleware,
|
|
||||||
businessEventMiddleware,
|
|
||||||
comprehensiveLoggingMiddleware
|
|
||||||
} from './middleware';
|
|
||||||
|
|
||||||
// Type exports
|
|
||||||
export type {
|
|
||||||
LogLevel,
|
|
||||||
LogContext,
|
|
||||||
LogMetadata,
|
|
||||||
LoggerOptions,
|
|
||||||
LokiTransportOptions,
|
|
||||||
PerformanceTimer,
|
|
||||||
LokiLogEntry,
|
|
||||||
StructuredLog
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
export type { LoggingMiddlewareOptions } from './middleware';
|
|
||||||
|
|
||||||
export { GracefulShutdownManager } from './gracefulShutdown';
|
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,47 @@
|
||||||
/**
|
/**
|
||||||
* Enhanced Pino-based logger with Loki integration for Stock Bot platform
|
* Simplified Pino-based logger for Stock Bot platform
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - High performance JSON logging with Pino
|
* - High performance JSON logging with Pino
|
||||||
* - Multiple log levels (debug, info, warn, error, http, verbose, silly)
|
* - Console, file, and Loki transports
|
||||||
* - Console and file logging with pino-pretty formatting
|
|
||||||
* - Loki integration for centralized logging
|
|
||||||
* - Structured logging with metadata
|
* - Structured logging with metadata
|
||||||
* - Flexible message handling (string or object)
|
|
||||||
* - Service-specific context
|
* - Service-specific context
|
||||||
* - TypeScript-friendly interfaces
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import { loggingConfig, lokiConfig } from '@stock-bot/config';
|
import { loggingConfig, lokiConfig } from '@stock-bot/config';
|
||||||
import type { LogLevel, LogContext, LogMetadata } from './types';
|
import type { LogLevel, LogContext, LogMetadata } from './types';
|
||||||
|
|
||||||
// Global logger instances cache
|
// Simple cache for logger instances
|
||||||
const loggerInstances = new Map<string, pino.Logger>();
|
const loggerCache = new Map<string, pino.Logger>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create transport configuration for Pino based on options
|
* Create transport configuration
|
||||||
*/
|
*/
|
||||||
function createTransports(serviceName: string, options?: {
|
function createTransports(serviceName: string): any {
|
||||||
enableConsole?: boolean;
|
|
||||||
enableFile?: boolean;
|
|
||||||
enableLoki?: boolean;
|
|
||||||
}): any {
|
|
||||||
const {
|
|
||||||
enableConsole = loggingConfig.LOG_CONSOLE,
|
|
||||||
enableFile = loggingConfig.LOG_FILE,
|
|
||||||
enableLoki = true
|
|
||||||
} = options || {};
|
|
||||||
|
|
||||||
const targets: any[] = [];
|
const targets: any[] = [];
|
||||||
// Console transport with pretty formatting
|
// const isDev = loggingConfig.LOG_ENVIRONMENT === 'development';
|
||||||
if (enableConsole) {
|
|
||||||
|
// Console transport
|
||||||
|
if (loggingConfig.LOG_CONSOLE) {
|
||||||
targets.push({
|
targets.push({
|
||||||
target: 'pino-pretty',
|
target: 'pino-pretty',
|
||||||
level: loggingConfig.LOG_LEVEL,
|
level: loggingConfig.LOG_LEVEL,
|
||||||
options: {
|
options: {
|
||||||
colorize: true,
|
colorize: true,
|
||||||
translateTime: 'yyyy-mm-dd HH:MM:ss.l',
|
translateTime: 'yyyy-mm-dd HH:MM:ss.l',
|
||||||
ignore: 'pid,hostname,environment,version,service',
|
|
||||||
messageFormat: '[{service}] {msg}',
|
messageFormat: '[{service}] {msg}',
|
||||||
|
singleLine: true,
|
||||||
|
hideObject: false,
|
||||||
|
ignore: 'pid,hostname,service,environment,version',
|
||||||
errorLikeObjectKeys: ['err', 'error'],
|
errorLikeObjectKeys: ['err', 'error'],
|
||||||
errorProps: 'message,stack,name,code',
|
errorProps: 'message,stack,name,code',
|
||||||
singleLine: false, // Allow multiline for better metadata display
|
|
||||||
hideObject: false, // Show metadata objects
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// File transport for general logs
|
// File transport
|
||||||
if (enableFile) {
|
if (loggingConfig.LOG_FILE) {
|
||||||
targets.push({
|
targets.push({
|
||||||
target: 'pino/file',
|
target: 'pino/file',
|
||||||
level: loggingConfig.LOG_LEVEL,
|
level: loggingConfig.LOG_LEVEL,
|
||||||
|
|
@ -62,335 +50,153 @@ function createTransports(serviceName: string, options?: {
|
||||||
mkdir: true
|
mkdir: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Separate error file if enabled
|
|
||||||
if (loggingConfig.LOG_ERROR_FILE) {
|
|
||||||
targets.push({
|
|
||||||
target: 'pino/file',
|
|
||||||
level: 'error',
|
|
||||||
options: {
|
|
||||||
destination: `${loggingConfig.LOG_FILE_PATH}/${serviceName}-error.log`,
|
|
||||||
mkdir: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loki transport for centralized logging
|
// Loki transport
|
||||||
if (enableLoki && lokiConfig.LOKI_HOST) {
|
if (lokiConfig.LOKI_HOST) {
|
||||||
targets.push({
|
targets.push({
|
||||||
target: 'pino-loki',
|
target: 'pino-loki',
|
||||||
level: loggingConfig.LOG_LEVEL,
|
level: loggingConfig.LOG_LEVEL,
|
||||||
options: {
|
options: {
|
||||||
batching: true,
|
|
||||||
interval: lokiConfig.LOKI_BATCH_WAIT,
|
|
||||||
host: lokiConfig.LOKI_URL || `http://${lokiConfig.LOKI_HOST}:${lokiConfig.LOKI_PORT}`,
|
host: lokiConfig.LOKI_URL || `http://${lokiConfig.LOKI_HOST}:${lokiConfig.LOKI_PORT}`,
|
||||||
basicAuth: lokiConfig.LOKI_USERNAME && lokiConfig.LOKI_PASSWORD
|
|
||||||
? {
|
|
||||||
username: lokiConfig.LOKI_USERNAME,
|
|
||||||
password: lokiConfig.LOKI_PASSWORD
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
labels: {
|
labels: {
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
environment: lokiConfig.LOKI_ENVIRONMENT_LABEL,
|
environment: lokiConfig.LOKI_ENVIRONMENT_LABEL
|
||||||
...(lokiConfig.LOKI_DEFAULT_LABELS ? JSON.parse(lokiConfig.LOKI_DEFAULT_LABELS) : {})
|
}
|
||||||
},
|
|
||||||
timeout: lokiConfig.LOKI_PUSH_TIMEOUT || 10000,
|
|
||||||
silenceErrors: false,
|
|
||||||
// Better JSON handling
|
|
||||||
replaceTimestamp: false,
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return targets.length > 0 ? { targets } : null;
|
||||||
targets
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create or retrieve a logger instance for a specific service
|
* Get or create pino logger
|
||||||
*/
|
*/
|
||||||
export function createLogger(serviceName: string, options?: {
|
function getPinoLogger(serviceName: string): pino.Logger {
|
||||||
level?: LogLevel;
|
if (!loggerCache.has(serviceName)) {
|
||||||
enableLoki?: boolean;
|
const transport = createTransports(serviceName);
|
||||||
enableFile?: boolean;
|
|
||||||
enableConsole?: boolean;
|
const config: pino.LoggerOptions = {
|
||||||
}): pino.Logger {
|
level: loggingConfig.LOG_LEVEL,
|
||||||
const key = `${serviceName}-${JSON.stringify(options || {})}`;
|
base: {
|
||||||
|
service: serviceName,
|
||||||
if (loggerInstances.has(key)) {
|
environment: loggingConfig.LOG_ENVIRONMENT,
|
||||||
return loggerInstances.get(key)!;
|
version: loggingConfig.LOG_SERVICE_VERSION
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const logger = buildLogger(serviceName, options);
|
if (transport) {
|
||||||
loggerInstances.set(key, logger);
|
config.transport = transport;
|
||||||
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a Pino logger with all configured transports
|
|
||||||
*/
|
|
||||||
function buildLogger(serviceName: string, options?: {
|
|
||||||
level?: LogLevel;
|
|
||||||
enableLoki?: boolean;
|
|
||||||
enableFile?: boolean;
|
|
||||||
enableConsole?: boolean;
|
|
||||||
}): pino.Logger {
|
|
||||||
const {
|
|
||||||
level = loggingConfig.LOG_LEVEL as LogLevel,
|
|
||||||
enableLoki = true,
|
|
||||||
enableFile = loggingConfig.LOG_FILE,
|
|
||||||
enableConsole = loggingConfig.LOG_CONSOLE
|
|
||||||
} = options || {};
|
|
||||||
|
|
||||||
const transport = createTransports(serviceName, {
|
|
||||||
enableConsole,
|
|
||||||
enableFile,
|
|
||||||
enableLoki
|
|
||||||
});
|
|
||||||
const loggerConfig: pino.LoggerOptions = {
|
|
||||||
level: level,
|
|
||||||
timestamp: () => `,"timestamp":"${new Date().toISOString()}"`,
|
|
||||||
base: {
|
|
||||||
service: serviceName,
|
|
||||||
environment: loggingConfig.LOG_ENVIRONMENT,
|
|
||||||
version: loggingConfig.LOG_SERVICE_VERSION
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Only add transport if targets exist to avoid worker thread issues
|
loggerCache.set(serviceName, pino(config));
|
||||||
if (transport && transport.targets && transport.targets.length > 0) {
|
|
||||||
loggerConfig.transport = transport;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pino(loggerConfig);
|
return loggerCache.get(serviceName)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhanced Logger class with convenience methods and flexible message handling
|
* Simplified Logger class
|
||||||
*/
|
*/
|
||||||
export class Logger {
|
export class Logger {
|
||||||
private pino: pino.Logger;
|
private pino: pino.Logger;
|
||||||
private serviceName: string;
|
|
||||||
private context: LogContext;
|
private context: LogContext;
|
||||||
constructor(serviceName: string, context: LogContext = {}, options?: {
|
|
||||||
level?: LogLevel;
|
constructor(serviceName: string, context: LogContext = {}) {
|
||||||
enableLoki?: boolean;
|
this.pino = getPinoLogger(serviceName);
|
||||||
enableFile?: boolean;
|
|
||||||
enableConsole?: boolean;
|
|
||||||
}) {
|
|
||||||
this.serviceName = serviceName;
|
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.pino = createLogger(serviceName, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flexible log method that accepts string or object messages
|
* Core log method
|
||||||
*/
|
*/
|
||||||
log(level: LogLevel, message: string | object, metadata?: LogMetadata): void {
|
private log(level: LogLevel, message: string | object, metadata?: LogMetadata): void {
|
||||||
const logData = {
|
const data = { ...this.context, ...metadata };
|
||||||
...this.context,
|
|
||||||
...metadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof message === 'string') {
|
if (typeof message === 'string') {
|
||||||
(this.pino as any)[level](logData, message);
|
(this.pino as any)[level](data, message);
|
||||||
} else {
|
} else {
|
||||||
// For object messages, use the message object as the main log data
|
(this.pino as any)[level]({ ...data, data: message }, 'Object logged');
|
||||||
// and add metadata without overwriting properties from the message
|
|
||||||
const messageObj = message as Record<string, any>;
|
|
||||||
const combinedData = { ...logData };
|
|
||||||
|
|
||||||
// Add message properties that don't conflict with metadata
|
|
||||||
Object.keys(messageObj).forEach(key => {
|
|
||||||
if (!(key in combinedData)) {
|
|
||||||
combinedData[key] = messageObj[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(this.pino as any)[level](combinedData, messageObj.msg || messageObj.message || 'Object log');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Simple log level methods
|
||||||
* Debug level logging
|
|
||||||
*/
|
|
||||||
debug(message: string | object, metadata?: LogMetadata): void {
|
debug(message: string | object, metadata?: LogMetadata): void {
|
||||||
this.log('debug', message, metadata);
|
this.log('debug', message, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verbose level logging
|
|
||||||
*/
|
|
||||||
verbose(message: string | object, metadata?: LogMetadata): void {
|
|
||||||
// Map verbose to debug level since Pino doesn't have verbose by default
|
|
||||||
this.log('debug', message, { ...metadata, originalLevel: 'verbose' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Silly level logging
|
|
||||||
*/
|
|
||||||
silly(message: string | object, metadata?: LogMetadata): void {
|
|
||||||
// Map silly to debug level since Pino doesn't have silly by default
|
|
||||||
this.log('debug', message, { ...metadata, originalLevel: 'silly' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Info level logging
|
|
||||||
*/
|
|
||||||
info(message: string | object, metadata?: LogMetadata): void {
|
info(message: string | object, metadata?: LogMetadata): void {
|
||||||
this.log('info', message, metadata);
|
this.log('info', message, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Warning level logging
|
|
||||||
*/
|
|
||||||
warn(message: string | object, metadata?: LogMetadata): void {
|
warn(message: string | object, metadata?: LogMetadata): void {
|
||||||
this.log('warn', message, metadata);
|
this.log('warn', message, metadata);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Error level logging with proper pino-pretty formatting
|
error(message: string | object, metadata?: LogMetadata & { error?: any }): void {
|
||||||
*/
|
const data = { ...metadata };
|
||||||
error(message: string | object, error?: Error | any, metadata?: LogMetadata): void {
|
|
||||||
const logData: LogMetadata = { ...metadata };
|
|
||||||
|
|
||||||
if (error) {
|
// Handle any type of error automatically
|
||||||
if (error instanceof Error) {
|
if (data.error) {
|
||||||
// Use 'err' key for pino-pretty compatibility
|
data.err = this.normalizeError(data.error);
|
||||||
logData.err = error;
|
delete data.error;
|
||||||
} else {
|
|
||||||
logData.error = error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log('error', message, logData);
|
this.log('error', message, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request logging
|
* Normalize any error type to a structured format
|
||||||
*/
|
*/
|
||||||
http(message: string | object, requestData?: {
|
private normalizeError(error: any): any {
|
||||||
method?: string;
|
if (error instanceof Error) {
|
||||||
url?: string;
|
return {
|
||||||
statusCode?: number;
|
name: error.name,
|
||||||
responseTime?: number;
|
message: error.message,
|
||||||
userAgent?: string;
|
stack: error.stack,
|
||||||
ip?: string;
|
};
|
||||||
}): void {
|
}
|
||||||
if (!loggingConfig.LOG_HTTP_REQUESTS) return;
|
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
this.log('info', message, {
|
// Handle error-like objects
|
||||||
request: requestData,
|
return {
|
||||||
type: 'http_request'
|
name: error.name || 'UnknownError',
|
||||||
});
|
message: error.message || error.toString(),
|
||||||
|
...(error.stack && { stack: error.stack }),
|
||||||
|
...(error.code && { code: error.code }),
|
||||||
|
...(error.status && { status: error.status })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle primitives (string, number, etc.)
|
||||||
|
return {
|
||||||
|
name: 'UnknownError',
|
||||||
|
message: String(error)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performance/timing logging
|
* Create child logger with additional context
|
||||||
*/
|
|
||||||
performance(message: string | object, timing: {
|
|
||||||
operation: string;
|
|
||||||
duration: number;
|
|
||||||
startTime?: number;
|
|
||||||
endTime?: number;
|
|
||||||
}): void {
|
|
||||||
if (!loggingConfig.LOG_PERFORMANCE) return;
|
|
||||||
|
|
||||||
this.log('info', message, {
|
|
||||||
performance: timing,
|
|
||||||
type: 'performance'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Business event logging
|
|
||||||
*/
|
|
||||||
business(message: string | object, event: {
|
|
||||||
type: string;
|
|
||||||
entity?: string;
|
|
||||||
action?: string;
|
|
||||||
result?: 'success' | 'failure' | 'partial';
|
|
||||||
amount?: number;
|
|
||||||
symbol?: string;
|
|
||||||
}): void {
|
|
||||||
this.log('info', message, {
|
|
||||||
business: event,
|
|
||||||
type: 'business_event'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security event logging
|
|
||||||
*/
|
|
||||||
security(message: string | object, event: {
|
|
||||||
type: 'authentication' | 'authorization' | 'access' | 'vulnerability';
|
|
||||||
user?: string;
|
|
||||||
resource?: string;
|
|
||||||
action?: string;
|
|
||||||
result?: 'success' | 'failure';
|
|
||||||
ip?: string;
|
|
||||||
severity?: 'low' | 'medium' | 'high' | 'critical';
|
|
||||||
}): void {
|
|
||||||
this.log('warn', message, {
|
|
||||||
security: event,
|
|
||||||
type: 'security_event'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a child logger with additional context
|
|
||||||
*/
|
*/
|
||||||
child(context: LogContext): Logger {
|
child(context: LogContext): Logger {
|
||||||
return new Logger(this.serviceName, { ...this.context, ...context });
|
return new Logger((this.pino.bindings() as any).service, { ...this.context, ...context });
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add persistent context to this logger
|
|
||||||
*/
|
|
||||||
addContext(context: LogContext): void {
|
|
||||||
this.context = { ...this.context, ...context };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the underlying Pino logger
|
|
||||||
*/
|
|
||||||
getPinoLogger(): pino.Logger {
|
|
||||||
return this.pino;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gracefully close all transports
|
|
||||||
*/
|
|
||||||
async close(): Promise<void> {
|
|
||||||
// Pino doesn't require explicit closing like Winston
|
|
||||||
// But we can flush any pending logs
|
|
||||||
this.pino.flush();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a default logger instance for a service
|
* Main factory function
|
||||||
*/
|
*/
|
||||||
export function getLogger(serviceName: string, context?: LogContext): Logger {
|
export function getLogger(serviceName: string, context?: LogContext): Logger {
|
||||||
return new Logger(serviceName, context);
|
return new Logger(serviceName, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shutdown all logger instances gracefully
|
* Keep backward compatibility
|
||||||
*/
|
*/
|
||||||
export async function shutdownLoggers(): Promise<void> {
|
export function createLogger(serviceName: string): pino.Logger {
|
||||||
// Flush all logger instances
|
return getPinoLogger(serviceName);
|
||||||
const flushPromises = Array.from(loggerInstances.values()).map(logger => {
|
|
||||||
logger.flush();
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(flushPromises);
|
|
||||||
loggerInstances.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export types for convenience
|
// Export types for convenience
|
||||||
|
|
|
||||||
|
|
@ -1,462 +0,0 @@
|
||||||
/**
|
|
||||||
* Comprehensive Hono middleware for request logging with Pino integration
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Context, Next } from 'hono';
|
|
||||||
import { Logger } from './logger';
|
|
||||||
import { generateCorrelationId, createTimer, sanitizeMetadata, LogThrottle } from './utils';
|
|
||||||
|
|
||||||
export interface LoggingMiddlewareOptions {
|
|
||||||
logger?: Logger;
|
|
||||||
serviceName?: string;
|
|
||||||
skipPaths?: string[];
|
|
||||||
skipSuccessfulRequests?: boolean;
|
|
||||||
logRequestBody?: boolean;
|
|
||||||
logResponseBody?: boolean;
|
|
||||||
maxBodySize?: number;
|
|
||||||
enablePerformanceMetrics?: boolean;
|
|
||||||
enableThrottling?: boolean;
|
|
||||||
throttleConfig?: {
|
|
||||||
maxLogs?: number;
|
|
||||||
windowMs?: number;
|
|
||||||
};
|
|
||||||
sensitiveHeaders?: string[];
|
|
||||||
redactSensitiveData?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hono middleware for HTTP request/response logging with enhanced features
|
|
||||||
*/
|
|
||||||
export function loggingMiddleware(options: LoggingMiddlewareOptions = {}) {
|
|
||||||
const {
|
|
||||||
serviceName = 'unknown-service',
|
|
||||||
skipPaths = ['/health', '/metrics', '/favicon.ico'],
|
|
||||||
skipSuccessfulRequests = false,
|
|
||||||
logRequestBody = false,
|
|
||||||
logResponseBody = false,
|
|
||||||
maxBodySize = 1024,
|
|
||||||
enablePerformanceMetrics = true,
|
|
||||||
enableThrottling = false,
|
|
||||||
throttleConfig = { maxLogs: 100, windowMs: 60000 },
|
|
||||||
sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token'],
|
|
||||||
redactSensitiveData = true
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
// Create logger if not provided
|
|
||||||
const logger = options.logger || new Logger(serviceName);
|
|
||||||
|
|
||||||
// Create throttle instance if enabled
|
|
||||||
const throttle = enableThrottling ? new LogThrottle(throttleConfig.maxLogs, throttleConfig.windowMs) : null;
|
|
||||||
|
|
||||||
return async (c: Context, next: Next) => {
|
|
||||||
const url = new URL(c.req.url);
|
|
||||||
const path = url.pathname;
|
|
||||||
|
|
||||||
// Skip certain paths
|
|
||||||
if (skipPaths.some(skipPath => path.startsWith(skipPath))) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check throttling
|
|
||||||
const throttleKey = `${c.req.method}-${path}`;
|
|
||||||
if (throttle && !throttle.shouldLog(throttleKey)) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate correlation ID
|
|
||||||
const correlationId = generateCorrelationId();
|
|
||||||
// Store correlation ID in context for later use
|
|
||||||
c.set('correlationId', correlationId);
|
|
||||||
// Set correlation ID as response header
|
|
||||||
c.header('x-correlation-id', correlationId);
|
|
||||||
|
|
||||||
// Start timer
|
|
||||||
const timer = createTimer(`${c.req.method} ${path}`);
|
|
||||||
|
|
||||||
// Extract request headers (sanitized)
|
|
||||||
const rawHeaders = Object.fromEntries(
|
|
||||||
Array.from(c.req.raw.headers.entries())
|
|
||||||
);
|
|
||||||
|
|
||||||
const headers = redactSensitiveData
|
|
||||||
? Object.fromEntries(
|
|
||||||
Object.entries(rawHeaders).map(([key, value]) => [
|
|
||||||
key,
|
|
||||||
sensitiveHeaders.some(sensitive => key.toLowerCase().includes(sensitive.toLowerCase()))
|
|
||||||
? '[REDACTED]'
|
|
||||||
: value
|
|
||||||
])
|
|
||||||
)
|
|
||||||
: rawHeaders; // Extract request metadata following LogMetadata.request structure
|
|
||||||
const requestMetadata: any = {
|
|
||||||
method: c.req.method,
|
|
||||||
url: c.req.url,
|
|
||||||
path: path,
|
|
||||||
userAgent: c.req.header('user-agent'),
|
|
||||||
contentType: c.req.header('content-type'),
|
|
||||||
contentLength: c.req.header('content-length'),
|
|
||||||
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown',
|
|
||||||
headers: headers
|
|
||||||
};
|
|
||||||
// Add request body if enabled
|
|
||||||
if (logRequestBody) {
|
|
||||||
try {
|
|
||||||
const body = await c.req.text();
|
|
||||||
if (body) {
|
|
||||||
requestMetadata.body = body.length > maxBodySize
|
|
||||||
? body.substring(0, maxBodySize) + '...[truncated]'
|
|
||||||
: body;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Body might not be available or already consumed
|
|
||||||
requestMetadata.bodyError = 'Unable to read request body';
|
|
||||||
}
|
|
||||||
} // Log request start with structured data
|
|
||||||
logger.http('HTTP Request started', {
|
|
||||||
method: c.req.method,
|
|
||||||
url: c.req.url,
|
|
||||||
userAgent: c.req.header('user-agent'),
|
|
||||||
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
|
|
||||||
});
|
|
||||||
|
|
||||||
let error: Error | null = null;
|
|
||||||
let responseTime: number = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Process request
|
|
||||||
await next();
|
|
||||||
} catch (err) {
|
|
||||||
error = err instanceof Error ? err : new Error(String(err));
|
|
||||||
throw error; // Re-throw to maintain error handling flow
|
|
||||||
} finally {
|
|
||||||
// Calculate response time
|
|
||||||
const timing = timer.end();
|
|
||||||
responseTime = timing.duration;
|
|
||||||
|
|
||||||
// Get response information
|
|
||||||
const response = c.res;
|
|
||||||
const status = response.status;
|
|
||||||
|
|
||||||
// Determine log level based on status code
|
|
||||||
const isError = status >= 400 || error !== null;
|
|
||||||
const isSuccess = status >= 200 && status < 300;
|
|
||||||
const isRedirect = status >= 300 && status < 400;
|
|
||||||
|
|
||||||
// Skip successful requests if configured
|
|
||||||
if (skipSuccessfulRequests && isSuccess && !error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract response headers (sanitized)
|
|
||||||
const rawResponseHeaders = Object.fromEntries(
|
|
||||||
Array.from(response.headers.entries())
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseHeaders = redactSensitiveData
|
|
||||||
? Object.fromEntries(
|
|
||||||
Object.entries(rawResponseHeaders).map(([key, value]) => [
|
|
||||||
key,
|
|
||||||
sensitiveHeaders.some(sensitive => key.toLowerCase().includes(sensitive.toLowerCase()))
|
|
||||||
? '[REDACTED]'
|
|
||||||
: value
|
|
||||||
])
|
|
||||||
)
|
|
||||||
: rawResponseHeaders;
|
|
||||||
|
|
||||||
const responseMetadata: any = {
|
|
||||||
statusCode: status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers: responseHeaders,
|
|
||||||
contentLength: response.headers.get('content-length'),
|
|
||||||
contentType: response.headers.get('content-type')
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add response body if enabled and not an error
|
|
||||||
if (logResponseBody && !isError) {
|
|
||||||
try {
|
|
||||||
const responseBody = await response.clone().text();
|
|
||||||
if (responseBody) {
|
|
||||||
responseMetadata.body = responseBody.length > maxBodySize
|
|
||||||
? responseBody.substring(0, maxBodySize) + '...[truncated]'
|
|
||||||
: responseBody;
|
|
||||||
}
|
|
||||||
} catch (bodyError) {
|
|
||||||
responseMetadata.bodyError = 'Unable to read response body';
|
|
||||||
}
|
|
||||||
} // Performance metrics matching LogMetadata.performance structure
|
|
||||||
const performanceMetrics = {
|
|
||||||
operation: timing.operation,
|
|
||||||
duration: timing.duration,
|
|
||||||
startTime: enablePerformanceMetrics ? timing.startTime : undefined,
|
|
||||||
endTime: enablePerformanceMetrics ? timing.endTime : undefined
|
|
||||||
}; // Create comprehensive log entry
|
|
||||||
const logEntry: any = {
|
|
||||||
correlationId,
|
|
||||||
request: requestMetadata,
|
|
||||||
response: responseMetadata,
|
|
||||||
performance: performanceMetrics,
|
|
||||||
type: error ? 'custom' : isError ? 'custom' : 'http_request'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sanitize if needed
|
|
||||||
const finalLogEntry = redactSensitiveData ? sanitizeMetadata(logEntry) : logEntry; // Log based on status code and errors
|
|
||||||
if (error) {
|
|
||||||
logger.error('HTTP Request error', error, finalLogEntry);
|
|
||||||
} else if (isError) {
|
|
||||||
logger.warn('HTTP Request failed', finalLogEntry);
|
|
||||||
} else if (isRedirect) {
|
|
||||||
logger.info('HTTP Request redirected', finalLogEntry);
|
|
||||||
} else {
|
|
||||||
logger.http('HTTP Request completed', {
|
|
||||||
method: c.req.method,
|
|
||||||
url: c.req.url,
|
|
||||||
statusCode: status,
|
|
||||||
responseTime: timing.duration,
|
|
||||||
userAgent: c.req.header('user-agent'),
|
|
||||||
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error logging middleware for Hono
|
|
||||||
*/
|
|
||||||
export function errorLoggingMiddleware(logger?: Logger) {
|
|
||||||
return async (c: Context, next: Next) => {
|
|
||||||
const errorLogger = logger || new Logger('error-handler');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await next();
|
|
||||||
} catch (err) {
|
|
||||||
const correlationId = c.get('correlationId') || c.req.header('x-correlation-id');
|
|
||||||
const url = new URL(c.req.url);
|
|
||||||
|
|
||||||
const requestMetadata = {
|
|
||||||
method: c.req.method,
|
|
||||||
url: c.req.url,
|
|
||||||
path: url.pathname,
|
|
||||||
userAgent: c.req.header('user-agent'),
|
|
||||||
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
|
|
||||||
};
|
|
||||||
|
|
||||||
errorLogger.error('Unhandled HTTP error', err instanceof Error ? err : new Error(String(err)), {
|
|
||||||
correlationId,
|
|
||||||
request: requestMetadata,
|
|
||||||
response: {
|
|
||||||
statusCode: c.res.status
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-throw the error so Hono can handle it
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a child logger with request context for Hono
|
|
||||||
*/
|
|
||||||
export function createRequestLogger(c: Context, baseLogger: Logger): Logger {
|
|
||||||
const correlationId = c.get('correlationId') || c.req.header('x-correlation-id');
|
|
||||||
const url = new URL(c.req.url);
|
|
||||||
|
|
||||||
return baseLogger.child({
|
|
||||||
correlationId,
|
|
||||||
requestId: correlationId,
|
|
||||||
method: c.req.method,
|
|
||||||
path: url.pathname,
|
|
||||||
userAgent: c.req.header('user-agent'),
|
|
||||||
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performance monitoring middleware for specific operations
|
|
||||||
*/
|
|
||||||
export function performanceMiddleware(operationName?: string, logger?: Logger) {
|
|
||||||
return async (c: Context, next: Next) => {
|
|
||||||
const perfLogger = logger || new Logger('performance');
|
|
||||||
const operation = operationName || `${c.req.method} ${new URL(c.req.url).pathname}`;
|
|
||||||
const timer = createTimer(operation);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await next();
|
|
||||||
|
|
||||||
const timing = timer.end();
|
|
||||||
perfLogger.info('Operation completed', {
|
|
||||||
type: 'performance',
|
|
||||||
performance: timing,
|
|
||||||
correlationId: c.get('correlationId')
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const timing = timer.end();
|
|
||||||
perfLogger.warn('Operation failed', {
|
|
||||||
type: 'performance',
|
|
||||||
performance: timing,
|
|
||||||
correlationId: c.get('correlationId'),
|
|
||||||
error: error instanceof Error ? {
|
|
||||||
name: error.name,
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack
|
|
||||||
} : { message: String(error) }
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security event logging middleware
|
|
||||||
*/
|
|
||||||
export function securityMiddleware(logger?: Logger) {
|
|
||||||
return async (c: Context, next: Next) => {
|
|
||||||
const secLogger = logger || new Logger('security');
|
|
||||||
const url = new URL(c.req.url);
|
|
||||||
|
|
||||||
// Log authentication attempts
|
|
||||||
const authHeader = c.req.header('authorization');
|
|
||||||
if (authHeader) {
|
|
||||||
secLogger.info('Authentication attempt', {
|
|
||||||
type: 'security_event',
|
|
||||||
security: {
|
|
||||||
type: 'authentication',
|
|
||||||
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown',
|
|
||||||
resource: url.pathname,
|
|
||||||
action: 'access_attempt'
|
|
||||||
},
|
|
||||||
correlationId: c.get('correlationId')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
|
||||||
|
|
||||||
// Log access control
|
|
||||||
const status = c.res.status;
|
|
||||||
if (status === 401 || status === 403) {
|
|
||||||
secLogger.warn('Access denied', {
|
|
||||||
type: 'security_event',
|
|
||||||
security: {
|
|
||||||
type: 'authorization',
|
|
||||||
result: 'failure',
|
|
||||||
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown',
|
|
||||||
resource: url.pathname,
|
|
||||||
action: c.req.method,
|
|
||||||
severity: status === 401 ? 'medium' : 'high'
|
|
||||||
},
|
|
||||||
correlationId: c.get('correlationId')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Business event logging middleware for trading operations
|
|
||||||
*/
|
|
||||||
export function businessEventMiddleware(logger?: Logger) {
|
|
||||||
return async (c: Context, next: Next) => {
|
|
||||||
const bizLogger = logger || new Logger('business');
|
|
||||||
const url = new URL(c.req.url);
|
|
||||||
const path = url.pathname;
|
|
||||||
|
|
||||||
// Check if this is a business-critical endpoint
|
|
||||||
const businessEndpoints = [
|
|
||||||
'/api/orders',
|
|
||||||
'/api/trades',
|
|
||||||
'/api/portfolio',
|
|
||||||
'/api/strategies',
|
|
||||||
'/api/signals'
|
|
||||||
];
|
|
||||||
|
|
||||||
const isBusinessEndpoint = businessEndpoints.some(endpoint => path.startsWith(endpoint));
|
|
||||||
|
|
||||||
if (isBusinessEndpoint) {
|
|
||||||
const timer = createTimer(`business_${c.req.method}_${path}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await next();
|
|
||||||
|
|
||||||
const timing = timer.end();
|
|
||||||
const status = c.res.status;
|
|
||||||
|
|
||||||
bizLogger.info('Business operation completed', {
|
|
||||||
type: 'business_event',
|
|
||||||
business: {
|
|
||||||
type: 'trading_operation',
|
|
||||||
action: c.req.method,
|
|
||||||
result: status >= 200 && status < 300 ? 'success' : 'failure'
|
|
||||||
},
|
|
||||||
performance: timing,
|
|
||||||
correlationId: c.get('correlationId')
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const timing = timer.end();
|
|
||||||
|
|
||||||
bizLogger.error('Business operation failed', error, {
|
|
||||||
type: 'business_event',
|
|
||||||
business: {
|
|
||||||
type: 'trading_operation',
|
|
||||||
action: c.req.method,
|
|
||||||
result: 'failure'
|
|
||||||
},
|
|
||||||
performance: timing,
|
|
||||||
correlationId: c.get('correlationId')
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comprehensive logging middleware that combines all logging features
|
|
||||||
*/
|
|
||||||
export function comprehensiveLoggingMiddleware(options: LoggingMiddlewareOptions & {
|
|
||||||
enableSecurity?: boolean;
|
|
||||||
enableBusiness?: boolean;
|
|
||||||
enablePerformance?: boolean;
|
|
||||||
} = {}) {
|
|
||||||
const {
|
|
||||||
enableSecurity = true,
|
|
||||||
enableBusiness = true,
|
|
||||||
enablePerformance = true,
|
|
||||||
...loggingOptions
|
|
||||||
} = options;
|
|
||||||
return async (c: Context, next: Next) => {
|
|
||||||
const middlewares: Array<(c: Context, next: Next) => Promise<void>> = [];
|
|
||||||
|
|
||||||
// Add security middleware
|
|
||||||
if (enableSecurity) {
|
|
||||||
middlewares.push(securityMiddleware(options.logger));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add performance middleware
|
|
||||||
if (enablePerformance) {
|
|
||||||
middlewares.push(performanceMiddleware(undefined, options.logger));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add business event middleware
|
|
||||||
if (enableBusiness) {
|
|
||||||
middlewares.push(businessEventMiddleware(options.logger));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add main logging middleware
|
|
||||||
middlewares.push(loggingMiddleware(loggingOptions));
|
|
||||||
|
|
||||||
// Execute middleware chain
|
|
||||||
let index = 0;
|
|
||||||
async function dispatch(i: number): Promise<void> {
|
|
||||||
if (i >= middlewares.length) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const middleware = middlewares[i];
|
|
||||||
return middleware(c, () => dispatch(i + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatch(0);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +1,16 @@
|
||||||
/**
|
/**
|
||||||
* Type definitions for the logger library
|
* Simplified type definitions for the logger library
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Standard log levels
|
// Standard log levels (simplified to pino defaults)
|
||||||
export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly';
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
// Context that persists across log calls
|
// Context that persists across log calls
|
||||||
export interface LogContext {
|
export interface LogContext {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
requestId?: string;
|
|
||||||
sessionId?: string;
|
|
||||||
userId?: string;
|
|
||||||
traceId?: string;
|
|
||||||
spanId?: string;
|
|
||||||
correlationId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata for individual log entries
|
// Metadata for individual log entries
|
||||||
export interface LogMetadata {
|
export interface LogMetadata {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
correlationId?: string;
|
|
||||||
error?: {
|
|
||||||
name: string;
|
|
||||||
message: string;
|
|
||||||
stack?: string;
|
|
||||||
} | any;request?: {
|
|
||||||
method?: string;
|
|
||||||
url?: string;
|
|
||||||
path?: string;
|
|
||||||
statusCode?: number;
|
|
||||||
responseTime?: number;
|
|
||||||
userAgent?: string;
|
|
||||||
ip?: string;
|
|
||||||
body?: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
contentType?: string;
|
|
||||||
contentLength?: string;
|
|
||||||
};
|
|
||||||
performance?: {
|
|
||||||
operation: string;
|
|
||||||
duration: number;
|
|
||||||
startTime?: number;
|
|
||||||
endTime?: number;
|
|
||||||
};
|
|
||||||
business?: {
|
|
||||||
type: string;
|
|
||||||
entity?: string;
|
|
||||||
action?: string;
|
|
||||||
result?: 'success' | 'failure' | 'partial';
|
|
||||||
amount?: number;
|
|
||||||
symbol?: string;
|
|
||||||
};
|
|
||||||
security?: {
|
|
||||||
type: 'authentication' | 'authorization' | 'access' | 'vulnerability';
|
|
||||||
user?: string;
|
|
||||||
resource?: string;
|
|
||||||
action?: string;
|
|
||||||
result?: 'success' | 'failure';
|
|
||||||
ip?: string;
|
|
||||||
severity?: 'low' | 'medium' | 'high' | 'critical';
|
|
||||||
};
|
|
||||||
type?: 'http_request' | 'performance' | 'business_event' | 'security_event' | 'system' | 'custom';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger configuration options
|
|
||||||
export interface LoggerOptions {
|
|
||||||
level?: LogLevel;
|
|
||||||
enableLoki?: boolean;
|
|
||||||
enableFile?: boolean;
|
|
||||||
enableConsole?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loki-specific configuration
|
|
||||||
export interface LokiTransportOptions {
|
|
||||||
host: string;
|
|
||||||
port?: number;
|
|
||||||
url?: string;
|
|
||||||
username?: string;
|
|
||||||
password?: string;
|
|
||||||
tenantId?: string;
|
|
||||||
labels?: Record<string, string>;
|
|
||||||
batchSize?: number;
|
|
||||||
interval?: number;
|
|
||||||
timeout?: number;
|
|
||||||
json?: boolean;
|
|
||||||
batching?: boolean;
|
|
||||||
basicAuth?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance timer utility type
|
|
||||||
export interface PerformanceTimer {
|
|
||||||
operation: string;
|
|
||||||
startTime: number;
|
|
||||||
end(): { operation: string; duration: number; startTime: number; endTime: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log entry structure for Loki
|
|
||||||
export interface LokiLogEntry {
|
|
||||||
timestamp: string;
|
|
||||||
level: LogLevel;
|
|
||||||
message: string;
|
|
||||||
service: string;
|
|
||||||
environment?: string;
|
|
||||||
version?: string;
|
|
||||||
metadata?: LogMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Structured log format
|
|
||||||
export interface StructuredLog {
|
|
||||||
timestamp: string;
|
|
||||||
level: LogLevel;
|
|
||||||
service: string;
|
|
||||||
message: string;
|
|
||||||
context?: LogContext;
|
|
||||||
metadata?: LogMetadata;
|
|
||||||
environment?: string;
|
|
||||||
version?: string;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
/**
|
|
||||||
* Utility functions for logging operations
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { PerformanceTimer, LogMetadata } from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a performance timer to measure operation duration
|
|
||||||
*/
|
|
||||||
export function createTimer(operation: string): PerformanceTimer {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
return {
|
|
||||||
operation,
|
|
||||||
startTime,
|
|
||||||
end() {
|
|
||||||
const endTime = Date.now();
|
|
||||||
return {
|
|
||||||
operation,
|
|
||||||
duration: endTime - startTime,
|
|
||||||
startTime,
|
|
||||||
endTime
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format error for logging
|
|
||||||
*/
|
|
||||||
export function formatError(error: Error | any): LogMetadata['error'] {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return {
|
|
||||||
name: error.name,
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === 'object' && error !== null) {
|
|
||||||
return {
|
|
||||||
name: error.constructor?.name || 'UnknownError',
|
|
||||||
message: error.message || error.toString(),
|
|
||||||
...error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'UnknownError',
|
|
||||||
message: String(error)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitize sensitive data from log metadata
|
|
||||||
*/
|
|
||||||
export function sanitizeMetadata(metadata: LogMetadata): LogMetadata {
|
|
||||||
const sensitiveKeys = [
|
|
||||||
'password', 'token', 'secret', 'key', 'apiKey', 'api_key',
|
|
||||||
'authorization', 'auth', 'credential', 'credentials'
|
|
||||||
];
|
|
||||||
|
|
||||||
function sanitizeValue(value: any): any {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return '[REDACTED]';
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.map(sanitizeValue);
|
|
||||||
}
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
const sanitized: any = {};
|
|
||||||
for (const [key, val] of Object.entries(value)) {
|
|
||||||
const lowerKey = key.toLowerCase();
|
|
||||||
if (sensitiveKeys.some(sensitive => lowerKey.includes(sensitive))) {
|
|
||||||
sanitized[key] = '[REDACTED]';
|
|
||||||
} else {
|
|
||||||
sanitized[key] = sanitizeValue(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitized: LogMetadata = {};
|
|
||||||
for (const [key, value] of Object.entries(metadata)) {
|
|
||||||
const lowerKey = key.toLowerCase();
|
|
||||||
if (sensitiveKeys.some(sensitive => lowerKey.includes(sensitive))) {
|
|
||||||
sanitized[key] = '[REDACTED]';
|
|
||||||
} else {
|
|
||||||
sanitized[key] = sanitizeValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a correlation ID for request tracking
|
|
||||||
*/
|
|
||||||
export function generateCorrelationId(): string {
|
|
||||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract relevant HTTP request information for logging
|
|
||||||
*/
|
|
||||||
export function extractHttpMetadata(req: any): LogMetadata['request'] {
|
|
||||||
if (!req) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url || req.originalUrl,
|
|
||||||
userAgent: req.get?.('User-Agent') || req.headers?.['user-agent'],
|
|
||||||
ip: req.ip || req.connection?.remoteAddress || req.headers?.['x-forwarded-for']
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create standardized business event metadata
|
|
||||||
*/
|
|
||||||
export function createBusinessEvent(
|
|
||||||
type: string,
|
|
||||||
action: string,
|
|
||||||
options?: {
|
|
||||||
entity?: string;
|
|
||||||
result?: 'success' | 'failure' | 'partial';
|
|
||||||
amount?: number;
|
|
||||||
symbol?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
): LogMetadata {
|
|
||||||
return {
|
|
||||||
business: {
|
|
||||||
type,
|
|
||||||
action,
|
|
||||||
...options
|
|
||||||
},
|
|
||||||
type: 'business_event'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create standardized security event metadata
|
|
||||||
*/
|
|
||||||
export function createSecurityEvent(
|
|
||||||
type: 'authentication' | 'authorization' | 'access' | 'vulnerability',
|
|
||||||
options?: {
|
|
||||||
user?: string;
|
|
||||||
resource?: string;
|
|
||||||
action?: string;
|
|
||||||
result?: 'success' | 'failure';
|
|
||||||
ip?: string;
|
|
||||||
severity?: 'low' | 'medium' | 'high' | 'critical';
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
): LogMetadata {
|
|
||||||
return {
|
|
||||||
security: {
|
|
||||||
type,
|
|
||||||
...options
|
|
||||||
},
|
|
||||||
type: 'security_event'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mask sensitive data in strings
|
|
||||||
*/
|
|
||||||
export function maskSensitiveData(str: string): string {
|
|
||||||
// Credit card numbers
|
|
||||||
str = str.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '****-****-****-****');
|
|
||||||
|
|
||||||
// Email addresses
|
|
||||||
str = str.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '***@***.***');
|
|
||||||
|
|
||||||
// API keys (common patterns)
|
|
||||||
str = str.replace(/\b[A-Za-z0-9]{32,}\b/g, '[API_KEY]');
|
|
||||||
|
|
||||||
// JWT tokens
|
|
||||||
str = str.replace(/eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g, '[JWT_TOKEN]');
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate log entry size for batching
|
|
||||||
*/
|
|
||||||
export function calculateLogSize(entry: any): number {
|
|
||||||
return JSON.stringify(entry).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throttle logging to prevent spam
|
|
||||||
*/
|
|
||||||
export class LogThrottle {
|
|
||||||
private counters = new Map<string, { count: number; lastReset: number }>();
|
|
||||||
private readonly maxLogs: number;
|
|
||||||
private readonly windowMs: number;
|
|
||||||
|
|
||||||
constructor(maxLogs = 100, windowMs = 60000) {
|
|
||||||
this.maxLogs = maxLogs;
|
|
||||||
this.windowMs = windowMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldLog(key: string): boolean {
|
|
||||||
const now = Date.now();
|
|
||||||
const counter = this.counters.get(key);
|
|
||||||
|
|
||||||
if (!counter || now - counter.lastReset > this.windowMs) {
|
|
||||||
this.counters.set(key, { count: 1, lastReset: now });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter.count >= this.maxLogs) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
counter.count++;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(key?: string): void {
|
|
||||||
if (key) {
|
|
||||||
this.counters.delete(key);
|
|
||||||
} else {
|
|
||||||
this.counters.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue