mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-19 00:23:35 -05:00
184 lines
5.3 KiB
TypeScript
184 lines
5.3 KiB
TypeScript
import Pino, { type Logger, type LoggerOptions, stdSerializers } from "pino";
|
|
import { type TLogLevel, ZLogLevel } from "../types/logger";
|
|
|
|
const IS_PRODUCTION = !process.env.NODE_ENV || process.env.NODE_ENV === "production";
|
|
const IS_BUILD = process.env.NEXT_PHASE === "phase-production-build";
|
|
|
|
const getLogLevel = (): TLogLevel => {
|
|
let logLevel: TLogLevel = "info";
|
|
|
|
if (IS_PRODUCTION) logLevel = "warn";
|
|
if (IS_BUILD) logLevel = "error"; // Only show errors during build
|
|
|
|
const envLogLevel = process.env.LOG_LEVEL;
|
|
|
|
const logLevelResult = ZLogLevel.safeParse(envLogLevel);
|
|
if (logLevelResult.success) logLevel = logLevelResult.data;
|
|
|
|
return logLevel;
|
|
};
|
|
|
|
const baseLoggerConfig: LoggerOptions = {
|
|
level: getLogLevel(),
|
|
serializers: {
|
|
err: stdSerializers.err,
|
|
req: stdSerializers.req,
|
|
res: stdSerializers.res,
|
|
},
|
|
customLevels: {
|
|
debug: 20,
|
|
info: 30,
|
|
warn: 40,
|
|
error: 50,
|
|
fatal: 60,
|
|
audit: 90,
|
|
},
|
|
useOnlyCustomLevels: true,
|
|
timestamp: true,
|
|
name: "formbricks",
|
|
};
|
|
|
|
/**
|
|
* Build transport configuration based on environment.
|
|
* - Development: pino-pretty for readable console output
|
|
* - Production: JSON to stdout (default Pino behavior)
|
|
* - Both: optional pino-opentelemetry-transport for SigNoz log correlation when OTEL is configured
|
|
*/
|
|
const buildTransport = (): LoggerOptions["transport"] => {
|
|
const isEdgeRuntime = process.env.NEXT_RUNTIME === "edge";
|
|
|
|
const hasOtelEndpoint =
|
|
process.env.NEXT_RUNTIME === "nodejs" && Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
|
|
|
|
const serviceName = process.env.OTEL_SERVICE_NAME ?? "formbricks";
|
|
const serviceVersion = process.env.npm_package_version ?? "0.0.0";
|
|
|
|
const otelTarget = {
|
|
target: "pino-opentelemetry-transport",
|
|
options: {
|
|
resourceAttributes: {
|
|
"service.name": serviceName,
|
|
"service.version": serviceVersion,
|
|
"deployment.environment": process.env.ENVIRONMENT ?? process.env.NODE_ENV ?? "development",
|
|
},
|
|
},
|
|
level: getLogLevel(),
|
|
};
|
|
|
|
const prettyTarget = {
|
|
target: "pino-pretty",
|
|
options: {
|
|
colorize: true,
|
|
levelFirst: true,
|
|
translateTime: "SYS:standard",
|
|
ignore: "pid,hostname,ip,requestId",
|
|
customLevels: "trace:10,debug:20,info:30,warn:40,error:50,fatal:60,audit:90",
|
|
useOnlyCustomProps: true,
|
|
},
|
|
level: getLogLevel(),
|
|
};
|
|
|
|
if (!IS_PRODUCTION) {
|
|
// Edge Runtime does not support worker_threads — skip pino-pretty to avoid crashes
|
|
if (isEdgeRuntime) {
|
|
return undefined;
|
|
}
|
|
|
|
// Development: pretty print + optional OTEL
|
|
if (hasOtelEndpoint) {
|
|
return { targets: [prettyTarget, otelTarget] };
|
|
}
|
|
return { target: prettyTarget.target, options: prettyTarget.options };
|
|
}
|
|
|
|
// Production: stdout JSON + optional OTEL
|
|
if (hasOtelEndpoint) {
|
|
const fileTarget = {
|
|
target: "pino/file",
|
|
options: { destination: 1 }, // stdout
|
|
level: getLogLevel(),
|
|
};
|
|
return { targets: [fileTarget, otelTarget] };
|
|
}
|
|
|
|
return undefined; // Default JSON to stdout
|
|
};
|
|
|
|
const transport = buildTransport();
|
|
|
|
// Pino does not allow custom `formatters` (functions) when using multi-target transports
|
|
// because targets run in worker threads and functions cannot be serialized.
|
|
// Only attach the level formatter when we're NOT using `{ targets: [...] }`.
|
|
const useMultiTransport = transport !== undefined && "targets" in transport;
|
|
|
|
const loggerConfig: LoggerOptions = {
|
|
...baseLoggerConfig,
|
|
transport,
|
|
...(!useMultiTransport && {
|
|
formatters: {
|
|
level: (label) => ({ level: label }),
|
|
},
|
|
}),
|
|
};
|
|
|
|
const pinoLogger: Logger = Pino(loggerConfig);
|
|
|
|
// Ensure all log levels are properly bound
|
|
const boundLogger = {
|
|
debug: pinoLogger.debug.bind(pinoLogger),
|
|
info: pinoLogger.info.bind(pinoLogger),
|
|
audit: (pinoLogger as Logger & { audit: typeof pinoLogger.info }).audit.bind(pinoLogger),
|
|
warn: pinoLogger.warn.bind(pinoLogger),
|
|
error: pinoLogger.error.bind(pinoLogger),
|
|
fatal: pinoLogger.fatal.bind(pinoLogger),
|
|
};
|
|
|
|
const extendedLogger = {
|
|
...boundLogger,
|
|
withContext: (context: Record<string, unknown>) => pinoLogger.child(context),
|
|
request: (req: Request) =>
|
|
pinoLogger.child({
|
|
method: req.method,
|
|
url: req.url,
|
|
}),
|
|
};
|
|
|
|
export type ExtendedLogger = typeof extendedLogger;
|
|
export const logger: ExtendedLogger = extendedLogger;
|
|
|
|
const handleShutdown = (event: string, err?: Error): void => {
|
|
if (err) {
|
|
logger.error(err, `Error during shutdown (${event})`);
|
|
}
|
|
logger.info({ event }, "Process is exiting");
|
|
|
|
pinoLogger.flush();
|
|
};
|
|
|
|
// Create a separate function for attaching Node.js process handlers
|
|
const attachNodeProcessHandlers = (): void => {
|
|
// Only attach handlers if we're in a Node.js environment with full process support
|
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
process.on("uncaughtException", (err) => {
|
|
handleShutdown("uncaughtException", err);
|
|
});
|
|
process.on("unhandledRejection", (err) => {
|
|
handleShutdown("unhandledRejection", err as Error);
|
|
});
|
|
process.on("SIGTERM", () => {
|
|
handleShutdown("SIGTERM");
|
|
});
|
|
process.on("SIGINT", () => {
|
|
handleShutdown("SIGINT");
|
|
});
|
|
}
|
|
};
|
|
|
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
try {
|
|
attachNodeProcessHandlers();
|
|
} catch (e) {
|
|
logger.error(e, "Error attaching process event handlers");
|
|
}
|
|
}
|