added logger
This commit is contained in:
parent
dd27f3bf2c
commit
58ae897e90
13 changed files with 1493 additions and 12 deletions
342
libs/logger/README.md
Normal file
342
libs/logger/README.md
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
# @stock-bot/logger
|
||||
|
||||
Enhanced logging library with Loki integration for the Stock Bot platform.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Multiple Log Levels**: debug, info, warn, error, http
|
||||
- 🌐 **Loki Integration**: Centralized logging with Grafana visualization
|
||||
- 📁 **File Logging**: Daily rotating log files with compression
|
||||
- 🎨 **Console Logging**: Colored, formatted console output
|
||||
- 📊 **Structured Logging**: JSON-formatted logs with metadata
|
||||
- ⚡ **Performance Optimized**: Batching and async logging
|
||||
- 🔐 **Security**: Automatic sensitive data masking
|
||||
- 🎭 **Express Middleware**: Request/response logging
|
||||
- 📈 **Business Events**: Specialized logging for trading operations
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd libs/logger
|
||||
npm install
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Simple Logging
|
||||
|
||||
```typescript
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('my-service');
|
||||
|
||||
logger.info('Service started');
|
||||
logger.warn('This is a warning');
|
||||
logger.error('An error occurred', new Error('Something went wrong'));
|
||||
```
|
||||
|
||||
### With Context
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@stock-bot/logger';
|
||||
|
||||
const logger = new Logger('trading-service', {
|
||||
userId: '12345',
|
||||
sessionId: 'abc-def-ghi'
|
||||
});
|
||||
|
||||
logger.info('Trade executed', {
|
||||
symbol: 'AAPL',
|
||||
quantity: 100,
|
||||
price: 150.25
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Logging
|
||||
|
||||
```typescript
|
||||
import { getLogger, createTimer } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('data-processor');
|
||||
const timer = createTimer('data-processing');
|
||||
|
||||
// ... do some work ...
|
||||
|
||||
const timing = timer.end();
|
||||
logger.performance('Data processing completed', timing);
|
||||
```
|
||||
|
||||
### Business Events
|
||||
|
||||
```typescript
|
||||
import { getLogger, createBusinessEvent } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('order-service');
|
||||
|
||||
logger.business('Order placed', createBusinessEvent(
|
||||
'order',
|
||||
'place',
|
||||
{
|
||||
entity: 'order-123',
|
||||
result: 'success',
|
||||
symbol: 'TSLA',
|
||||
amount: 50000
|
||||
}
|
||||
));
|
||||
```
|
||||
|
||||
### Security Events
|
||||
|
||||
```typescript
|
||||
import { getLogger, createSecurityEvent } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('auth-service');
|
||||
|
||||
logger.security('Failed login attempt', createSecurityEvent(
|
||||
'authentication',
|
||||
{
|
||||
user: 'john@example.com',
|
||||
result: 'failure',
|
||||
ip: '192.168.1.100',
|
||||
severity: 'medium'
|
||||
}
|
||||
));
|
||||
```
|
||||
|
||||
## Express Middleware
|
||||
|
||||
### Basic Request Logging
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { loggingMiddleware } from '@stock-bot/logger';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(loggingMiddleware({
|
||||
serviceName: 'api-gateway',
|
||||
skipPaths: ['/health', '/metrics']
|
||||
}));
|
||||
```
|
||||
|
||||
### Error Logging
|
||||
|
||||
```typescript
|
||||
import { errorLoggingMiddleware, getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('api-gateway');
|
||||
|
||||
// Add after your routes but before error handlers
|
||||
app.use(errorLoggingMiddleware(logger));
|
||||
```
|
||||
|
||||
### Request-scoped Logger
|
||||
|
||||
```typescript
|
||||
import { createRequestLogger, getLogger } from '@stock-bot/logger';
|
||||
|
||||
const baseLogger = getLogger('api-gateway');
|
||||
|
||||
app.use((req, res, next) => {
|
||||
req.logger = createRequestLogger(req, baseLogger);
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/api/data', (req, res) => {
|
||||
req.logger.info('Processing data request');
|
||||
// ... handle request ...
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The logger uses configuration from `@stock-bot/config`. Key environment variables:
|
||||
|
||||
```bash
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
LOG_CONSOLE=true
|
||||
LOG_FILE=true
|
||||
LOG_FILE_PATH=./logs
|
||||
|
||||
# Loki
|
||||
LOKI_HOST=localhost
|
||||
LOKI_PORT=3100
|
||||
LOKI_BATCH_SIZE=1024
|
||||
LOKI_FLUSH_INTERVAL_MS=5000
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Child Loggers
|
||||
|
||||
```typescript
|
||||
const parentLogger = getLogger('trading-service');
|
||||
const orderLogger = parentLogger.child({
|
||||
module: 'order-processing',
|
||||
orderId: '12345'
|
||||
});
|
||||
|
||||
orderLogger.info('Order validated'); // Will include parent context
|
||||
```
|
||||
|
||||
### Custom Transports
|
||||
|
||||
```typescript
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = createLogger('custom-service', {
|
||||
level: 'debug',
|
||||
enableLoki: true,
|
||||
enableFile: false,
|
||||
enableConsole: true
|
||||
});
|
||||
```
|
||||
|
||||
### Sensitive Data Masking
|
||||
|
||||
```typescript
|
||||
import { sanitizeMetadata, maskSensitiveData } from '@stock-bot/logger';
|
||||
|
||||
const unsafeData = {
|
||||
username: 'john',
|
||||
password: 'secret123',
|
||||
apiKey: 'abc123def456'
|
||||
};
|
||||
|
||||
const safeData = sanitizeMetadata(unsafeData);
|
||||
// { username: 'john', password: '[REDACTED]', apiKey: '[REDACTED]' }
|
||||
|
||||
const message = maskSensitiveData('User API key: abc123def456');
|
||||
// 'User API key: [API_KEY]'
|
||||
```
|
||||
|
||||
### Log Throttling
|
||||
|
||||
```typescript
|
||||
import { LogThrottle } from '@stock-bot/logger';
|
||||
|
||||
const throttle = new LogThrottle(10, 60000); // 10 logs per minute
|
||||
|
||||
if (throttle.shouldLog('error-key')) {
|
||||
logger.error('This error will be throttled');
|
||||
}
|
||||
```
|
||||
|
||||
## Viewing Logs
|
||||
|
||||
### Grafana Dashboard
|
||||
|
||||
1. Start the monitoring stack: `docker-compose up grafana loki`
|
||||
2. Open Grafana at http://localhost:3000
|
||||
3. Use the "Stock Bot Logs" dashboard
|
||||
4. Query logs with LogQL: `{service="your-service"}`
|
||||
|
||||
### Log Files
|
||||
|
||||
When file logging is enabled, logs are written to:
|
||||
- `./logs/{service-name}-YYYY-MM-DD.log` - All logs
|
||||
- `./logs/{service-name}-error-YYYY-MM-DD.log` - Error logs only
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use appropriate log levels**:
|
||||
- `debug`: Detailed development information
|
||||
- `info`: General operational messages
|
||||
- `warn`: Potential issues
|
||||
- `error`: Actual errors requiring attention
|
||||
|
||||
2. **Include context**: Always provide relevant metadata
|
||||
```typescript
|
||||
logger.info('Trade executed', { symbol, quantity, price, orderId });
|
||||
```
|
||||
|
||||
3. **Use structured logging**: Avoid string concatenation
|
||||
```typescript
|
||||
// Good
|
||||
logger.info('User logged in', { userId, ip, userAgent });
|
||||
|
||||
// Avoid
|
||||
logger.info(`User ${userId} logged in from ${ip}`);
|
||||
```
|
||||
|
||||
4. **Handle sensitive data**: Use sanitization utilities
|
||||
```typescript
|
||||
const safeMetadata = sanitizeMetadata(requestData);
|
||||
logger.info('API request', safeMetadata);
|
||||
```
|
||||
|
||||
5. **Use correlation IDs**: Track requests across services
|
||||
```typescript
|
||||
const logger = getLogger('service').child({
|
||||
correlationId: req.headers['x-correlation-id']
|
||||
});
|
||||
```
|
||||
|
||||
## Integration with Services
|
||||
|
||||
To use in your service:
|
||||
|
||||
1. Add dependency to your service's `package.json`:
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@stock-bot/logger": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Update your service's `tsconfig.json` references:
|
||||
```json
|
||||
{
|
||||
"references": [
|
||||
{ "path": "../../../libs/logger" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
3. Import and use:
|
||||
```typescript
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('my-service');
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Logs are batched and sent asynchronously to Loki
|
||||
- File logging uses daily rotation to prevent large files
|
||||
- Console logging can be disabled in production
|
||||
- Use log throttling for high-frequency events
|
||||
- Sensitive data is automatically masked
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Logs not appearing in Loki
|
||||
|
||||
1. Check Loki connection:
|
||||
```bash
|
||||
curl http://localhost:3100/ready
|
||||
```
|
||||
|
||||
2. Verify environment variables:
|
||||
```bash
|
||||
echo $LOKI_HOST $LOKI_PORT
|
||||
```
|
||||
|
||||
3. Check container logs:
|
||||
```bash
|
||||
docker logs stock-bot-loki
|
||||
```
|
||||
|
||||
### High memory usage
|
||||
|
||||
- Reduce `LOKI_BATCH_SIZE` if batching too many logs
|
||||
- Decrease `LOKI_FLUSH_INTERVAL_MS` to flush more frequently
|
||||
- Disable file logging if not needed
|
||||
|
||||
### Missing logs
|
||||
|
||||
- Check log level configuration
|
||||
- Verify service name matches expectations
|
||||
- Ensure proper error handling around logger calls
|
||||
26
libs/logger/package.json
Normal file
26
libs/logger/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@stock-bot/logger",
|
||||
"version": "1.0.0",
|
||||
"description": "Enhanced logging library with Loki integration for stock-bot services",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/types": "workspace:*",
|
||||
"@stock-bot/config": "workspace:*",
|
||||
"winston": "^3.11.0",
|
||||
"winston-loki": "^6.0.8",
|
||||
"winston-daily-rotate-file": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.5.0",
|
||||
"jest": "^29.5.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
43
libs/logger/src/index.ts
Normal file
43
libs/logger/src/index.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* @stock-bot/logger - Enhanced logging library with Loki integration
|
||||
*
|
||||
* Main exports for the logger library
|
||||
*/
|
||||
|
||||
// Core logger classes and functions
|
||||
export { Logger, createLogger, getLogger, shutdownLoggers } from './logger';
|
||||
|
||||
// Utility functions
|
||||
export {
|
||||
createTimer,
|
||||
formatError,
|
||||
sanitizeMetadata,
|
||||
generateCorrelationId,
|
||||
extractHttpMetadata,
|
||||
createBusinessEvent,
|
||||
createSecurityEvent,
|
||||
maskSensitiveData,
|
||||
calculateLogSize,
|
||||
LogThrottle
|
||||
} from './utils';
|
||||
|
||||
// Express middleware
|
||||
export {
|
||||
loggingMiddleware,
|
||||
errorLoggingMiddleware,
|
||||
createRequestLogger
|
||||
} from './middleware';
|
||||
|
||||
// Type exports
|
||||
export type {
|
||||
LogLevel,
|
||||
LogContext,
|
||||
LogMetadata,
|
||||
LoggerOptions,
|
||||
LokiTransportOptions,
|
||||
PerformanceTimer,
|
||||
LokiLogEntry,
|
||||
StructuredLog
|
||||
} from './types';
|
||||
|
||||
export type { LoggingMiddlewareOptions } from './middleware';
|
||||
358
libs/logger/src/logger.ts
Normal file
358
libs/logger/src/logger.ts
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
/**
|
||||
* Enhanced logger with Loki integration for Stock Bot platform
|
||||
*
|
||||
* Features:
|
||||
* - Multiple log levels (debug, info, warn, error)
|
||||
* - Console and file logging
|
||||
* - Loki integration for centralized logging
|
||||
* - Structured logging with metadata
|
||||
* - Performance optimized with batching
|
||||
* - Service-specific context
|
||||
*/
|
||||
|
||||
import winston from 'winston';
|
||||
import LokiTransport from 'winston-loki';
|
||||
import DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import { loggingConfig, lokiConfig } from '@stock-bot/config';
|
||||
import type { LogLevel, LogContext, LogMetadata } from './types';
|
||||
|
||||
// Global logger instances cache
|
||||
const loggerInstances = new Map<string, winston.Logger>();
|
||||
|
||||
/**
|
||||
* Create or retrieve a logger instance for a specific service
|
||||
*/
|
||||
export function createLogger(serviceName: string, options?: {
|
||||
level?: LogLevel;
|
||||
enableLoki?: boolean;
|
||||
enableFile?: boolean;
|
||||
enableConsole?: boolean;
|
||||
}): winston.Logger {
|
||||
const key = `${serviceName}-${JSON.stringify(options || {})}`;
|
||||
|
||||
if (loggerInstances.has(key)) {
|
||||
return loggerInstances.get(key)!;
|
||||
}
|
||||
|
||||
const logger = buildLogger(serviceName, options);
|
||||
loggerInstances.set(key, logger);
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a winston logger with all configured transports
|
||||
*/
|
||||
function buildLogger(serviceName: string, options?: {
|
||||
level?: LogLevel;
|
||||
enableLoki?: boolean;
|
||||
enableFile?: boolean;
|
||||
enableConsole?: boolean;
|
||||
}): winston.Logger {
|
||||
const {
|
||||
level = loggingConfig.LOG_LEVEL as LogLevel,
|
||||
enableLoki = true,
|
||||
enableFile = loggingConfig.LOG_FILE,
|
||||
enableConsole = loggingConfig.LOG_CONSOLE
|
||||
} = options || {}; // Base logger configuration
|
||||
const transports: winston.transport[] = [];
|
||||
|
||||
// Console transport
|
||||
if (enableConsole) {
|
||||
transports.push(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple(),
|
||||
winston.format.printf(({ timestamp, level, service, message, metadata }) => {
|
||||
const meta = metadata && Object.keys(metadata).length > 0
|
||||
? `\n${JSON.stringify(metadata, null, 2)}`
|
||||
: '';
|
||||
return `${timestamp} [${level}] [${service}] ${message}${meta}`;
|
||||
})
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
// File transport with daily rotation
|
||||
if (enableFile) {
|
||||
// General log file
|
||||
transports.push(new DailyRotateFile({
|
||||
filename: `${loggingConfig.LOG_FILE_PATH}/${serviceName}-%DATE%.log`,
|
||||
datePattern: loggingConfig.LOG_FILE_DATE_PATTERN,
|
||||
zippedArchive: true,
|
||||
maxSize: loggingConfig.LOG_FILE_MAX_SIZE,
|
||||
maxFiles: loggingConfig.LOG_FILE_MAX_FILES,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
)
|
||||
}));
|
||||
|
||||
// Separate error log file
|
||||
if (loggingConfig.LOG_ERROR_FILE) {
|
||||
transports.push(new DailyRotateFile({
|
||||
level: 'error',
|
||||
filename: `${loggingConfig.LOG_FILE_PATH}/${serviceName}-error-%DATE%.log`,
|
||||
datePattern: loggingConfig.LOG_FILE_DATE_PATTERN,
|
||||
zippedArchive: true,
|
||||
maxSize: loggingConfig.LOG_FILE_MAX_SIZE,
|
||||
maxFiles: loggingConfig.LOG_FILE_MAX_FILES,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Loki transport for centralized logging
|
||||
if (enableLoki && lokiConfig.LOKI_HOST) {
|
||||
try {
|
||||
const lokiTransport = new LokiTransport({
|
||||
host: lokiConfig.LOKI_URL || `http://${lokiConfig.LOKI_HOST}:${lokiConfig.LOKI_PORT}`,
|
||||
labels: {
|
||||
service: serviceName,
|
||||
environment: lokiConfig.LOKI_ENVIRONMENT_LABEL,
|
||||
...(lokiConfig.LOKI_DEFAULT_LABELS ? JSON.parse(lokiConfig.LOKI_DEFAULT_LABELS) : {})
|
||||
},
|
||||
json: true,
|
||||
batching: true,
|
||||
interval: lokiConfig.LOKI_FLUSH_INTERVAL_MS,
|
||||
timeout: lokiConfig.LOKI_PUSH_TIMEOUT,
|
||||
basicAuth: lokiConfig.LOKI_USERNAME && lokiConfig.LOKI_PASSWORD
|
||||
? `${lokiConfig.LOKI_USERNAME}:${lokiConfig.LOKI_PASSWORD}`
|
||||
: undefined,
|
||||
onConnectionError: (err) => {
|
||||
console.error('Loki connection error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
transports.push(lokiTransport);
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize Loki transport:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const loggerConfig: winston.LoggerOptions = {
|
||||
level,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.metadata({
|
||||
fillExcept: ['message', 'level', 'timestamp', 'service']
|
||||
}),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: {
|
||||
service: serviceName,
|
||||
environment: loggingConfig.LOG_ENVIRONMENT,
|
||||
version: loggingConfig.LOG_SERVICE_VERSION
|
||||
},
|
||||
transports
|
||||
};
|
||||
|
||||
return winston.createLogger(loggerConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced Logger class with convenience methods
|
||||
*/
|
||||
export class Logger {
|
||||
private winston: winston.Logger;
|
||||
private serviceName: string;
|
||||
private context: LogContext;
|
||||
|
||||
constructor(serviceName: string, context: LogContext = {}, options?: {
|
||||
level?: LogLevel;
|
||||
enableLoki?: boolean;
|
||||
enableFile?: boolean;
|
||||
enableConsole?: boolean;
|
||||
}) {
|
||||
this.serviceName = serviceName;
|
||||
this.context = context;
|
||||
this.winston = createLogger(serviceName, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug level logging
|
||||
*/
|
||||
debug(message: string, metadata?: LogMetadata): void {
|
||||
this.log('debug', message, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Info level logging
|
||||
*/
|
||||
info(message: string, metadata?: LogMetadata): void {
|
||||
this.log('info', message, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning level logging
|
||||
*/
|
||||
warn(message: string, metadata?: LogMetadata): void {
|
||||
this.log('warn', message, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error level logging
|
||||
*/
|
||||
error(message: string, error?: Error | any, metadata?: LogMetadata): void {
|
||||
const logData: LogMetadata = { ...metadata };
|
||||
|
||||
if (error) {
|
||||
if (error instanceof Error) {
|
||||
logData.error = {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: loggingConfig.LOG_ERROR_STACK ? error.stack : undefined
|
||||
};
|
||||
} else {
|
||||
logData.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
this.log('error', message, logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request logging
|
||||
*/
|
||||
http(message: string, requestData?: {
|
||||
method?: string;
|
||||
url?: string;
|
||||
statusCode?: number;
|
||||
responseTime?: number;
|
||||
userAgent?: string;
|
||||
ip?: string;
|
||||
}): void {
|
||||
if (!loggingConfig.LOG_HTTP_REQUESTS) return;
|
||||
|
||||
this.log('http', message, {
|
||||
request: requestData,
|
||||
type: 'http_request'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performance/timing logging
|
||||
*/
|
||||
performance(message: string, 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, 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, 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 {
|
||||
return new Logger(this.serviceName, { ...this.context, ...context });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add persistent context to this logger
|
||||
*/
|
||||
addContext(context: LogContext): void {
|
||||
this.context = { ...this.context, ...context };
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal logging method
|
||||
*/
|
||||
private log(level: LogLevel, message: string, metadata?: LogMetadata): void {
|
||||
const logData = {
|
||||
...this.context,
|
||||
...metadata,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.winston.log(level, message, logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying winston logger
|
||||
*/
|
||||
getWinstonLogger(): winston.Logger {
|
||||
return this.winston;
|
||||
}
|
||||
/**
|
||||
* Gracefully close all transports
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.winston.end(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default logger instance for a service
|
||||
*/
|
||||
export function getLogger(serviceName: string, context?: LogContext): Logger {
|
||||
return new Logger(serviceName, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown all logger instances gracefully
|
||||
*/
|
||||
export async function shutdownLoggers(): Promise<void> {
|
||||
const closePromises = Array.from(loggerInstances.values()).map(logger =>
|
||||
new Promise<void>((resolve) => {
|
||||
logger.end(() => {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(closePromises);
|
||||
loggerInstances.clear();
|
||||
}
|
||||
|
||||
// Export types for convenience
|
||||
export type { LogLevel, LogContext, LogMetadata } from './types';
|
||||
200
libs/logger/src/middleware.ts
Normal file
200
libs/logger/src/middleware.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* Hono middleware for request logging
|
||||
*/
|
||||
|
||||
import type { Context, Next } from 'hono';
|
||||
import { Logger } from './logger';
|
||||
import { generateCorrelationId, createTimer } from './utils';
|
||||
|
||||
export interface LoggingMiddlewareOptions {
|
||||
logger?: Logger;
|
||||
serviceName?: string;
|
||||
skipPaths?: string[];
|
||||
skipSuccessfulRequests?: boolean;
|
||||
logRequestBody?: boolean;
|
||||
logResponseBody?: boolean;
|
||||
maxBodySize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hono middleware for HTTP request/response logging
|
||||
*/
|
||||
export function loggingMiddleware(options: LoggingMiddlewareOptions = {}) {
|
||||
const {
|
||||
serviceName = 'unknown-service',
|
||||
skipPaths = ['/health', '/metrics', '/favicon.ico'],
|
||||
skipSuccessfulRequests = false,
|
||||
logRequestBody = false,
|
||||
logResponseBody = false,
|
||||
maxBodySize = 1024
|
||||
} = options;
|
||||
|
||||
// Create logger if not provided
|
||||
const logger = options.logger || new Logger(serviceName);
|
||||
|
||||
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();
|
||||
} // 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 metadata
|
||||
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'
|
||||
};
|
||||
|
||||
// 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
|
||||
}
|
||||
} // Log request start
|
||||
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'
|
||||
});
|
||||
|
||||
// Process request
|
||||
await next();
|
||||
|
||||
// Calculate response time
|
||||
const timing = timer.end();
|
||||
|
||||
// Get response information
|
||||
const response = c.res;
|
||||
const status = response.status;
|
||||
|
||||
// Determine log level based on status code
|
||||
const isError = status >= 400;
|
||||
const isSuccess = status >= 200 && status < 300;
|
||||
|
||||
// Skip successful requests if configured
|
||||
if (skipSuccessfulRequests && isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const responseMetadata: any = {
|
||||
correlationId,
|
||||
request: requestMetadata,
|
||||
response: {
|
||||
statusCode: status,
|
||||
statusText: response.statusText,
|
||||
contentLength: response.headers.get('content-length'),
|
||||
contentType: response.headers.get('content-type')
|
||||
},
|
||||
performance: timing
|
||||
};
|
||||
|
||||
// Add response body if enabled
|
||||
if (logResponseBody) {
|
||||
try {
|
||||
const responseBody = await response.clone().text();
|
||||
if (responseBody) {
|
||||
responseMetadata.response.body = responseBody.length > maxBodySize
|
||||
? responseBody.substring(0, maxBodySize) + '...[truncated]'
|
||||
: responseBody;
|
||||
}
|
||||
} catch (error) {
|
||||
// Response body might not be available
|
||||
}
|
||||
} // Log based on status code
|
||||
if (isError) {
|
||||
logger.error('HTTP Request failed', undefined, {
|
||||
correlationId,
|
||||
request: requestMetadata,
|
||||
response: {
|
||||
statusCode: status,
|
||||
statusText: response.statusText,
|
||||
contentLength: response.headers.get('content-length'),
|
||||
contentType: response.headers.get('content-type')
|
||||
},
|
||||
performance: timing
|
||||
});
|
||||
} 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'
|
||||
});
|
||||
}
|
||||
119
libs/logger/src/types.ts
Normal file
119
libs/logger/src/types.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Type definitions for the logger library
|
||||
*/
|
||||
|
||||
// Standard log levels
|
||||
export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly';
|
||||
|
||||
// Context that persists across log calls
|
||||
export interface LogContext {
|
||||
[key: string]: any;
|
||||
requestId?: string;
|
||||
sessionId?: string;
|
||||
userId?: string;
|
||||
traceId?: string;
|
||||
spanId?: string;
|
||||
correlationId?: string;
|
||||
}
|
||||
|
||||
// Metadata for individual log entries
|
||||
export interface LogMetadata {
|
||||
[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;
|
||||
}
|
||||
230
libs/logger/src/utils.ts
Normal file
230
libs/logger/src/utils.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
libs/logger/tsconfig.json
Normal file
16
libs/logger/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
|
||||
"references": [
|
||||
{ "path": "../config" },
|
||||
{ "path": "../types" }
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue