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;