diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 2891c19524..7f987cb24d 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -5,4 +5,5 @@ export * from "./logger/Logger"; export * from "./logger/SpyTransport"; export * from "./logger/ConsoleTransport"; export * from "./logger/Formatters"; -export * from "./pinoLogger"; +export * from "./pinoLogger/Logger"; +export * from "./pinoLogger/Transports"; diff --git a/packages/logger/src/logger/PagerDutyV2Transport.ts b/packages/logger/src/logger/PagerDutyV2Transport.ts index 489d88bd05..bb3dc1842f 100644 --- a/packages/logger/src/logger/PagerDutyV2Transport.ts +++ b/packages/logger/src/logger/PagerDutyV2Transport.ts @@ -1,32 +1,11 @@ // This transport enables winston logging to send messages to pager duty v2 api. import Transport from "winston-transport"; -import { event } from "@pagerduty/pdjs"; -import * as ss from "superstruct"; -import { removeAnchorTextFromLinks } from "./Formatters"; import { TransportError } from "./TransportError"; +import type { Config } from "../shared/PagerDutyV2Transport"; +import { sendPagerDutyEvent } from "../shared/PagerDutyV2Transport"; type TransportOptions = ConstructorParameters[0]; -export type Severity = "critical" | "error" | "warning" | "info"; -export type Action = "trigger" | "acknowledge" | "resolve"; - -const Config = ss.object({ - integrationKey: ss.string(), - customServices: ss.optional(ss.record(ss.string(), ss.string())), - logTransportErrors: ss.optional(ss.boolean()), -}); -// Config object becomes a type -// { -// integrationKey: string; -// customServices?: Record; -// logTransportErrors?: boolean; -// } -export type Config = ss.Infer; - -// this turns an unknown ( like json parsed data) into a config, or throws an error -export function createConfig(config: unknown): Config { - return ss.create(config, Config); -} export class PagerDutyV2Transport extends Transport { private readonly integrationKey: string; @@ -41,33 +20,12 @@ export class PagerDutyV2Transport extends Transport { this.customServices = customServices; this.logTransportErrors = logTransportErrors; } - // pd v2 severity only supports critical, error, warning or info. - public static convertLevelToSeverity(level?: string): Severity { - if (!level) return "error"; - if (level === "warn") return "warning"; - if (level === "info" || level === "critical") return level; - return "error"; - } // Note: info must be any because that's what the base class uses. async log(info: any, callback: (error?: unknown) => void): Promise { try { // we route to different pd services using the integration key (routing_key), or multiple services with the custom services object const routing_key = this.customServices[info.notificationPath] ?? this.integrationKey; - // PagerDuty does not support anchor text in links, so we remove it from markdown if it exists. - if (typeof info.mrkdwn === "string") info.mrkdwn = removeAnchorTextFromLinks(info.mrkdwn); - await event({ - data: { - routing_key, - event_action: "trigger" as Action, - payload: { - summary: `${info.level}: ${info.at} ⭢ ${info.message}`, - severity: PagerDutyV2Transport.convertLevelToSeverity(info.level), - source: info["bot-identifier"] ? info["bot-identifier"] : undefined, - // we can put any structured data in here as long as it is can be repped as json - custom_details: info, - }, - }, - }); + await sendPagerDutyEvent(routing_key, info); } catch (error) { // We don't want to emit error if this same transport is used to log transport errors to avoid recursion. if (!this.logTransportErrors) return callback(new TransportError("PagerDuty V2", error, info)); diff --git a/packages/logger/src/logger/Transports.ts b/packages/logger/src/logger/Transports.ts index 6bd9c3e943..1c266a1ea1 100644 --- a/packages/logger/src/logger/Transports.ts +++ b/packages/logger/src/logger/Transports.ts @@ -17,11 +17,9 @@ import { createConfig as discordTicketCreateConfig, DiscordTicketTransport, } from "./DiscordTicketTransport"; -import { - PagerDutyV2Transport, - Config as PagerDutyV2Config, - createConfig as pagerDutyV2CreateConfig, -} from "./PagerDutyV2Transport"; +import { PagerDutyV2Transport } from "./PagerDutyV2Transport"; +import type { Config as PagerDutyV2Config } from "../shared/PagerDutyV2Transport"; +import { createConfig as pagerDutyV2CreateConfig } from "../shared/PagerDutyV2Transport"; import { DiscordTransport } from "./DiscordTransport"; import type Transport from "winston-transport"; import dotenv from "dotenv"; diff --git a/packages/logger/src/pinoLogger.ts b/packages/logger/src/pinoLogger/Logger.ts similarity index 57% rename from packages/logger/src/pinoLogger.ts rename to packages/logger/src/pinoLogger/Logger.ts index 40837d8d51..8c133f0e03 100644 --- a/packages/logger/src/pinoLogger.ts +++ b/packages/logger/src/pinoLogger/Logger.ts @@ -1,7 +1,14 @@ -import { pino, LevelWithSilentOrString, Logger as PinoLogger, LoggerOptions as PinoLoggerOptions } from "pino"; +import { + pino, + LevelWithSilentOrString, + Logger as PinoLogger, + LoggerOptions as PinoLoggerOptions, + stdSerializers, +} from "pino"; import { createGcpLoggingPinoConfig } from "@google-cloud/pino-logging-gcp-config"; -import { noBotId } from "./constants"; -import { generateRandomRunId } from "./logger/Logger"; +import { noBotId } from "../constants"; +import { generateRandomRunId } from "../logger/Logger"; +import { createPinoTransports } from "./Transports"; export type { PinoLogger }; export type { PinoLoggerOptions }; @@ -17,7 +24,7 @@ export function createPinoLogger({ runIdentifier = process.env.RUN_IDENTIFIER || generateRandomRunId(), level = "info", }: Partial = {}): PinoLogger { - return pino(createPinoConfig({ botIdentifier, runIdentifier, level })); + return pino(createPinoConfig({ botIdentifier, runIdentifier, level }), createPinoTransports({ level })); } export function createPinoConfig({ @@ -25,8 +32,18 @@ export function createPinoConfig({ runIdentifier = process.env.RUN_IDENTIFIER || generateRandomRunId(), level = "info", }: Partial = {}): PinoLoggerOptions { - return createGcpLoggingPinoConfig(undefined, { + const gcpConfig = createGcpLoggingPinoConfig(undefined, { level, base: { "bot-identifier": botIdentifier, "run-identifier": runIdentifier }, }); + + // Add error serializers to properly log Error objects (matching Winston behavior) + return { + ...gcpConfig, + serializers: { + ...gcpConfig.serializers, + err: stdSerializers.err, + error: stdSerializers.err, // Use err serializer for 'error' field too + }, + }; } diff --git a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts new file mode 100644 index 0000000000..9909176f96 --- /dev/null +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -0,0 +1,23 @@ +// This transport enables pino logging to send messages to PagerDuty v2 API. +// Pino transports run in worker threads for performance, so they can import dependencies. +import build from "pino-abstract-transport"; +import type { Transform } from "stream"; +import type { Config } from "../shared/PagerDutyV2Transport"; +import { createConfig, sendPagerDutyEvent } from "../shared/PagerDutyV2Transport"; + +export default async function (opts: Config): Promise { + const config = createConfig(opts); + + return build(async function (source) { + for await (const obj of source) { + try { + // Get routing key from custom services or use default integration key + const routing_key = config.customServices?.[obj.notificationPath] ?? config.integrationKey; + await sendPagerDutyEvent(routing_key, obj); + } catch (error) { + // Always log transport errors in Pino since there's no callback mechanism like Winston + console.error("PagerDuty v2 transport error:", error); + } + } + }); +} diff --git a/packages/logger/src/pinoLogger/Transports.ts b/packages/logger/src/pinoLogger/Transports.ts new file mode 100644 index 0000000000..f1ab7517bf --- /dev/null +++ b/packages/logger/src/pinoLogger/Transports.ts @@ -0,0 +1,46 @@ +import { transport, TransportTargetOptions } from "pino"; +import type { Config as PagerDutyV2Config } from "../shared/PagerDutyV2Transport"; +import { createConfig as pagerDutyV2CreateConfig } from "../shared/PagerDutyV2Transport"; +import dotenv from "dotenv"; +import minimist from "minimist"; +import path from "path"; + +dotenv.config(); +const argv = minimist(process.argv.slice(), {}); + +interface TransportsConfig { + level?: string; + pagerDutyV2Config?: PagerDutyV2Config & { disabled?: boolean }; +} + +export function createPinoTransports(transportsConfig: TransportsConfig = {}): ReturnType { + const targets: TransportTargetOptions[] = []; + const level = transportsConfig.level || process.env.LOG_LEVEL || "info"; + + // stdout transport (for GCP Logging and local dev) + targets.push({ + target: "pino/file", + level, + options: { destination: 1 }, + }); + + // Skip additional transports in test environment + if (argv._.indexOf("test") === -1) { + // Add PagerDuty V2 transport if configured + if (transportsConfig.pagerDutyV2Config || process.env.PAGER_DUTY_V2_CONFIG) { + // to disable pdv2, pass in a "disabled=true" in configs or env. + const { disabled = false, ...pagerDutyV2Config } = + transportsConfig.pagerDutyV2Config ?? JSON.parse(process.env.PAGER_DUTY_V2_CONFIG || "null"); + // this will throw an error if an invalid configuration is present + if (!disabled) { + targets.push({ + target: path.join(__dirname, "PagerDutyV2Transport.js"), + level: "error", + options: pagerDutyV2CreateConfig(pagerDutyV2Config), + }); + } + } + } + + return transport({ targets }); +} diff --git a/packages/logger/src/shared/PagerDutyV2Transport.ts b/packages/logger/src/shared/PagerDutyV2Transport.ts new file mode 100644 index 0000000000..ed485a11f3 --- /dev/null +++ b/packages/logger/src/shared/PagerDutyV2Transport.ts @@ -0,0 +1,68 @@ +// Shared PagerDuty V2 configuration and utilities +// Used by both Winston and Pino PagerDuty transports +import * as ss from "superstruct"; +import { event } from "@pagerduty/pdjs"; +import { levels } from "pino"; +import { removeAnchorTextFromLinks } from "../logger/Formatters"; + +export type Severity = "critical" | "error" | "warning" | "info"; +export type Action = "trigger" | "acknowledge" | "resolve"; + +const Config = ss.object({ + integrationKey: ss.string(), + customServices: ss.optional(ss.record(ss.string(), ss.string())), + logTransportErrors: ss.optional(ss.boolean()), +}); + +export type Config = ss.Infer; + +// This turns an unknown (like json parsed data) into a config, or throws an error +export function createConfig(config: unknown): Config { + return ss.create(config, Config); +} + +// PD v2 severity only supports critical, error, warning or info. +// Handles both Winston string levels and Pino numeric levels. +export function convertLevelToSeverity(level?: string | number): Severity { + if (!level) return "info"; + + // Convert numeric Pino levels to string names using Pino's built-in mapping + const levelStr = typeof level === "number" ? levels.labels[level] : String(level).toLowerCase(); + + // Map level names to PagerDuty severity values + if (levelStr === "fatal") return "critical"; + if (levelStr === "error") return "error"; + if (levelStr === "warn") return "warning"; + if (levelStr === "info") return "info"; + if (levelStr === "critical") return "critical"; + + // Unknown/unmapped levels (debug, trace, etc.) default to lowest severity + return "info"; +} + +// Send event to PagerDuty V2 API +// Accepts the whole log object and routing key, extracts necessary fields +export async function sendPagerDutyEvent(routing_key: string, logObj: any): Promise { + // PagerDuty does not support anchor text in links, so we remove it from markdown if it exists. + if (typeof logObj.mrkdwn === "string") { + logObj.mrkdwn = removeAnchorTextFromLinks(logObj.mrkdwn); + } + + // Convert numeric Pino levels to strings for summary (Winston already uses strings) + const levelStr = typeof logObj.level === "number" ? levels.labels[logObj.level] : logObj.level; + + const payload: any = { + summary: `${levelStr}: ${logObj.at} ⭢ ${logObj.message}`, + severity: convertLevelToSeverity(logObj.level), + source: logObj["bot-identifier"] ? logObj["bot-identifier"] : undefined, + custom_details: logObj, + }; + + await event({ + data: { + routing_key, + event_action: "trigger" as Action, + payload, + }, + }); +} diff --git a/packages/logger/test/logger/PagerDutyV2Transport.js b/packages/logger/test/logger/PagerDutyV2Transport.js new file mode 100644 index 0000000000..0775334db0 --- /dev/null +++ b/packages/logger/test/logger/PagerDutyV2Transport.js @@ -0,0 +1,160 @@ +const { assert } = require("chai"); +const sinon = require("sinon"); +const { PagerDutyV2Transport } = require("../../dist/logger/PagerDutyV2Transport.js"); +const PagerDutyShared = require("../../dist/shared/PagerDutyV2Transport.js"); + +describe("Winston PagerDutyV2Transport", function () { + let sendPagerDutyEventStub; + + beforeEach(function () { + // Stub the shared sendPagerDutyEvent function + sendPagerDutyEventStub = sinon.stub(PagerDutyShared, "sendPagerDutyEvent").resolves(); + }); + + afterEach(function () { + sendPagerDutyEventStub.restore(); + }); + + describe("Initialization", function () { + it("Should create transport with required config", function () { + const transport = new PagerDutyV2Transport({ level: "error" }, { integrationKey: "test-key" }); + + assert.equal(transport.integrationKey, "test-key"); + assert.deepEqual(transport.customServices, {}); + assert.equal(transport.logTransportErrors, false); + }); + + it("Should create transport with full config", function () { + const transport = new PagerDutyV2Transport( + { level: "error" }, + { + integrationKey: "test-key", + customServices: { path1: "key1" }, + logTransportErrors: true, + } + ); + + assert.equal(transport.integrationKey, "test-key"); + assert.deepEqual(transport.customServices, { path1: "key1" }); + assert.equal(transport.logTransportErrors, true); + }); + }); + + describe("log method", function () { + it("Should send event with default routing key", async function () { + const transport = new PagerDutyV2Transport({ level: "error" }, { integrationKey: "default-key" }); + + const info = { + level: "error", + at: "TestModule", + message: "Test error", + }; + + await new Promise((resolve) => { + transport.log(info, resolve); + }); + + assert(sendPagerDutyEventStub.calledOnce); + assert.equal(sendPagerDutyEventStub.firstCall.args[0], "default-key"); + assert.deepEqual(sendPagerDutyEventStub.firstCall.args[1], info); + }); + + it("Should use custom routing key when notificationPath matches", async function () { + const transport = new PagerDutyV2Transport( + { level: "error" }, + { + integrationKey: "default-key", + customServices: { + "liquidator-error": "liquidator-key", + "monitor-alert": "monitor-key", + }, + } + ); + + const info = { + level: "error", + at: "TestModule", + message: "Test error", + notificationPath: "liquidator-error", + }; + + await new Promise((resolve) => { + transport.log(info, resolve); + }); + + assert(sendPagerDutyEventStub.calledOnce); + assert.equal(sendPagerDutyEventStub.firstCall.args[0], "liquidator-key"); + }); + + it("Should use default routing key when notificationPath doesn't match", async function () { + const transport = new PagerDutyV2Transport( + { level: "error" }, + { + integrationKey: "default-key", + customServices: { "known-path": "custom-key" }, + } + ); + + const info = { + level: "error", + at: "TestModule", + message: "Test error", + notificationPath: "unknown-path", + }; + + await new Promise((resolve) => { + transport.log(info, resolve); + }); + + assert.equal(sendPagerDutyEventStub.firstCall.args[0], "default-key"); + }); + + it("Should call callback on success", async function () { + const transport = new PagerDutyV2Transport({ level: "error" }, { integrationKey: "test-key" }); + + const callback = sinon.spy(); + await transport.log({ level: "error", at: "Test", message: "Test" }, callback); + + assert(callback.calledOnce); + assert(callback.calledWith()); + }); + + it("Should return TransportError in callback when error occurs and logTransportErrors is false", async function () { + sendPagerDutyEventStub.rejects(new Error("API Error")); + + const transport = new PagerDutyV2Transport( + { level: "error" }, + { integrationKey: "test-key", logTransportErrors: false } + ); + + const callback = sinon.spy(); + const info = { level: "error", at: "Test", message: "Test" }; + + await transport.log(info, callback); + + assert(callback.calledOnce); + const error = callback.firstCall.args[0]; + assert(error); + assert.include(error.message, "PagerDuty V2"); + }); + + it("Should log to console when error occurs and logTransportErrors is true", async function () { + sendPagerDutyEventStub.rejects(new Error("API Error")); + const consoleStub = sinon.stub(console, "error"); + + const transport = new PagerDutyV2Transport( + { level: "error" }, + { integrationKey: "test-key", logTransportErrors: true } + ); + + const callback = sinon.spy(); + await transport.log({ level: "error", at: "Test", message: "Test" }, callback); + + assert(consoleStub.calledOnce); + assert.include(consoleStub.firstCall.args[0], "PagerDuty v2 error"); + assert(callback.calledWith()); + + consoleStub.restore(); + }); + }); +}); diff --git a/packages/logger/test/pinoLogger/PagerDutyV2Transport.js b/packages/logger/test/pinoLogger/PagerDutyV2Transport.js new file mode 100644 index 0000000000..31a41fe382 --- /dev/null +++ b/packages/logger/test/pinoLogger/PagerDutyV2Transport.js @@ -0,0 +1,139 @@ +const { assert } = require("chai"); +const sinon = require("sinon"); +const PagerDutyShared = require("../../dist/shared/PagerDutyV2Transport.js"); + +describe("Pino PagerDutyV2Transport", function () { + let sendPagerDutyEventStub; + let createTransport; + + // Helper to write log to Pino transport in correct format (newline-delimited JSON) + function writeLog(transport, logObj) { + transport.write(JSON.stringify(logObj) + "\n"); + } + + beforeEach(function () { + // Stub the shared sendPagerDutyEvent function + sendPagerDutyEventStub = sinon.stub(PagerDutyShared, "sendPagerDutyEvent").resolves(); + + // Dynamically require the transport to ensure fresh module state + delete require.cache[require.resolve("../../dist/pinoLogger/PagerDutyV2Transport.js")]; + createTransport = require("../../dist/pinoLogger/PagerDutyV2Transport.js").default; + }); + + afterEach(function () { + sendPagerDutyEventStub.restore(); + }); + + describe("Initialization", function () { + it("Should create transport with valid config", async function () { + const transport = await createTransport({ integrationKey: "test-key" }); + assert.ok(transport); + assert.ok(transport.write); + }); + + it("Should throw error for invalid config", async function () { + try { + await createTransport({ invalidField: "value" }); + assert.fail("Should have thrown error"); + } catch (error) { + assert.include(error.message, "Expected a string"); + } + }); + }); + + describe("Log processing", function () { + it("Should process log with default routing key", async function () { + const transport = await createTransport({ integrationKey: "default-key" }); + + const logLine = { + level: 50, + at: "TestModule", + message: "Test error", + "bot-identifier": "test-bot", + }; + + // Write the log in Pino format (newline-delimited JSON) + writeLog(transport, logLine); + + // Give async processing time to complete + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(sendPagerDutyEventStub.calledOnce); + assert.equal(sendPagerDutyEventStub.firstCall.args[0], "default-key"); + assert.deepEqual(sendPagerDutyEventStub.firstCall.args[1], logLine); + }); + + it("Should use custom routing key when notificationPath matches", async function () { + const transport = await createTransport({ + integrationKey: "default-key", + customServices: { + "liquidator-error": "liquidator-key", + }, + }); + + const logLine = { + level: 50, + at: "TestModule", + message: "Test error", + notificationPath: "liquidator-error", + }; + + writeLog(transport, logLine); + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(sendPagerDutyEventStub.calledOnce); + assert.equal(sendPagerDutyEventStub.firstCall.args[0], "liquidator-key"); + }); + + it("Should process multiple log entries", async function () { + const transport = await createTransport({ integrationKey: "test-key" }); + + const logs = [ + { level: 50, at: "Module1", message: "Error 1" }, + { level: 40, at: "Module2", message: "Warning 1" }, + { level: 50, at: "Module3", message: "Error 2" }, + ]; + + logs.forEach((log) => writeLog(transport, log)); + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert.equal(sendPagerDutyEventStub.callCount, 3); + }); + + it("Should log errors to console when sendPagerDutyEvent fails", async function () { + sendPagerDutyEventStub.rejects(new Error("API Error")); + const consoleStub = sinon.stub(console, "error"); + + const transport = await createTransport({ integrationKey: "test-key" }); + + const logLine = { level: 50, at: "Test", message: "Test" }; + writeLog(transport, logLine); + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(consoleStub.calledOnce); + assert.include(consoleStub.firstCall.args[0], "PagerDuty v2 transport error"); + + consoleStub.restore(); + }); + + it("Should handle logs with numeric levels (Pino format)", async function () { + const transport = await createTransport({ integrationKey: "test-key" }); + + const pinoLog = { + level: 50, // error + at: "TestModule", + message: "Test error", + timestamp: { seconds: 1234567890, nanos: 123456789 }, + "bot-identifier": "test-bot", + }; + + writeLog(transport, pinoLog); + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(sendPagerDutyEventStub.calledOnce); + const receivedLog = sendPagerDutyEventStub.firstCall.args[1]; + assert.equal(receivedLog.level, 50); + assert.equal(receivedLog.at, "TestModule"); + }); + }); +}); diff --git a/packages/logger/test/shared/PagerDutyV2Transport.js b/packages/logger/test/shared/PagerDutyV2Transport.js new file mode 100644 index 0000000000..df16cc98a5 --- /dev/null +++ b/packages/logger/test/shared/PagerDutyV2Transport.js @@ -0,0 +1,113 @@ +const { assert } = require("chai"); +const { createConfig, convertLevelToSeverity } = require("../../dist/shared/PagerDutyV2Transport.js"); + +describe("PagerDuty V2 Shared Utilities", function () { + describe("createConfig", function () { + it("Should create valid config with required fields", function () { + const config = createConfig({ integrationKey: "test-key-123" }); + assert.equal(config.integrationKey, "test-key-123"); + assert.deepEqual(config.customServices, undefined); + assert.equal(config.logTransportErrors, undefined); + }); + + it("Should create valid config with all fields", function () { + const config = createConfig({ + integrationKey: "test-key-123", + customServices: { path1: "key1", path2: "key2" }, + logTransportErrors: true, + }); + assert.equal(config.integrationKey, "test-key-123"); + assert.deepEqual(config.customServices, { path1: "key1", path2: "key2" }); + assert.equal(config.logTransportErrors, true); + }); + + it("Should throw error for missing integrationKey", function () { + assert.throws(() => createConfig({}), /Expected a string/); + }); + + it("Should throw error for invalid integrationKey type", function () { + assert.throws(() => createConfig({ integrationKey: 123 }), /Expected a string/); + }); + + it("Should throw error for invalid customServices type", function () { + assert.throws( + () => createConfig({ integrationKey: "test-key", customServices: "invalid" }), + /Expected an object/ + ); + }); + }); + + describe("convertLevelToSeverity", function () { + describe("Winston string levels", function () { + it("Should convert error to error", function () { + assert.equal(convertLevelToSeverity("error"), "error"); + }); + + it("Should convert warn to warning", function () { + assert.equal(convertLevelToSeverity("warn"), "warning"); + }); + + it("Should convert info to info", function () { + assert.equal(convertLevelToSeverity("info"), "info"); + }); + + it("Should convert debug to info (lowest severity)", function () { + assert.equal(convertLevelToSeverity("debug"), "info"); + }); + + it("Should convert fatal to critical", function () { + assert.equal(convertLevelToSeverity("fatal"), "critical"); + }); + + it("Should convert critical to critical", function () { + assert.equal(convertLevelToSeverity("critical"), "critical"); + }); + + it("Should handle uppercase levels", function () { + assert.equal(convertLevelToSeverity("ERROR"), "error"); + assert.equal(convertLevelToSeverity("WARN"), "warning"); + }); + + it("Should handle undefined level", function () { + assert.equal(convertLevelToSeverity(undefined), "info"); + }); + }); + + describe("Pino numeric levels", function () { + it("Should convert 60 (fatal) to critical", function () { + // Pino levels.labels[60] = "fatal" which maps to "critical" + assert.equal(convertLevelToSeverity(60), "critical"); + }); + + it("Should convert 50 (error) to error", function () { + // Pino levels.labels[50] = "error" which maps to "error" + assert.equal(convertLevelToSeverity(50), "error"); + }); + + it("Should convert 40 (warn) to warning", function () { + // Pino levels.labels[40] = "warn" which maps to "warning" + assert.equal(convertLevelToSeverity(40), "warning"); + }); + + it("Should convert 30 (info) to info", function () { + // Pino levels.labels[30] = "info" which maps to "info" + assert.equal(convertLevelToSeverity(30), "info"); + }); + + it("Should convert 20 (debug) to info (lowest severity)", function () { + // Pino levels.labels[20] = "debug" which maps to "info" (lowest PD severity) + assert.equal(convertLevelToSeverity(20), "info"); + }); + + it("Should convert 10 (trace) to info (lowest severity)", function () { + // Pino levels.labels[10] = "trace" which maps to "info" (lowest PD severity) + assert.equal(convertLevelToSeverity(10), "info"); + }); + }); + }); + + // Note: sendPagerDutyEvent integration with actual PagerDuty API is tested indirectly + // through Winston and Pino transport tests where the function is properly stubbed. + // Testing the actual API call would require real API credentials and is better suited + // for integration tests run separately from unit tests. +});