moved folders around
This commit is contained in:
parent
4f89affc2b
commit
36cb84b343
202 changed files with 1160 additions and 660 deletions
14
libs/core/logger/src/index.ts
Normal file
14
libs/core/logger/src/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* @stock-bot/logger - Simplified logging library
|
||||
*
|
||||
* Main exports for the logger library
|
||||
*/
|
||||
|
||||
// Core logger classes and functions
|
||||
export { Logger, getLogger, shutdownLoggers, setLoggerConfig } from './logger';
|
||||
|
||||
// Type definitions
|
||||
export type { LogLevel, LogContext, LogMetadata, LoggerConfig } from './types';
|
||||
|
||||
// Default export
|
||||
export { getLogger as default } from './logger';
|
||||
402
libs/core/logger/src/logger.ts
Normal file
402
libs/core/logger/src/logger.ts
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
/**
|
||||
* Simplified Pino-based logger for Stock Bot platform
|
||||
*
|
||||
* Features:
|
||||
* - High performance JSON logging with Pino
|
||||
* - Console, file, and Loki transports
|
||||
* - Structured logging with metadata
|
||||
* - Service-specific context
|
||||
*/
|
||||
|
||||
import pino from 'pino';
|
||||
import pretty from 'pino-pretty';
|
||||
import type { LogContext, LoggerConfig, LogLevel, LogMetadata } from './types';
|
||||
|
||||
// Simple cache for logger instances
|
||||
const loggerCache = new Map<string, pino.Logger>();
|
||||
|
||||
// Global config that can be set
|
||||
let globalConfig: LoggerConfig = {
|
||||
logLevel: 'info', // Default to info, but trace and fatal are supported
|
||||
logConsole: true,
|
||||
logFile: false,
|
||||
logFilePath: './logs',
|
||||
logLoki: false,
|
||||
environment: 'development',
|
||||
hideObject: false,
|
||||
};
|
||||
|
||||
// Log level priorities for comparison
|
||||
const LOG_LEVELS: Record<LogLevel, number> = {
|
||||
trace: 10,
|
||||
debug: 20,
|
||||
info: 30,
|
||||
warn: 40,
|
||||
error: 50,
|
||||
fatal: 60,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set global logger configuration
|
||||
*/
|
||||
export function setLoggerConfig(config: LoggerConfig): void {
|
||||
globalConfig = { ...globalConfig, ...config };
|
||||
// Clear cache to force recreation with new config
|
||||
loggerCache.clear();
|
||||
}
|
||||
/**
|
||||
* Create logger destination using multistream approach:
|
||||
* - Console: In-process pretty stream (fast shutdown, disabled in production)
|
||||
* - File/Loki: Worker transports (default timeout, ok to wait)
|
||||
*/
|
||||
function createDestination(
|
||||
serviceName: string,
|
||||
config: LoggerConfig = globalConfig
|
||||
): pino.DestinationStream | null {
|
||||
const streams: pino.StreamEntry[] = [];
|
||||
|
||||
// Console: In-process pretty stream for dev (fast shutdown)
|
||||
if (config.logConsole && config.environment !== 'production') {
|
||||
const prettyStream = pretty({
|
||||
sync: true, // IMPORTANT: Make async to prevent blocking the event loop
|
||||
colorize: true,
|
||||
translateTime: 'yyyy-mm-dd HH:MM:ss.l',
|
||||
messageFormat: '[{service}{childName}] {msg}',
|
||||
singleLine: false, // This was causing logs to be on one line
|
||||
hideObject: false, // Hide metadata objects
|
||||
ignore: 'pid,hostname,service,environment,version,childName',
|
||||
errorLikeObjectKeys: ['err', 'error'],
|
||||
errorProps: 'message,stack,name,code',
|
||||
});
|
||||
streams.push({ stream: prettyStream });
|
||||
}
|
||||
|
||||
// File: Worker transport (has timeout but acceptable)
|
||||
if (config.logFile) {
|
||||
streams.push(
|
||||
pino.transport({
|
||||
target: 'pino/file',
|
||||
level: config.logLevel || 'info',
|
||||
options: {
|
||||
destination: `${config.logFilePath}/${serviceName}.log`,
|
||||
mkdir: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Loki: Worker transport (has timeout but acceptable)
|
||||
if (config.logLoki && config.lokiHost) {
|
||||
streams.push(
|
||||
pino.transport({
|
||||
target: 'pino-loki',
|
||||
level: config.logLevel || 'info',
|
||||
options: {
|
||||
host: config.lokiHost,
|
||||
labels: {
|
||||
service: serviceName,
|
||||
environment: config.environment || 'development',
|
||||
},
|
||||
ignore: 'childName',
|
||||
...(config.lokiUser && config.lokiPassword
|
||||
? {
|
||||
basicAuth: {
|
||||
username: config.lokiUser,
|
||||
password: config.lokiPassword,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return streams.length > 0 ? pino.multistream(streams) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create pino logger
|
||||
*/
|
||||
function getPinoLogger(serviceName: string, config: LoggerConfig = globalConfig): pino.Logger {
|
||||
const cacheKey = `${serviceName}-${JSON.stringify(config)}`;
|
||||
if (!loggerCache.has(cacheKey)) {
|
||||
const destination = createDestination(serviceName, config);
|
||||
|
||||
const loggerOptions: pino.LoggerOptions = {
|
||||
level: config.logLevel || 'info',
|
||||
base: {
|
||||
service: serviceName,
|
||||
environment: config.environment || 'development',
|
||||
version: '1.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
const logger = destination ? pino(loggerOptions, destination) : pino(loggerOptions);
|
||||
|
||||
loggerCache.set(cacheKey, logger);
|
||||
}
|
||||
|
||||
const logger = loggerCache.get(cacheKey);
|
||||
if (!logger) {
|
||||
throw new Error(`Expected logger ${cacheKey} to exist in cache`);
|
||||
}
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified Logger class
|
||||
*/
|
||||
export class Logger {
|
||||
private pino: pino.Logger;
|
||||
private context: LogContext;
|
||||
private serviceName: string;
|
||||
private childName?: string;
|
||||
|
||||
constructor(serviceName: string, context: LogContext = {}, config?: LoggerConfig) {
|
||||
this.pino = getPinoLogger(serviceName, config);
|
||||
this.context = context;
|
||||
this.serviceName = serviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a log level should be output based on global config
|
||||
*/
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
const currentLevel = globalConfig.logLevel || 'info';
|
||||
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
||||
}
|
||||
|
||||
/**
|
||||
* Core log method
|
||||
*/
|
||||
private log(level: LogLevel, message: string | object, metadata?: LogMetadata): void {
|
||||
// Skip if level is below current threshold
|
||||
if (!this.shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = { ...this.context, ...metadata };
|
||||
|
||||
// Hide all metadata if hideObject is enabled
|
||||
if (globalConfig.hideObject) {
|
||||
data = {}; // Clear all metadata
|
||||
}
|
||||
|
||||
if (typeof message === 'string') {
|
||||
(this.pino as any)[level](data, message);
|
||||
} else {
|
||||
if (globalConfig.hideObject) {
|
||||
(this.pino as any)[level]({}, `Object logged (hidden)`);
|
||||
} else {
|
||||
(this.pino as any)[level]({ ...data, data: message }, 'Object logged');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Simple log level methods
|
||||
trace(message: string | object, metadata?: LogMetadata): void {
|
||||
this.log('trace', message, metadata);
|
||||
}
|
||||
|
||||
debug(message: string | object, metadata?: LogMetadata): void {
|
||||
this.log('debug', message, metadata);
|
||||
}
|
||||
|
||||
info(message: string | object, metadata?: LogMetadata): void {
|
||||
this.log('info', message, metadata);
|
||||
}
|
||||
|
||||
warn(message: string | object, metadata?: LogMetadata): void {
|
||||
this.log('warn', message, metadata);
|
||||
}
|
||||
|
||||
error(message: string | object, metadata?: (LogMetadata & { error?: any }) | unknown): void {
|
||||
let data: any = {};
|
||||
|
||||
// Handle metadata parameter normalization
|
||||
if (metadata instanceof Error) {
|
||||
// Direct Error object as metadata
|
||||
data = { error: metadata };
|
||||
} else if (metadata !== null && typeof metadata === 'object') {
|
||||
// Object metadata (including arrays, but not null)
|
||||
data = { ...metadata };
|
||||
} else if (metadata !== undefined) {
|
||||
// Primitive values (string, number, boolean, etc.)
|
||||
data = { metadata };
|
||||
}
|
||||
|
||||
// Handle multiple error properties in metadata
|
||||
const errorKeys = ['error', 'err', 'primaryError', 'secondaryError'];
|
||||
errorKeys.forEach(key => {
|
||||
if (data[key]) {
|
||||
const normalizedKey = key === 'error' ? 'err' : `${key}_normalized`;
|
||||
data[normalizedKey] = this.normalizeError(data[key]);
|
||||
|
||||
// Only delete the original 'error' key to maintain other error properties
|
||||
if (key === 'error') {
|
||||
delete data.error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.log('error', message, data);
|
||||
}
|
||||
|
||||
fatal(message: string | object, metadata?: (LogMetadata & { error?: any }) | unknown): void {
|
||||
let data: any = {};
|
||||
|
||||
// Handle metadata parameter normalization (same as error)
|
||||
if (metadata instanceof Error) {
|
||||
data = { error: metadata };
|
||||
} else if (metadata !== null && typeof metadata === 'object') {
|
||||
data = { ...metadata };
|
||||
} else if (metadata !== undefined) {
|
||||
data = { metadata };
|
||||
}
|
||||
|
||||
// Normalize error objects in the data
|
||||
const errorKeys = ['error', 'err', 'primaryError', 'secondaryError'];
|
||||
errorKeys.forEach(key => {
|
||||
if (data[key]) {
|
||||
const normalizedKey = key === 'error' ? 'err' : `${key}_normalized`;
|
||||
data[normalizedKey] = this.normalizeError(data[key]);
|
||||
|
||||
if (key === 'error') {
|
||||
delete data.error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.log('fatal', message, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize any error type to a structured format
|
||||
*/
|
||||
private normalizeError(error: any): any {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
if (error && typeof error === 'object') {
|
||||
// Handle error-like objects
|
||||
return {
|
||||
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),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Create child logger with additional context
|
||||
*/
|
||||
child(serviceName: string, context?: LogContext): Logger {
|
||||
// Create child logger that shares the same pino instance with additional context
|
||||
const childLogger = Object.create(Logger.prototype);
|
||||
childLogger.serviceName = this.serviceName;
|
||||
childLogger.childName = serviceName;
|
||||
childLogger.context = { ...this.context, ...context };
|
||||
const childBindings = {
|
||||
service: this.serviceName,
|
||||
childName: ' -> ' + serviceName,
|
||||
...(context || childLogger.context),
|
||||
};
|
||||
|
||||
childLogger.pino = this.pino.child(childBindings);
|
||||
return childLogger;
|
||||
// }
|
||||
// childLogger.pino = this.pino.child(context || childLogger.context); // Let pino handle level inheritance naturally
|
||||
// return childLogger;
|
||||
}
|
||||
|
||||
// Getters for service and context
|
||||
getServiceName(): string {
|
||||
return this.serviceName;
|
||||
}
|
||||
getChildName(): string | undefined {
|
||||
return this.childName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main factory function
|
||||
*/
|
||||
export function getLogger(
|
||||
serviceName: string,
|
||||
context?: LogContext,
|
||||
config?: LoggerConfig
|
||||
): Logger {
|
||||
return new Logger(serviceName, context, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully shutdown all logger instances
|
||||
* This ensures all transports are flushed and closed properly
|
||||
*/
|
||||
export async function shutdownLoggers(): Promise<void> {
|
||||
try {
|
||||
// Log final message before shutdown
|
||||
for (const logger of loggerCache.values()) {
|
||||
logger.info('Logger shutting down...');
|
||||
}
|
||||
|
||||
const flushPromises = Array.from(loggerCache.values()).map(logger => logger.flush());
|
||||
|
||||
await Promise.all(flushPromises);
|
||||
|
||||
// Give transports time to finish writing
|
||||
// This is especially important for file and network transports
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Logger shutdown failed:', error);
|
||||
} finally {
|
||||
loggerCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown - flush all logger transports quickly
|
||||
* Use this in your application shutdown handlers
|
||||
*/
|
||||
export async function gracefulShutdown(): Promise<void> {
|
||||
const flushPromises: Promise<void>[] = [];
|
||||
|
||||
for (const logger of loggerCache.values()) {
|
||||
// Use pino v9's flush() method - this is much faster than the complex shutdown
|
||||
flushPromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
logger.flush((err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(flushPromises);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Logger graceful shutdown failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Export types for convenience
|
||||
export type { LogContext, LogLevel, LogMetadata } from './types';
|
||||
30
libs/core/logger/src/types.ts
Normal file
30
libs/core/logger/src/types.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Simplified type definitions for the logger library
|
||||
*/
|
||||
|
||||
// Standard log levels (simplified to pino defaults)
|
||||
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
|
||||
// Context that persists across log calls
|
||||
export interface LogContext {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Metadata for individual log entries
|
||||
export interface LogMetadata {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Logger configuration
|
||||
export interface LoggerConfig {
|
||||
logLevel?: LogLevel;
|
||||
logConsole?: boolean;
|
||||
logFile?: boolean;
|
||||
logFilePath?: string;
|
||||
logLoki?: boolean;
|
||||
lokiHost?: string;
|
||||
lokiUser?: string;
|
||||
lokiPassword?: string;
|
||||
environment?: string;
|
||||
hideObject?: boolean;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue