Spaces:
Runtime error
Runtime error
| /** | |
| * Structured Logger — Pino-based logger for OmniRoute | |
| * | |
| * Usage: | |
| * import { logger } from "@/shared/utils/logger"; | |
| * const log = logger.child({ module: "proxy" }); | |
| * log.info({ model: "gpt-4o" }, "Request received"); | |
| * log.error({ err }, "Connection failed"); | |
| * | |
| * In development, output is pretty-printed via pino-pretty. | |
| * In production, output is structured JSON for log aggregation. | |
| * | |
| * When APP_LOG_TO_FILE is enabled (default: true), logs are also written | |
| * as JSON lines to the file specified by APP_LOG_FILE_PATH. | |
| */ | |
| import pino from "pino"; | |
| import { resolve } from "path"; | |
| import { getLogConfig, initLogRotation } from "@/lib/logRotation"; | |
| import { getAppLogLevel } from "@/lib/logEnv"; | |
| import { redactLogArgs } from "@/shared/utils/logRedaction"; | |
| const isDev = process.env.NODE_ENV !== "production"; | |
| const baseConfig: pino.LoggerOptions = { | |
| level: getAppLogLevel(isDev ? "debug" : "info"), | |
| base: { service: "omniroute" }, | |
| timestamp: pino.stdTimeFunctions.isoTime, | |
| formatters: { | |
| level(label: string) { | |
| return { level: label }; | |
| }, | |
| }, | |
| // Final defense-in-depth redaction net: runs in the main thread (transport-safe) and | |
| // scrubs credentials that slip into any log message/object/error. See logRedaction.ts. | |
| hooks: { | |
| logMethod(inputArgs: unknown[], method: (...args: unknown[]) => void) { | |
| return (method as (...a: unknown[]) => void).apply(this, redactLogArgs(inputArgs)); | |
| }, | |
| }, | |
| }; | |
| function getTransportCompatibleConfig(): pino.LoggerOptions { | |
| const { formatters, ...rest } = baseConfig; | |
| if (!formatters) return rest; | |
| const { level: _levelFormatter, ...safeFormatters } = formatters; | |
| return Object.keys(safeFormatters).length > 0 ? { ...rest, formatters: safeFormatters } : rest; | |
| } | |
| /** | |
| * Build the logger with optional file transport. | |
| * Uses pino transport targets for all destinations. | |
| */ | |
| function buildLogger(): pino.Logger { | |
| const logConfig = getLogConfig(); | |
| const logLevel = (baseConfig.level as string) || "info"; | |
| const transportConfig = getTransportCompatibleConfig(); | |
| // If file logging is enabled, set up dual transport (stdout + file) | |
| if (logConfig.logToFile) { | |
| try { | |
| // Initialize log directory and rotation | |
| initLogRotation(); | |
| // Resolve to absolute path for pino worker threads | |
| const absLogPath = resolve(logConfig.logFilePath); | |
| if (isDev) { | |
| // Dev: pino-pretty → stdout, JSON → file | |
| return pino({ | |
| ...transportConfig, | |
| transport: { | |
| targets: [ | |
| { | |
| target: "pino-pretty", | |
| options: { | |
| colorize: true, | |
| translateTime: "HH:MM:ss.l", | |
| ignore: "pid,hostname,service", | |
| messageFormat: "[{module}] {msg}", | |
| destination: 1, | |
| }, | |
| level: logLevel, | |
| }, | |
| { | |
| target: "pino/file", | |
| options: { destination: absLogPath, mkdir: true }, | |
| level: logLevel, | |
| }, | |
| ], | |
| }, | |
| }); | |
| } | |
| // Production: JSON → stdout + JSON → file | |
| return pino({ | |
| ...transportConfig, | |
| transport: { | |
| targets: [ | |
| { | |
| target: "pino/file", | |
| options: { destination: 1 }, // stdout | |
| level: logLevel, | |
| }, | |
| { | |
| target: "pino/file", | |
| options: { destination: absLogPath, mkdir: true }, | |
| level: logLevel, | |
| }, | |
| ], | |
| }, | |
| }); | |
| } catch (err) { | |
| // Log the actual error for diagnostics (issue #165) | |
| try { | |
| process.stderr.write( | |
| `[logger] Failed to set up file transport, attempting sync fallback: ${(err as Error)?.message || err}\n` | |
| ); | |
| } catch {} | |
| // Fallback: use sync pino.destination() instead of worker-thread transport | |
| // pino.transport() uses worker threads which can fail in Next.js production bundles | |
| try { | |
| const absLogPath = resolve(logConfig.logFilePath); | |
| const fileDestination = pino.destination({ dest: absLogPath, mkdir: true, sync: true }); | |
| // Production fallback: JSON to both stdout and file via multistream | |
| return pino( | |
| baseConfig, | |
| pino.multistream([ | |
| { stream: process.stdout, level: logLevel as pino.Level }, | |
| { stream: fileDestination, level: logLevel as pino.Level }, | |
| ]) | |
| ); | |
| } catch (fallbackErr) { | |
| try { | |
| process.stderr.write( | |
| `[logger] Sync fallback also failed, falling back to console only: ${(fallbackErr as Error)?.message || fallbackErr}\n` | |
| ); | |
| } catch {} | |
| } | |
| } | |
| } | |
| // Console-only (no file logging) | |
| if (isDev) { | |
| return pino({ | |
| ...baseConfig, | |
| transport: { | |
| target: "pino-pretty", | |
| options: { | |
| colorize: true, | |
| translateTime: "HH:MM:ss.l", | |
| ignore: "pid,hostname,service", | |
| messageFormat: "[{module}] {msg}", | |
| }, | |
| }, | |
| }); | |
| } | |
| return pino(baseConfig); | |
| } | |
| export const logger = buildLogger(); | |
| /** | |
| * Create a child logger with a module tag. | |
| * @param {string} module - Module name for log context (e.g., "proxy", "db", "sse") | |
| * @returns {pino.Logger} | |
| */ | |
| export function createLogger(module: string) { | |
| return logger.child({ module }); | |
| } | |
| export default logger; | |