import pino, { type TransportTargetOptions } from "pino"; import { loadConfig } from "./config.js"; import type { AppConfig } from "./types.js"; interface LogTransportProvider { getTarget(): TransportTargetOptions | null; getStartupMessage(): string | null; } class ConsoleLogTransport implements LogTransportProvider { public constructor(private readonly config: AppConfig) {} public getTarget(): TransportTargetOptions { return { target: "pino-pretty", level: this.config.logLevel, options: { colorize: true, translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l", ignore: "pid,hostname", messageFormat: "{msg}", levelFirst: true, }, }; } public getStartupMessage(): string { return "Logging to console"; } } class LokiLogTransport implements LogTransportProvider { private readonly host: string | null; public constructor( private readonly config: AppConfig, lokiUri: string | null ) { this.host = lokiUri; } public getTarget(): TransportTargetOptions | null { if (this.host === null) { return null; } return { target: "pino-loki", level: this.config.logLevel, options: { host: this.host, json: false, batching: true, interval: 5, replaceTimestamp: true, labels: this.buildLabels(), messageFormat: "{msg}", ignore: "pid,hostname", }, }; } private buildLabels(): Record { const labels: Record = { job: this.config.lokiJob, }; if (this.config.lokiEnvironment) { labels.environment = this.config.lokiEnvironment; } if (this.config.lokiInstance) { labels.instance = this.config.lokiInstance; } return labels; } public getStartupMessage(): string | null { return this.host !== null ? `Logging to Loki: ${this.host}` : null; } } function buildLogger(config: AppConfig) { const consoleTransport = new ConsoleLogTransport(config); const lokiTransport = new LokiLogTransport(config, config.lokiUri); const transports: LogTransportProvider[] = [consoleTransport, lokiTransport]; const instance = pino({ level: config.logLevel, timestamp: pino.stdTimeFunctions.isoTime, transport: { targets: transports .map((t) => t.getTarget()) .filter((target): target is TransportTargetOptions => target !== null), }, }); return { instance, transports }; } let _instance: pino.Logger | null = null; let _transports: LogTransportProvider[] = []; export function initLogger(config: AppConfig): pino.Logger { const result = buildLogger(config); _instance = result.instance; _transports = result.transports; return _instance; } function getInstance(): pino.Logger { if (_instance === null) { return initLogger(loadConfig()); } return _instance; } // Export as a getter so consumers see the lazily-initialized instance. export const logger: pino.Logger = new Proxy({} as pino.Logger, { get(_, prop) { const inst = getInstance(); const value = (inst as unknown as Record)[prop]; return typeof value === "function" ? value.bind(inst) : value; }, }); export function logActiveTransports(log: pino.Logger): void { for (const t of _transports) { const msg = t.getStartupMessage(); if (msg !== null) { log.info(msg); } } } export function createLogger(name: string) { return logger.child({ name }); }