done
This commit is contained in:
parent
caf1c5fcaf
commit
20b7180a43
5 changed files with 207 additions and 158 deletions
|
|
@ -1,15 +1,15 @@
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import {
|
|
||||||
ConfigManagerOptions,
|
|
||||||
Environment,
|
|
||||||
ConfigLoader,
|
|
||||||
DeepPartial,
|
|
||||||
ConfigSchema
|
|
||||||
} from './types';
|
|
||||||
import { ConfigError, ConfigValidationError } from './errors';
|
|
||||||
import { EnvLoader } from './loaders/env.loader';
|
import { EnvLoader } from './loaders/env.loader';
|
||||||
import { FileLoader } from './loaders/file.loader';
|
import { FileLoader } from './loaders/file.loader';
|
||||||
|
import { ConfigError, ConfigValidationError } from './errors';
|
||||||
|
import {
|
||||||
|
ConfigLoader,
|
||||||
|
ConfigManagerOptions,
|
||||||
|
ConfigSchema,
|
||||||
|
DeepPartial,
|
||||||
|
Environment,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
export class ConfigManager<T = Record<string, unknown>> {
|
export class ConfigManager<T = Record<string, unknown>> {
|
||||||
private config: T | null = null;
|
private config: T | null = null;
|
||||||
|
|
@ -59,7 +59,11 @@ export class ConfigManager<T = Record<string, unknown>> {
|
||||||
const mergedConfig = this.deepMerge(...configs) as T;
|
const mergedConfig = this.deepMerge(...configs) as T;
|
||||||
|
|
||||||
// Add environment if not present
|
// Add environment if not present
|
||||||
if (typeof mergedConfig === 'object' && mergedConfig !== null && !('environment' in mergedConfig)) {
|
if (
|
||||||
|
typeof mergedConfig === 'object' &&
|
||||||
|
mergedConfig !== null &&
|
||||||
|
!('environment' in mergedConfig)
|
||||||
|
) {
|
||||||
(mergedConfig as Record<string, unknown>)['environment'] = this.environment;
|
(mergedConfig as Record<string, unknown>)['environment'] = this.environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,10 +73,7 @@ export class ConfigManager<T = Record<string, unknown>> {
|
||||||
this.config = this.schema.parse(mergedConfig) as T;
|
this.config = this.schema.parse(mergedConfig) as T;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
throw new ConfigValidationError(
|
throw new ConfigValidationError('Configuration validation failed', error.errors);
|
||||||
'Configuration validation failed',
|
|
||||||
error.errors
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +133,10 @@ export class ConfigManager<T = Record<string, unknown>> {
|
||||||
throw new ConfigError('Configuration not initialized. Call initialize() first.');
|
throw new ConfigError('Configuration not initialized. Call initialize() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = this.deepMerge(this.config as Record<string, unknown>, updates as Record<string, unknown>) as T;
|
const updated = this.deepMerge(
|
||||||
|
this.config as Record<string, unknown>,
|
||||||
|
updates as Record<string, unknown>
|
||||||
|
) as T;
|
||||||
|
|
||||||
// Re-validate if schema is present
|
// Re-validate if schema is present
|
||||||
if (this.schema) {
|
if (this.schema) {
|
||||||
|
|
@ -210,7 +214,7 @@ export class ConfigManager<T = Record<string, unknown>> {
|
||||||
!(value instanceof RegExp)
|
!(value instanceof RegExp)
|
||||||
) {
|
) {
|
||||||
result[key] = this.deepMerge(
|
result[key] = this.deepMerge(
|
||||||
(result[key] as Record<string, unknown>) || {} as Record<string, unknown>,
|
(result[key] as Record<string, unknown>) || ({} as Record<string, unknown>),
|
||||||
value as Record<string, unknown>
|
value as Record<string, unknown>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ConfigLoader } from '../types';
|
|
||||||
import { ConfigLoaderError } from '../errors';
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
import { ConfigLoaderError } from '../errors';
|
||||||
|
import { ConfigLoader } from '../types';
|
||||||
|
|
||||||
export interface EnvLoaderOptions {
|
export interface EnvLoaderOptions {
|
||||||
convertCase?: boolean;
|
convertCase?: boolean;
|
||||||
|
|
@ -21,7 +21,7 @@ export class EnvLoader implements ConfigLoader {
|
||||||
parseJson: true,
|
parseJson: true,
|
||||||
parseValues: true,
|
parseValues: true,
|
||||||
nestedDelimiter: '_',
|
nestedDelimiter: '_',
|
||||||
...options
|
...options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,9 +41,7 @@ export class EnvLoader implements ConfigLoader {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configKey = this.prefix
|
const configKey = this.prefix ? key.slice(this.prefix.length) : key;
|
||||||
? key.slice(this.prefix.length)
|
|
||||||
: key;
|
|
||||||
|
|
||||||
if (!this.options.convertCase && !this.options.nestedDelimiter) {
|
if (!this.options.convertCase && !this.options.nestedDelimiter) {
|
||||||
// Simple case - just keep the key as is
|
// Simple case - just keep the key as is
|
||||||
|
|
@ -56,10 +54,7 @@ export class EnvLoader implements ConfigLoader {
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ConfigLoaderError(
|
throw new ConfigLoaderError(`Failed to load environment variables: ${error}`, 'EnvLoader');
|
||||||
`Failed to load environment variables: ${error}`,
|
|
||||||
'EnvLoader'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,7 +75,11 @@ export class EnvLoader implements ConfigLoader {
|
||||||
// Convert to camelCase
|
// Convert to camelCase
|
||||||
const camelKey = this.toCamelCase(key);
|
const camelKey = this.toCamelCase(key);
|
||||||
config[camelKey] = parsedValue;
|
config[camelKey] = parsedValue;
|
||||||
} else if (this.options.nestedDelimiter && this.options.nestedDelimiter !== '_' && key.includes(this.options.nestedDelimiter)) {
|
} else if (
|
||||||
|
this.options.nestedDelimiter &&
|
||||||
|
this.options.nestedDelimiter !== '_' &&
|
||||||
|
key.includes(this.options.nestedDelimiter)
|
||||||
|
) {
|
||||||
// Handle nested delimiter (e.g., APP__NAME -> { APP: { NAME: value } })
|
// Handle nested delimiter (e.g., APP__NAME -> { APP: { NAME: value } })
|
||||||
const parts = key.split(this.options.nestedDelimiter);
|
const parts = key.split(this.options.nestedDelimiter);
|
||||||
this.setNestedValue(config, parts, parsedValue);
|
this.setNestedValue(config, parts, parsedValue);
|
||||||
|
|
@ -126,63 +125,61 @@ export class EnvLoader implements ConfigLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
private toCamelCase(str: string): string {
|
private toCamelCase(str: string): string {
|
||||||
return str
|
return str.toLowerCase().replace(/_([a-z])/g, (_, char) => char.toUpperCase());
|
||||||
.toLowerCase()
|
|
||||||
.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProviderMapping(envKey: string): { path: string[] } | null {
|
private getProviderMapping(envKey: string): { path: string[] } | null {
|
||||||
// Provider-specific and special environment variable mappings
|
// Provider-specific and special environment variable mappings
|
||||||
const providerMappings: Record<string, string[]> = {
|
const providerMappings: Record<string, string[]> = {
|
||||||
// WebShare provider mappings
|
// WebShare provider mappings
|
||||||
'WEBSHARE_API_KEY': ['webshare', 'apiKey'],
|
WEBSHARE_API_KEY: ['webshare', 'apiKey'],
|
||||||
'WEBSHARE_API_URL': ['webshare', 'apiUrl'],
|
WEBSHARE_API_URL: ['webshare', 'apiUrl'],
|
||||||
'WEBSHARE_ENABLED': ['webshare', 'enabled'],
|
WEBSHARE_ENABLED: ['webshare', 'enabled'],
|
||||||
|
|
||||||
// EOD provider mappings
|
// EOD provider mappings
|
||||||
'EOD_API_KEY': ['providers', 'eod', 'apiKey'],
|
EOD_API_KEY: ['providers', 'eod', 'apiKey'],
|
||||||
'EOD_BASE_URL': ['providers', 'eod', 'baseUrl'],
|
EOD_BASE_URL: ['providers', 'eod', 'baseUrl'],
|
||||||
'EOD_TIER': ['providers', 'eod', 'tier'],
|
EOD_TIER: ['providers', 'eod', 'tier'],
|
||||||
'EOD_ENABLED': ['providers', 'eod', 'enabled'],
|
EOD_ENABLED: ['providers', 'eod', 'enabled'],
|
||||||
'EOD_PRIORITY': ['providers', 'eod', 'priority'],
|
EOD_PRIORITY: ['providers', 'eod', 'priority'],
|
||||||
|
|
||||||
// Interactive Brokers provider mappings
|
// Interactive Brokers provider mappings
|
||||||
'IB_GATEWAY_HOST': ['providers', 'ib', 'gateway', 'host'],
|
IB_GATEWAY_HOST: ['providers', 'ib', 'gateway', 'host'],
|
||||||
'IB_GATEWAY_PORT': ['providers', 'ib', 'gateway', 'port'],
|
IB_GATEWAY_PORT: ['providers', 'ib', 'gateway', 'port'],
|
||||||
'IB_CLIENT_ID': ['providers', 'ib', 'gateway', 'clientId'],
|
IB_CLIENT_ID: ['providers', 'ib', 'gateway', 'clientId'],
|
||||||
'IB_ACCOUNT': ['providers', 'ib', 'account'],
|
IB_ACCOUNT: ['providers', 'ib', 'account'],
|
||||||
'IB_MARKET_DATA_TYPE': ['providers', 'ib', 'marketDataType'],
|
IB_MARKET_DATA_TYPE: ['providers', 'ib', 'marketDataType'],
|
||||||
'IB_ENABLED': ['providers', 'ib', 'enabled'],
|
IB_ENABLED: ['providers', 'ib', 'enabled'],
|
||||||
'IB_PRIORITY': ['providers', 'ib', 'priority'],
|
IB_PRIORITY: ['providers', 'ib', 'priority'],
|
||||||
|
|
||||||
// QuoteMedia provider mappings
|
// QuoteMedia provider mappings
|
||||||
'QM_USERNAME': ['providers', 'qm', 'username'],
|
QM_USERNAME: ['providers', 'qm', 'username'],
|
||||||
'QM_PASSWORD': ['providers', 'qm', 'password'],
|
QM_PASSWORD: ['providers', 'qm', 'password'],
|
||||||
'QM_BASE_URL': ['providers', 'qm', 'baseUrl'],
|
QM_BASE_URL: ['providers', 'qm', 'baseUrl'],
|
||||||
'QM_WEBMASTER_ID': ['providers', 'qm', 'webmasterId'],
|
QM_WEBMASTER_ID: ['providers', 'qm', 'webmasterId'],
|
||||||
'QM_ENABLED': ['providers', 'qm', 'enabled'],
|
QM_ENABLED: ['providers', 'qm', 'enabled'],
|
||||||
'QM_PRIORITY': ['providers', 'qm', 'priority'],
|
QM_PRIORITY: ['providers', 'qm', 'priority'],
|
||||||
|
|
||||||
// Yahoo Finance provider mappings
|
// Yahoo Finance provider mappings
|
||||||
'YAHOO_BASE_URL': ['providers', 'yahoo', 'baseUrl'],
|
YAHOO_BASE_URL: ['providers', 'yahoo', 'baseUrl'],
|
||||||
'YAHOO_COOKIE_JAR': ['providers', 'yahoo', 'cookieJar'],
|
YAHOO_COOKIE_JAR: ['providers', 'yahoo', 'cookieJar'],
|
||||||
'YAHOO_CRUMB': ['providers', 'yahoo', 'crumb'],
|
YAHOO_CRUMB: ['providers', 'yahoo', 'crumb'],
|
||||||
'YAHOO_ENABLED': ['providers', 'yahoo', 'enabled'],
|
YAHOO_ENABLED: ['providers', 'yahoo', 'enabled'],
|
||||||
'YAHOO_PRIORITY': ['providers', 'yahoo', 'priority'],
|
YAHOO_PRIORITY: ['providers', 'yahoo', 'priority'],
|
||||||
|
|
||||||
// General application config mappings
|
// General application config mappings
|
||||||
'NAME': ['name'],
|
NAME: ['name'],
|
||||||
'VERSION': ['version'],
|
VERSION: ['version'],
|
||||||
|
|
||||||
// Log mappings (using LOG_ prefix for all)
|
// Log mappings (using LOG_ prefix for all)
|
||||||
'LOG_LEVEL': ['log', 'level'],
|
LOG_LEVEL: ['log', 'level'],
|
||||||
'LOG_FORMAT': ['log', 'format'],
|
LOG_FORMAT: ['log', 'format'],
|
||||||
'LOG_LOKI_ENABLED': ['log', 'loki', 'enabled'],
|
LOG_LOKI_ENABLED: ['log', 'loki', 'enabled'],
|
||||||
'LOG_LOKI_HOST': ['log', 'loki', 'host'],
|
LOG_LOKI_HOST: ['log', 'loki', 'host'],
|
||||||
'LOG_LOKI_PORT': ['log', 'loki', 'port'],
|
LOG_LOKI_PORT: ['log', 'loki', 'port'],
|
||||||
|
|
||||||
// Special mappings to avoid conflicts
|
// Special mappings to avoid conflicts
|
||||||
'DEBUG_MODE': ['debug'],
|
DEBUG_MODE: ['debug'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapping = providerMappings[envKey];
|
const mapping = providerMappings[envKey];
|
||||||
|
|
@ -208,16 +205,26 @@ export class EnvLoader implements ConfigLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle booleans
|
// Handle booleans
|
||||||
if (value.toLowerCase() === 'true') {return true;}
|
if (value.toLowerCase() === 'true') {
|
||||||
if (value.toLowerCase() === 'false') {return false;}
|
return true;
|
||||||
|
}
|
||||||
|
if (value.toLowerCase() === 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle numbers
|
// Handle numbers
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
if (!isNaN(num) && value !== '') {return num;}
|
if (!isNaN(num) && value !== '') {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle null/undefined
|
// Handle null/undefined
|
||||||
if (value.toLowerCase() === 'null') {return null;}
|
if (value.toLowerCase() === 'null') {
|
||||||
if (value.toLowerCase() === 'undefined') {return undefined;}
|
return null;
|
||||||
|
}
|
||||||
|
if (value.toLowerCase() === 'undefined') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Return as string
|
// Return as string
|
||||||
return value;
|
return value;
|
||||||
|
|
@ -243,8 +250,10 @@ export class EnvLoader implements ConfigLoader {
|
||||||
let value = trimmed.substring(equalIndex + 1).trim();
|
let value = trimmed.substring(equalIndex + 1).trim();
|
||||||
|
|
||||||
// Remove surrounding quotes if present
|
// Remove surrounding quotes if present
|
||||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
if (
|
||||||
(value.startsWith("'") && value.endsWith("'"))) {
|
(value.startsWith('"') && value.endsWith('"')) ||
|
||||||
|
(value.startsWith("'") && value.endsWith("'"))
|
||||||
|
) {
|
||||||
value = value.slice(1, -1);
|
value = value.slice(1, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +266,10 @@ export class EnvLoader implements ConfigLoader {
|
||||||
// File not found is not an error (env files are optional)
|
// File not found is not an error (env files are optional)
|
||||||
if (error && typeof error === 'object' && 'code' in error && error.code !== 'ENOENT') {
|
if (error && typeof error === 'object' && 'code' in error && error.code !== 'ENOENT') {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(`Warning: Could not load env file ${filePath}:`, error instanceof Error ? error.message : String(error));
|
console.warn(
|
||||||
|
`Warning: Could not load env file ${filePath}:`,
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { readFileSync, existsSync } from 'fs';
|
import { existsSync, readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { ConfigLoader } from '../types';
|
|
||||||
import { ConfigLoaderError } from '../errors';
|
import { ConfigLoaderError } from '../errors';
|
||||||
|
import { ConfigLoader } from '../types';
|
||||||
|
|
||||||
export class FileLoader implements ConfigLoader {
|
export class FileLoader implements ConfigLoader {
|
||||||
readonly priority = 50; // Medium priority
|
readonly priority = 50; // Medium priority
|
||||||
|
|
@ -30,10 +30,7 @@ export class FileLoader implements ConfigLoader {
|
||||||
// Merge configs (later configs override earlier ones)
|
// Merge configs (later configs override earlier ones)
|
||||||
return this.deepMerge(...configs);
|
return this.deepMerge(...configs);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ConfigLoaderError(
|
throw new ConfigLoaderError(`Failed to load configuration files: ${error}`, 'FileLoader');
|
||||||
`Failed to load configuration files: ${error}`,
|
|
||||||
'FileLoader'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +53,10 @@ export class FileLoader implements ConfigLoader {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
} else if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
|
} else if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
|
||||||
result[key] = this.deepMerge(result[key] as Record<string, unknown> || {}, value as Record<string, unknown>);
|
result[key] = this.deepMerge(
|
||||||
|
(result[key] as Record<string, unknown>) || {},
|
||||||
|
value as Record<string, unknown>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
|
import pretty from 'pino-pretty';
|
||||||
import type { LogContext, LoggerConfig, LogLevel, LogMetadata } from './types';
|
import type { LogContext, LoggerConfig, LogLevel, LogMetadata } from './types';
|
||||||
|
|
||||||
// Simple cache for logger instances
|
// Simple cache for logger instances
|
||||||
|
|
@ -35,17 +36,20 @@ export function setLoggerConfig(config: LoggerConfig): void {
|
||||||
console.log('Logger config updated:', globalConfig.logLevel);
|
console.log('Logger config updated:', globalConfig.logLevel);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Create transport configuration
|
* 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 createTransports(serviceName: string, config: LoggerConfig = globalConfig): any {
|
function createDestination(
|
||||||
const targets: any[] = [];
|
serviceName: string,
|
||||||
|
config: LoggerConfig = globalConfig
|
||||||
|
): pino.DestinationStream | null {
|
||||||
|
const streams: pino.StreamEntry[] = [];
|
||||||
|
|
||||||
// Console transport
|
// Console: In-process pretty stream for dev (fast shutdown)
|
||||||
if (config.logConsole) {
|
if (config.logConsole && config.environment !== 'production') {
|
||||||
targets.push({
|
const prettyStream = pretty({
|
||||||
target: 'pino-pretty',
|
sync: true,
|
||||||
level: config.logLevel || 'info',
|
|
||||||
options: {
|
|
||||||
colorize: true,
|
colorize: true,
|
||||||
translateTime: 'yyyy-mm-dd HH:MM:ss.l',
|
translateTime: 'yyyy-mm-dd HH:MM:ss.l',
|
||||||
messageFormat: '[{service}{childName}] {msg}',
|
messageFormat: '[{service}{childName}] {msg}',
|
||||||
|
|
@ -54,27 +58,28 @@ function createTransports(serviceName: string, config: LoggerConfig = globalConf
|
||||||
ignore: 'pid,hostname,service,environment,version,childName',
|
ignore: 'pid,hostname,service,environment,version,childName',
|
||||||
errorLikeObjectKeys: ['err', 'error'],
|
errorLikeObjectKeys: ['err', 'error'],
|
||||||
errorProps: 'message,stack,name,code',
|
errorProps: 'message,stack,name,code',
|
||||||
// Ensure the transport respects the configured level
|
|
||||||
minimumLevel: config.logLevel || 'info',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
streams.push({ stream: prettyStream });
|
||||||
}
|
}
|
||||||
|
|
||||||
// File transport
|
// File: Worker transport (has timeout but acceptable)
|
||||||
if (config.logFile) {
|
if (config.logFile) {
|
||||||
targets.push({
|
streams.push(
|
||||||
|
pino.transport({
|
||||||
target: 'pino/file',
|
target: 'pino/file',
|
||||||
level: config.logLevel || 'info',
|
level: config.logLevel || 'info',
|
||||||
options: {
|
options: {
|
||||||
destination: `${config.logFilePath}/${serviceName}.log`,
|
destination: `${config.logFilePath}/${serviceName}.log`,
|
||||||
mkdir: true,
|
mkdir: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loki transport
|
// Loki: Worker transport (has timeout but acceptable)
|
||||||
if (config.logLoki && config.lokiHost) {
|
if (config.logLoki && config.lokiHost) {
|
||||||
targets.push({
|
streams.push(
|
||||||
|
pino.transport({
|
||||||
target: 'pino-loki',
|
target: 'pino-loki',
|
||||||
level: config.logLevel || 'info',
|
level: config.logLevel || 'info',
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -93,10 +98,11 @@ function createTransports(serviceName: string, config: LoggerConfig = globalConf
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return targets.length > 0 ? { targets } : null;
|
return streams.length > 0 ? pino.multistream(streams) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -105,7 +111,7 @@ function createTransports(serviceName: string, config: LoggerConfig = globalConf
|
||||||
function getPinoLogger(serviceName: string, config: LoggerConfig = globalConfig): pino.Logger {
|
function getPinoLogger(serviceName: string, config: LoggerConfig = globalConfig): pino.Logger {
|
||||||
const cacheKey = `${serviceName}-${JSON.stringify(config)}`;
|
const cacheKey = `${serviceName}-${JSON.stringify(config)}`;
|
||||||
if (!loggerCache.has(cacheKey)) {
|
if (!loggerCache.has(cacheKey)) {
|
||||||
const transport = createTransports(serviceName, config);
|
const destination = createDestination(serviceName, config);
|
||||||
|
|
||||||
const loggerOptions: pino.LoggerOptions = {
|
const loggerOptions: pino.LoggerOptions = {
|
||||||
level: config.logLevel || 'info',
|
level: config.logLevel || 'info',
|
||||||
|
|
@ -116,11 +122,8 @@ function getPinoLogger(serviceName: string, config: LoggerConfig = globalConfig)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (transport) {
|
const logger = destination ? pino(loggerOptions, destination) : pino(loggerOptions);
|
||||||
loggerOptions.transport = transport;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = pino(loggerOptions);
|
|
||||||
loggerCache.set(cacheKey, logger);
|
loggerCache.set(cacheKey, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -356,5 +359,35 @@ export async function shutdownLoggers(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 types for convenience
|
||||||
export type { LogContext, LogLevel, LogMetadata } from './types';
|
export type { LogContext, LogLevel, LogMetadata } from './types';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue