Files
formbricks/packages/logger/src/logger.ts
2026-03-18 06:32:55 +00:00

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");
}
}