moved folders around

This commit is contained in:
Boki 2025-06-21 18:27:00 -04:00
parent 4f89affc2b
commit 36cb84b343
202 changed files with 1160 additions and 660 deletions

View 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';

View 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';

View 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;
}