File size: 5,227 Bytes
6111b2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | /**
* 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";
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 };
},
},
};
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;
|