From e7106a35ea72a54cc04937b81dd798c5e2eeaf37 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Fri, 19 Dec 2025 14:53:27 +0000 Subject: [PATCH 01/18] fix(logger): pino pagerduty transport Signed-off-by: Reinis Martinsons --- packages/logger/src/index.ts | 2 +- .../logger/src/{pinoLogger.ts => pinoLogger/Logger.ts} | 4 ++-- packages/logger/src/pinoLogger/Transports.ts | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) rename packages/logger/src/{pinoLogger.ts => pinoLogger/Logger.ts} (91%) create mode 100644 packages/logger/src/pinoLogger/Transports.ts diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 2891c19524..35f1d8a3d5 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -5,4 +5,4 @@ export * from "./logger/Logger"; export * from "./logger/SpyTransport"; export * from "./logger/ConsoleTransport"; export * from "./logger/Formatters"; -export * from "./pinoLogger"; +export * from "./pinoLogger/Logger"; diff --git a/packages/logger/src/pinoLogger.ts b/packages/logger/src/pinoLogger/Logger.ts similarity index 91% rename from packages/logger/src/pinoLogger.ts rename to packages/logger/src/pinoLogger/Logger.ts index 40837d8d51..c9aac4b9fe 100644 --- a/packages/logger/src/pinoLogger.ts +++ b/packages/logger/src/pinoLogger/Logger.ts @@ -1,7 +1,7 @@ import { pino, LevelWithSilentOrString, Logger as PinoLogger, LoggerOptions as PinoLoggerOptions } 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"; export type { PinoLogger }; export type { PinoLoggerOptions }; diff --git a/packages/logger/src/pinoLogger/Transports.ts b/packages/logger/src/pinoLogger/Transports.ts new file mode 100644 index 0000000000..afdbebed21 --- /dev/null +++ b/packages/logger/src/pinoLogger/Transports.ts @@ -0,0 +1,10 @@ +import { transport } from "pino"; + +export function createPinoTransports(): ReturnType { + return transport({ + targets: [ + // stdout (GCP Logging) + { target: "pino/file", options: { destination: 1 } }, + ], + }); +} From 4a5205630a0083f4504c9f64209562580956c377 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Thu, 15 Jan 2026 10:25:59 +0000 Subject: [PATCH 02/18] wip Signed-off-by: Reinis Martinsons --- packages/discord-ticket-api/src/server.ts | 15 +++++++-------- packages/logger/src/pinoLogger/Logger.ts | 4 ++-- packages/logger/src/pinoLogger/Transports.ts | 3 ++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/discord-ticket-api/src/server.ts b/packages/discord-ticket-api/src/server.ts index 7b207e5dd3..1158f56c2d 100644 --- a/packages/discord-ticket-api/src/server.ts +++ b/packages/discord-ticket-api/src/server.ts @@ -1,8 +1,8 @@ -import Fastify from "fastify"; +import Fastify, { FastifyBaseLogger } from "fastify"; import helmet from "@fastify/helmet"; import cors from "@fastify/cors"; import sensible from "@fastify/sensible"; -import { createPinoConfig } from "@uma/logger"; +import { createPinoLogger } from "@uma/logger"; import { loadEnv } from "./env.js"; import { createQueue } from "./queue.js"; import { TicketQueueService } from "./services/TicketService.js"; @@ -10,12 +10,11 @@ import { ticketsRoutes } from "./routes/tickets.js"; export async function buildServer(): Promise<{ app: ReturnType; start: () => Promise }> { const env = loadEnv(); - const app = Fastify({ - logger: createPinoConfig({ - level: process.env.LOG_LEVEL || "info", - botIdentifier: process.env.BOT_IDENTIFIER || "ticketing-api", - }), - }); + const logger = createPinoLogger({ + level: process.env.LOG_LEVEL || "info", + botIdentifier: process.env.BOT_IDENTIFIER || "ticketing-api", + }) as FastifyBaseLogger; + const app = Fastify({ loggerInstance: logger }); await app.register(helmet); await app.register(cors, { origin: true, credentials: true }); diff --git a/packages/logger/src/pinoLogger/Logger.ts b/packages/logger/src/pinoLogger/Logger.ts index c9aac4b9fe..34a89b7947 100644 --- a/packages/logger/src/pinoLogger/Logger.ts +++ b/packages/logger/src/pinoLogger/Logger.ts @@ -2,6 +2,7 @@ import { pino, LevelWithSilentOrString, Logger as PinoLogger, LoggerOptions as P import { createGcpLoggingPinoConfig } from "@google-cloud/pino-logging-gcp-config"; import { noBotId } from "../constants"; import { generateRandomRunId } from "../logger/Logger"; +import { createPinoTransports } from "./Transports"; export type { PinoLogger }; export type { PinoLoggerOptions }; @@ -15,9 +16,8 @@ type CustomPinoLoggerOptions = { export function createPinoLogger({ botIdentifier = process.env.BOT_IDENTIFIER || noBotId, runIdentifier = process.env.RUN_IDENTIFIER || generateRandomRunId(), - level = "info", }: Partial = {}): PinoLogger { - return pino(createPinoConfig({ botIdentifier, runIdentifier, level })); + return pino(createPinoConfig({ botIdentifier, runIdentifier }), createPinoTransports()); } export function createPinoConfig({ diff --git a/packages/logger/src/pinoLogger/Transports.ts b/packages/logger/src/pinoLogger/Transports.ts index afdbebed21..adefe3b9cd 100644 --- a/packages/logger/src/pinoLogger/Transports.ts +++ b/packages/logger/src/pinoLogger/Transports.ts @@ -1,10 +1,11 @@ import { transport } from "pino"; export function createPinoTransports(): ReturnType { + const level = "error"; return transport({ targets: [ // stdout (GCP Logging) - { target: "pino/file", options: { destination: 1 } }, + { target: "pino/file", level, options: { destination: 1 } }, ], }); } From 6acb1b1611ba9db03e76666463a396fe087a613a Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Mon, 19 Jan 2026 17:14:35 +0000 Subject: [PATCH 03/18] wip(logger): add pino pagerduty v2 transport with shared config - Create shared PagerDuty config module (types, validation, level conversion) - Implement PagerDutyV2Transport for Pino using pino-abstract-transport - Refactor Winston PagerDutyV2Transport to use shared config - Add createPinoTransports with conditional PagerDuty integration - Fix log level handling (read from env/config instead of hardcoded) - Update createPinoLogger to pass level to transports - Export Transports from logger package index - Match Winston's fail-fast behavior for config validation WIP: needs review and testing before finalizing Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/index.ts | 1 + .../logger/src/logger/PagerDutyV2Transport.ts | 39 +++-------- packages/logger/src/pagerduty/SharedConfig.ts | 37 ++++++++++ packages/logger/src/pinoLogger/Logger.ts | 3 +- .../src/pinoLogger/PagerDutyV2Transport.ts | 67 +++++++++++++++++++ packages/logger/src/pinoLogger/Transports.ts | 53 ++++++++++++--- 6 files changed, 162 insertions(+), 38 deletions(-) create mode 100644 packages/logger/src/pagerduty/SharedConfig.ts create mode 100644 packages/logger/src/pinoLogger/PagerDutyV2Transport.ts diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 35f1d8a3d5..7f987cb24d 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -6,3 +6,4 @@ export * from "./logger/SpyTransport"; export * from "./logger/ConsoleTransport"; export * from "./logger/Formatters"; 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..1f3a675041 100644 --- a/packages/logger/src/logger/PagerDutyV2Transport.ts +++ b/packages/logger/src/logger/PagerDutyV2Transport.ts @@ -1,32 +1,22 @@ // 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 Severity, + type Action, + type Config, + createConfig, + convertLevelToSeverity, +} from "../pagerduty/SharedConfig"; 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); -} +// Re-export types for backwards compatibility +export type { Severity, Action, Config }; +export { createConfig }; export class PagerDutyV2Transport extends Transport { private readonly integrationKey: string; @@ -41,13 +31,6 @@ 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 { @@ -61,7 +44,7 @@ export class PagerDutyV2Transport extends Transport { event_action: "trigger" as Action, payload: { summary: `${info.level}: ${info.at} ⭢ ${info.message}`, - severity: PagerDutyV2Transport.convertLevelToSeverity(info.level), + severity: 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, diff --git a/packages/logger/src/pagerduty/SharedConfig.ts b/packages/logger/src/pagerduty/SharedConfig.ts new file mode 100644 index 0000000000..7a99564f6a --- /dev/null +++ b/packages/logger/src/pagerduty/SharedConfig.ts @@ -0,0 +1,37 @@ +// Shared PagerDuty V2 configuration and utilities +// Used by both Winston and Pino PagerDuty transports +import * as ss from "superstruct"; + +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 (typeof level === "number") { + // Pino uses numeric levels: trace=10, debug=20, info=30, warn=40, error=50, fatal=60 + if (level >= 60) return "critical"; + if (level >= 50) return "error"; + if (level >= 40) return "warning"; + return "info"; + } + if (!level) return "error"; + const levelStr = String(level).toLowerCase(); + if (levelStr === "warn") return "warning"; + if (levelStr === "fatal") return "critical"; + if (levelStr === "info" || levelStr === "critical") return levelStr as Severity; + return "error"; +} diff --git a/packages/logger/src/pinoLogger/Logger.ts b/packages/logger/src/pinoLogger/Logger.ts index 34a89b7947..0e1f0d60be 100644 --- a/packages/logger/src/pinoLogger/Logger.ts +++ b/packages/logger/src/pinoLogger/Logger.ts @@ -16,8 +16,9 @@ type CustomPinoLoggerOptions = { export function createPinoLogger({ botIdentifier = process.env.BOT_IDENTIFIER || noBotId, runIdentifier = process.env.RUN_IDENTIFIER || generateRandomRunId(), + level = "info", }: Partial = {}): PinoLogger { - return pino(createPinoConfig({ botIdentifier, runIdentifier }), createPinoTransports()); + return pino(createPinoConfig({ botIdentifier, runIdentifier, level }), createPinoTransports({ level })); } export function createPinoConfig({ diff --git a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts new file mode 100644 index 0000000000..5545537fc1 --- /dev/null +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -0,0 +1,67 @@ +// 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 { event } from "@pagerduty/pdjs"; +import { + type Severity, + type Action, + type Config, + createConfig, + convertLevelToSeverity, +} from "../pagerduty/SharedConfig"; +import { removeAnchorTextFromLinks } from "../logger/Formatters"; + +// Re-export types for external use +export type { Severity, Action, Config }; +export { createConfig }; + +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; + + // Extract message and format + const message = obj.msg || obj.message || "No message"; + const at = obj.at || obj.name || "unknown"; + const level = obj.level; + + // Remove anchor text from markdown if present + let mrkdwn = obj.mrkdwn; + if (typeof mrkdwn === "string") { + mrkdwn = removeAnchorTextFromLinks(mrkdwn); + } + + // Send event to PagerDuty + await event({ + data: { + routing_key, + event_action: "trigger" as Action, + payload: { + summary: `${level}: ${at} ⭢ ${message}`, + severity: convertLevelToSeverity(level), + source: obj["bot-identifier"] || obj.botIdentifier, + // Include all structured log data + custom_details: obj, + }, + }, + }); + } catch (error) { + // Log transport errors to console to avoid recursion + if (config.logTransportErrors) { + console.error("PagerDuty v2 transport error:", error); + } + } + } + }, + { + // Parse each line as JSON + parse: "lines", + } + ); +} diff --git a/packages/logger/src/pinoLogger/Transports.ts b/packages/logger/src/pinoLogger/Transports.ts index adefe3b9cd..e07dd0f1a8 100644 --- a/packages/logger/src/pinoLogger/Transports.ts +++ b/packages/logger/src/pinoLogger/Transports.ts @@ -1,11 +1,46 @@ -import { transport } from "pino"; - -export function createPinoTransports(): ReturnType { - const level = "error"; - return transport({ - targets: [ - // stdout (GCP Logging) - { target: "pino/file", level, options: { destination: 1 } }, - ], +import { transport, TransportTargetOptions } from "pino"; +import { Config as PagerDutyV2Config, createConfig as pagerDutyV2CreateConfig } from "./PagerDutyV2Transport"; +import dotenv from "dotenv"; +import minimist from "minimist"; +import path from "path"; + +dotenv.config(); +const argv = minimist(process.argv.slice(), {}); + +interface TransportsConfig { + environment?: string; + 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 }); } From a496d2532fb05a22e036b24bdc8d41626f2f4e2b Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 08:10:19 +0000 Subject: [PATCH 04/18] fix(logger): use import type syntax for linter compatibility Change from inline type imports (import { type Foo }) to separate import type statements to match project's linter configuration. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/logger/PagerDutyV2Transport.ts | 9 ++------- packages/logger/src/pinoLogger/PagerDutyV2Transport.ts | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/logger/src/logger/PagerDutyV2Transport.ts b/packages/logger/src/logger/PagerDutyV2Transport.ts index 1f3a675041..74f112e969 100644 --- a/packages/logger/src/logger/PagerDutyV2Transport.ts +++ b/packages/logger/src/logger/PagerDutyV2Transport.ts @@ -4,13 +4,8 @@ import { event } from "@pagerduty/pdjs"; import { removeAnchorTextFromLinks } from "./Formatters"; import { TransportError } from "./TransportError"; -import { - type Severity, - type Action, - type Config, - createConfig, - convertLevelToSeverity, -} from "../pagerduty/SharedConfig"; +import type { Severity, Action, Config } from "../pagerduty/SharedConfig"; +import { createConfig, convertLevelToSeverity } from "../pagerduty/SharedConfig"; type TransportOptions = ConstructorParameters[0]; diff --git a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts index 5545537fc1..819d06b099 100644 --- a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -3,13 +3,8 @@ import build from "pino-abstract-transport"; import type { Transform } from "stream"; import { event } from "@pagerduty/pdjs"; -import { - type Severity, - type Action, - type Config, - createConfig, - convertLevelToSeverity, -} from "../pagerduty/SharedConfig"; +import type { Severity, Action, Config } from "../pagerduty/SharedConfig"; +import { createConfig, convertLevelToSeverity } from "../pagerduty/SharedConfig"; import { removeAnchorTextFromLinks } from "../logger/Formatters"; // Re-export types for external use From 928064e72bb705bdd8b7e236f0c25beb86f3b50b Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 08:38:44 +0000 Subject: [PATCH 05/18] refactor(logger): remove unnecessary PagerDuty re-exports Remove re-exports of Config, createConfig, and types from transport files. These were only used internally and are now imported directly from the SharedConfig module by consumers (Transports.ts files). This makes it clear that these are shared utilities, not transport-specific APIs, and prevents them from being inadvertently exposed in the public API. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/logger/PagerDutyV2Transport.ts | 8 ++------ packages/logger/src/logger/Transports.ts | 8 +++----- packages/logger/src/pinoLogger/PagerDutyV2Transport.ts | 6 +----- packages/logger/src/pinoLogger/Transports.ts | 3 ++- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/logger/src/logger/PagerDutyV2Transport.ts b/packages/logger/src/logger/PagerDutyV2Transport.ts index 74f112e969..c320657ee8 100644 --- a/packages/logger/src/logger/PagerDutyV2Transport.ts +++ b/packages/logger/src/logger/PagerDutyV2Transport.ts @@ -4,15 +4,11 @@ import { event } from "@pagerduty/pdjs"; import { removeAnchorTextFromLinks } from "./Formatters"; import { TransportError } from "./TransportError"; -import type { Severity, Action, Config } from "../pagerduty/SharedConfig"; -import { createConfig, convertLevelToSeverity } from "../pagerduty/SharedConfig"; +import type { Config, Action } from "../pagerduty/SharedConfig"; +import { convertLevelToSeverity } from "../pagerduty/SharedConfig"; type TransportOptions = ConstructorParameters[0]; -// Re-export types for backwards compatibility -export type { Severity, Action, Config }; -export { createConfig }; - export class PagerDutyV2Transport extends Transport { private readonly integrationKey: string; private readonly customServices: { [key: string]: string }; diff --git a/packages/logger/src/logger/Transports.ts b/packages/logger/src/logger/Transports.ts index 6bd9c3e943..7f82837d1e 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 "../pagerduty/SharedConfig"; +import { createConfig as pagerDutyV2CreateConfig } from "../pagerduty/SharedConfig"; import { DiscordTransport } from "./DiscordTransport"; import type Transport from "winston-transport"; import dotenv from "dotenv"; diff --git a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts index 819d06b099..5fde4c76d6 100644 --- a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -3,14 +3,10 @@ import build from "pino-abstract-transport"; import type { Transform } from "stream"; import { event } from "@pagerduty/pdjs"; -import type { Severity, Action, Config } from "../pagerduty/SharedConfig"; +import type { Config, Action } from "../pagerduty/SharedConfig"; import { createConfig, convertLevelToSeverity } from "../pagerduty/SharedConfig"; import { removeAnchorTextFromLinks } from "../logger/Formatters"; -// Re-export types for external use -export type { Severity, Action, Config }; -export { createConfig }; - export default async function (opts: Config): Promise { const config = createConfig(opts); diff --git a/packages/logger/src/pinoLogger/Transports.ts b/packages/logger/src/pinoLogger/Transports.ts index e07dd0f1a8..5484fce787 100644 --- a/packages/logger/src/pinoLogger/Transports.ts +++ b/packages/logger/src/pinoLogger/Transports.ts @@ -1,5 +1,6 @@ import { transport, TransportTargetOptions } from "pino"; -import { Config as PagerDutyV2Config, createConfig as pagerDutyV2CreateConfig } from "./PagerDutyV2Transport"; +import type { Config as PagerDutyV2Config } from "../pagerduty/SharedConfig"; +import { createConfig as pagerDutyV2CreateConfig } from "../pagerduty/SharedConfig"; import dotenv from "dotenv"; import minimist from "minimist"; import path from "path"; From 86e36e71484bc1243c71c50dc8b2418bd7305cfd Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 09:06:27 +0000 Subject: [PATCH 06/18] refactor(logger): remove unused environment param from Pino TransportsConfig The environment parameter was copied from Winston's interface but is not needed for Pino. Unlike Winston, Pino doesn't require different transports for different environments - it always outputs JSON to stdout which GCP automatically parses. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/pinoLogger/Transports.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/logger/src/pinoLogger/Transports.ts b/packages/logger/src/pinoLogger/Transports.ts index 5484fce787..497c7d8d63 100644 --- a/packages/logger/src/pinoLogger/Transports.ts +++ b/packages/logger/src/pinoLogger/Transports.ts @@ -9,7 +9,6 @@ dotenv.config(); const argv = minimist(process.argv.slice(), {}); interface TransportsConfig { - environment?: string; level?: string; pagerDutyV2Config?: PagerDutyV2Config & { disabled?: boolean }; } From 349e9f4017eefb52bf69c83f7d78dddaa8756970 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 09:21:01 +0000 Subject: [PATCH 07/18] fix(logger): properly mutate obj.mrkdwn in Pino PagerDuty transport The previous implementation created a local variable that was never used, so the unmodified mrkdwn was being sent to PagerDuty. Now it directly mutates obj.mrkdwn to match the Winston implementation. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/pinoLogger/PagerDutyV2Transport.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts index 5fde4c76d6..9c97accf91 100644 --- a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -22,11 +22,8 @@ export default async function (opts: Config): Promise Date: Tue, 20 Jan 2026 09:43:05 +0000 Subject: [PATCH 08/18] refactor(logger): simplify PagerDuty event sending with shared helper Extract the PagerDuty event() call to a shared sendPagerDutyEvent() helper that accepts the whole log object and routing key. The helper now handles field extraction (level, message, at, botIdentifier) with fallbacks for both Winston and Pino log formats. This eliminates duplication and simplifies both transports - they now just call sendPagerDutyEvent(routing_key, logObj) instead of manually extracting and passing individual fields. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- .../logger/src/logger/PagerDutyV2Transport.ts | 19 ++---------- packages/logger/src/pagerduty/SharedConfig.ts | 30 +++++++++++++++++++ .../src/pinoLogger/PagerDutyV2Transport.ts | 24 ++------------- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/logger/src/logger/PagerDutyV2Transport.ts b/packages/logger/src/logger/PagerDutyV2Transport.ts index c320657ee8..7b5da96adf 100644 --- a/packages/logger/src/logger/PagerDutyV2Transport.ts +++ b/packages/logger/src/logger/PagerDutyV2Transport.ts @@ -1,11 +1,10 @@ // This transport enables winston logging to send messages to pager duty v2 api. import Transport from "winston-transport"; -import { event } from "@pagerduty/pdjs"; import { removeAnchorTextFromLinks } from "./Formatters"; import { TransportError } from "./TransportError"; -import type { Config, Action } from "../pagerduty/SharedConfig"; -import { convertLevelToSeverity } from "../pagerduty/SharedConfig"; +import type { Config } from "../pagerduty/SharedConfig"; +import { sendPagerDutyEvent } from "../pagerduty/SharedConfig"; type TransportOptions = ConstructorParameters[0]; @@ -29,19 +28,7 @@ export class PagerDutyV2Transport extends Transport { 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: 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/pagerduty/SharedConfig.ts b/packages/logger/src/pagerduty/SharedConfig.ts index 7a99564f6a..d27c097479 100644 --- a/packages/logger/src/pagerduty/SharedConfig.ts +++ b/packages/logger/src/pagerduty/SharedConfig.ts @@ -1,6 +1,7 @@ // Shared PagerDuty V2 configuration and utilities // Used by both Winston and Pino PagerDuty transports import * as ss from "superstruct"; +import { event } from "@pagerduty/pdjs"; export type Severity = "critical" | "error" | "warning" | "info"; export type Action = "trigger" | "acknowledge" | "resolve"; @@ -35,3 +36,32 @@ export function convertLevelToSeverity(level?: string | number): Severity { if (levelStr === "info" || levelStr === "critical") return levelStr as Severity; return "error"; } + +// 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 { + // Extract fields with fallbacks for both Winston and Pino log formats + const level = logObj.level; + const at = logObj.at || logObj.name || "unknown"; + const message = logObj.message || logObj.msg || "No message"; + const botIdentifier = logObj["bot-identifier"] || logObj.botIdentifier; + + const payload: any = { + summary: `${level}: ${at} ⭢ ${message}`, + severity: convertLevelToSeverity(level), + custom_details: logObj, + }; + + // Only include source if botIdentifier is provided + if (botIdentifier) { + payload.source = botIdentifier; + } + + await event({ + data: { + routing_key, + event_action: "trigger" as Action, + payload, + }, + }); +} diff --git a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts index 9c97accf91..70938a517f 100644 --- a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -2,9 +2,8 @@ // 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 { event } from "@pagerduty/pdjs"; -import type { Config, Action } from "../pagerduty/SharedConfig"; -import { createConfig, convertLevelToSeverity } from "../pagerduty/SharedConfig"; +import type { Config } from "../pagerduty/SharedConfig"; +import { createConfig, sendPagerDutyEvent } from "../pagerduty/SharedConfig"; import { removeAnchorTextFromLinks } from "../logger/Formatters"; export default async function (opts: Config): Promise { @@ -17,28 +16,11 @@ export default async function (opts: Config): Promise Date: Tue, 20 Jan 2026 09:54:17 +0000 Subject: [PATCH 09/18] refactor(logger): move markdown cleaning into sendPagerDutyEvent Move the removeAnchorTextFromLinks call into the shared sendPagerDutyEvent helper. This further simplifies both transports and ensures markdown is always cleaned consistently before sending to PagerDuty. Both transports are now extremely simple - just get the routing key and call sendPagerDutyEvent(routing_key, logObj). All field extraction, markdown cleaning, and event formatting is handled by the shared helper. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/logger/PagerDutyV2Transport.ts | 3 --- packages/logger/src/pagerduty/SharedConfig.ts | 6 ++++++ packages/logger/src/pinoLogger/PagerDutyV2Transport.ts | 6 ------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/logger/src/logger/PagerDutyV2Transport.ts b/packages/logger/src/logger/PagerDutyV2Transport.ts index 7b5da96adf..8038fee8e3 100644 --- a/packages/logger/src/logger/PagerDutyV2Transport.ts +++ b/packages/logger/src/logger/PagerDutyV2Transport.ts @@ -1,7 +1,6 @@ // This transport enables winston logging to send messages to pager duty v2 api. import Transport from "winston-transport"; -import { removeAnchorTextFromLinks } from "./Formatters"; import { TransportError } from "./TransportError"; import type { Config } from "../pagerduty/SharedConfig"; import { sendPagerDutyEvent } from "../pagerduty/SharedConfig"; @@ -26,8 +25,6 @@ export class PagerDutyV2Transport extends Transport { 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 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. diff --git a/packages/logger/src/pagerduty/SharedConfig.ts b/packages/logger/src/pagerduty/SharedConfig.ts index d27c097479..c36498c13e 100644 --- a/packages/logger/src/pagerduty/SharedConfig.ts +++ b/packages/logger/src/pagerduty/SharedConfig.ts @@ -2,6 +2,7 @@ // Used by both Winston and Pino PagerDuty transports import * as ss from "superstruct"; import { event } from "@pagerduty/pdjs"; +import { removeAnchorTextFromLinks } from "../logger/Formatters"; export type Severity = "critical" | "error" | "warning" | "info"; export type Action = "trigger" | "acknowledge" | "resolve"; @@ -40,6 +41,11 @@ export function convertLevelToSeverity(level?: string | number): Severity { // 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); + } + // Extract fields with fallbacks for both Winston and Pino log formats const level = logObj.level; const at = logObj.at || logObj.name || "unknown"; diff --git a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts index 70938a517f..65d544ab6f 100644 --- a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -4,7 +4,6 @@ import build from "pino-abstract-transport"; import type { Transform } from "stream"; import type { Config } from "../pagerduty/SharedConfig"; import { createConfig, sendPagerDutyEvent } from "../pagerduty/SharedConfig"; -import { removeAnchorTextFromLinks } from "../logger/Formatters"; export default async function (opts: Config): Promise { const config = createConfig(opts); @@ -15,11 +14,6 @@ export default async function (opts: Config): Promise Date: Tue, 20 Jan 2026 10:08:24 +0000 Subject: [PATCH 10/18] refactor(logger): move PagerDuty shared code to shared/PagerDutyV2Transport.ts Reorganize PagerDuty shared utilities from pagerduty/SharedConfig.ts to shared/PagerDutyV2Transport.ts. This structure: - Matches naming convention of the transport files - Allows shared/ directory to contain other transport utilities - Eliminates the single-file pagerduty/ directory - Clearly indicates these are shared PagerDuty V2 utilities Updated all imports from ../pagerduty/SharedConfig to ../shared/PagerDutyV2Transport. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/logger/PagerDutyV2Transport.ts | 4 ++-- packages/logger/src/logger/Transports.ts | 4 ++-- packages/logger/src/pinoLogger/PagerDutyV2Transport.ts | 4 ++-- packages/logger/src/pinoLogger/Transports.ts | 4 ++-- .../SharedConfig.ts => shared/PagerDutyV2Transport.ts} | 0 5 files changed, 8 insertions(+), 8 deletions(-) rename packages/logger/src/{pagerduty/SharedConfig.ts => shared/PagerDutyV2Transport.ts} (100%) diff --git a/packages/logger/src/logger/PagerDutyV2Transport.ts b/packages/logger/src/logger/PagerDutyV2Transport.ts index 8038fee8e3..bb3dc1842f 100644 --- a/packages/logger/src/logger/PagerDutyV2Transport.ts +++ b/packages/logger/src/logger/PagerDutyV2Transport.ts @@ -2,8 +2,8 @@ import Transport from "winston-transport"; import { TransportError } from "./TransportError"; -import type { Config } from "../pagerduty/SharedConfig"; -import { sendPagerDutyEvent } from "../pagerduty/SharedConfig"; +import type { Config } from "../shared/PagerDutyV2Transport"; +import { sendPagerDutyEvent } from "../shared/PagerDutyV2Transport"; type TransportOptions = ConstructorParameters[0]; diff --git a/packages/logger/src/logger/Transports.ts b/packages/logger/src/logger/Transports.ts index 7f82837d1e..1c266a1ea1 100644 --- a/packages/logger/src/logger/Transports.ts +++ b/packages/logger/src/logger/Transports.ts @@ -18,8 +18,8 @@ import { DiscordTicketTransport, } from "./DiscordTicketTransport"; import { PagerDutyV2Transport } from "./PagerDutyV2Transport"; -import type { Config as PagerDutyV2Config } from "../pagerduty/SharedConfig"; -import { createConfig as pagerDutyV2CreateConfig } from "../pagerduty/SharedConfig"; +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/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts index 65d544ab6f..8bd989eb84 100644 --- a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -2,8 +2,8 @@ // 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 "../pagerduty/SharedConfig"; -import { createConfig, sendPagerDutyEvent } from "../pagerduty/SharedConfig"; +import type { Config } from "../shared/PagerDutyV2Transport"; +import { createConfig, sendPagerDutyEvent } from "../shared/PagerDutyV2Transport"; export default async function (opts: Config): Promise { const config = createConfig(opts); diff --git a/packages/logger/src/pinoLogger/Transports.ts b/packages/logger/src/pinoLogger/Transports.ts index 497c7d8d63..f1ab7517bf 100644 --- a/packages/logger/src/pinoLogger/Transports.ts +++ b/packages/logger/src/pinoLogger/Transports.ts @@ -1,6 +1,6 @@ import { transport, TransportTargetOptions } from "pino"; -import type { Config as PagerDutyV2Config } from "../pagerduty/SharedConfig"; -import { createConfig as pagerDutyV2CreateConfig } from "../pagerduty/SharedConfig"; +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"; diff --git a/packages/logger/src/pagerduty/SharedConfig.ts b/packages/logger/src/shared/PagerDutyV2Transport.ts similarity index 100% rename from packages/logger/src/pagerduty/SharedConfig.ts rename to packages/logger/src/shared/PagerDutyV2Transport.ts From 1cdbd38c1fda13005d081fbc56e26aaad9f842de Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 11:52:38 +0000 Subject: [PATCH 11/18] refactor(logger): simplify PagerDuty payload construction Remove unnecessary intermediate variables and fallbacks from sendPagerDutyEvent. Directly use logObj fields in payload construction to match original Winston implementation behavior. This preserves the original behavior where missing required fields appear as "undefined" in PagerDuty, making bugs in log formatting obvious. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- .../logger/src/shared/PagerDutyV2Transport.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/logger/src/shared/PagerDutyV2Transport.ts b/packages/logger/src/shared/PagerDutyV2Transport.ts index c36498c13e..3730264469 100644 --- a/packages/logger/src/shared/PagerDutyV2Transport.ts +++ b/packages/logger/src/shared/PagerDutyV2Transport.ts @@ -46,21 +46,11 @@ export async function sendPagerDutyEvent(routing_key: string, logObj: any): Prom logObj.mrkdwn = removeAnchorTextFromLinks(logObj.mrkdwn); } - // Extract fields with fallbacks for both Winston and Pino log formats - const level = logObj.level; - const at = logObj.at || logObj.name || "unknown"; - const message = logObj.message || logObj.msg || "No message"; - const botIdentifier = logObj["bot-identifier"] || logObj.botIdentifier; - const payload: any = { - summary: `${level}: ${at} ⭢ ${message}`, - severity: convertLevelToSeverity(level), + summary: `${logObj.level}: ${logObj.at} ⭢ ${logObj.message}`, + severity: convertLevelToSeverity(logObj.level), + source: logObj["bot-identifier"] ? logObj["bot-identifier"] : undefined, custom_details: logObj, - }; - - // Only include source if botIdentifier is provided - if (botIdentifier) { - payload.source = botIdentifier; } await event({ From a292bff766dd880aae52b64bea0cc9c157f180c2 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 12:09:16 +0000 Subject: [PATCH 12/18] fix(logger): add missing semicolon Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/shared/PagerDutyV2Transport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/logger/src/shared/PagerDutyV2Transport.ts b/packages/logger/src/shared/PagerDutyV2Transport.ts index 3730264469..7366a61bc5 100644 --- a/packages/logger/src/shared/PagerDutyV2Transport.ts +++ b/packages/logger/src/shared/PagerDutyV2Transport.ts @@ -51,7 +51,7 @@ export async function sendPagerDutyEvent(routing_key: string, logObj: any): Prom severity: convertLevelToSeverity(logObj.level), source: logObj["bot-identifier"] ? logObj["bot-identifier"] : undefined, custom_details: logObj, - } + }; await event({ data: { From 01edd525a6db285f34518f5ea3a541b006c6b499 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 13:41:11 +0000 Subject: [PATCH 13/18] fix(logger): fix Pino PagerDuty transport to correctly parse logs - Remove parse: "lines" option which was causing logs to be passed as strings instead of parsed objects - Use Pino's levels.labels to convert numeric levels (50) to strings ("error") in summary - Always log transport errors to console in Pino (no callback mechanism like Winston) This fixes the issue where Pino logs were not being sent to PagerDuty due to incorrect parsing. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- .../src/pinoLogger/PagerDutyV2Transport.ts | 28 +++++++------------ .../logger/src/shared/PagerDutyV2Transport.ts | 6 +++- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts index 8bd989eb84..9909176f96 100644 --- a/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts +++ b/packages/logger/src/pinoLogger/PagerDutyV2Transport.ts @@ -8,24 +8,16 @@ 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) { - // Log transport errors to console to avoid recursion - if (config.logTransportErrors) { - console.error("PagerDuty v2 transport error:", error); - } - } + 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); } - }, - { - // Parse each line as JSON - parse: "lines", } - ); + }); } diff --git a/packages/logger/src/shared/PagerDutyV2Transport.ts b/packages/logger/src/shared/PagerDutyV2Transport.ts index 7366a61bc5..035486422d 100644 --- a/packages/logger/src/shared/PagerDutyV2Transport.ts +++ b/packages/logger/src/shared/PagerDutyV2Transport.ts @@ -2,6 +2,7 @@ // 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"; @@ -46,8 +47,11 @@ export async function sendPagerDutyEvent(routing_key: string, logObj: any): Prom 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: `${logObj.level}: ${logObj.at} ⭢ ${logObj.message}`, + summary: `${levelStr}: ${logObj.at} ⭢ ${logObj.message}`, severity: convertLevelToSeverity(logObj.level), source: logObj["bot-identifier"] ? logObj["bot-identifier"] : undefined, custom_details: logObj, From 10c160b4bf93091c3dcf4b154fce7975c6ca7d04 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 14:03:35 +0000 Subject: [PATCH 14/18] feat(logger): add error serializers to Pino logger Configure Pino with stdSerializers.err to properly serialize Error objects with type, message, and stack properties. This matches Winston's errorStackTracerFormatter behavior and ensures errors are logged correctly instead of appearing as empty objects. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/pinoLogger/Logger.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/logger/src/pinoLogger/Logger.ts b/packages/logger/src/pinoLogger/Logger.ts index 0e1f0d60be..8c133f0e03 100644 --- a/packages/logger/src/pinoLogger/Logger.ts +++ b/packages/logger/src/pinoLogger/Logger.ts @@ -1,4 +1,10 @@ -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"; @@ -26,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 + }, + }; } From 7a82824ff1da772421ff78147af3ebdd89935415 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 14:09:10 +0000 Subject: [PATCH 15/18] refactor(logger): use Pino levels.labels in convertLevelToSeverity Replace manual numeric range checks with Pino's levels.labels mapping to convert numeric levels to strings. This is cleaner, more maintainable, and reuses Pino's existing infrastructure. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/shared/PagerDutyV2Transport.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/logger/src/shared/PagerDutyV2Transport.ts b/packages/logger/src/shared/PagerDutyV2Transport.ts index 035486422d..9b404bca1f 100644 --- a/packages/logger/src/shared/PagerDutyV2Transport.ts +++ b/packages/logger/src/shared/PagerDutyV2Transport.ts @@ -24,15 +24,12 @@ export function createConfig(config: unknown): 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 (typeof level === "number") { - // Pino uses numeric levels: trace=10, debug=20, info=30, warn=40, error=50, fatal=60 - if (level >= 60) return "critical"; - if (level >= 50) return "error"; - if (level >= 40) return "warning"; - return "info"; - } if (!level) return "error"; - const levelStr = String(level).toLowerCase(); + + // 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 === "warn") return "warning"; if (levelStr === "fatal") return "critical"; if (levelStr === "info" || levelStr === "critical") return levelStr as Severity; From 4f2f9fb0cafacaaa17f230d04455719da5a03f05 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 15:07:41 +0000 Subject: [PATCH 16/18] fix(logger): correct severity mapping for unmapped log levels Fix convertLevelToSeverity to map unmapped levels (debug, trace) to "info" (lowest PagerDuty severity) instead of "error" (high severity). This bug was previously hidden because PagerDuty transports filter at level: "error", so debug/trace logs never reached the function in practice. This ensures correct severity mapping if transport level filtering is changed. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/logger/src/shared/PagerDutyV2Transport.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/logger/src/shared/PagerDutyV2Transport.ts b/packages/logger/src/shared/PagerDutyV2Transport.ts index 9b404bca1f..ed485a11f3 100644 --- a/packages/logger/src/shared/PagerDutyV2Transport.ts +++ b/packages/logger/src/shared/PagerDutyV2Transport.ts @@ -24,16 +24,20 @@ export function createConfig(config: unknown): 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 "error"; + 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 === "warn") return "warning"; if (levelStr === "fatal") return "critical"; - if (levelStr === "info" || levelStr === "critical") return levelStr as Severity; - return "error"; + 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 From ef55b58aefeda47555d19d50146f247ab56c9d15 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 15:22:34 +0000 Subject: [PATCH 17/18] test(logger): add comprehensive tests for PagerDuty V2 transports Add unit tests for: - Shared utilities (createConfig, convertLevelToSeverity) - Winston PagerDutyV2Transport (routing keys, error handling, callbacks) - Pino PagerDutyV2Transport (stream processing, routing keys, error handling) Tests verify correct behavior for both Winston string levels and Pino numeric levels, custom routing keys via notificationPath, and proper error handling. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- .../test/logger/PagerDutyV2Transport.js | 160 ++++++++++++++++++ .../test/pinoLogger/PagerDutyV2Transport.js | 139 +++++++++++++++ .../test/shared/PagerDutyV2Transport.js | 113 +++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 packages/logger/test/logger/PagerDutyV2Transport.js create mode 100644 packages/logger/test/pinoLogger/PagerDutyV2Transport.js create mode 100644 packages/logger/test/shared/PagerDutyV2Transport.js 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. +}); From 9f838c1338a3b04a52a949836ceeaee813409ba4 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 20 Jan 2026 15:30:40 +0000 Subject: [PATCH 18/18] revert(discord-ticket-api): revert server.ts to master version Revert discord-ticket-api server.ts changes to master version. These changes will be submitted in a separate PR. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Reinis Martinsons --- packages/discord-ticket-api/src/server.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/discord-ticket-api/src/server.ts b/packages/discord-ticket-api/src/server.ts index 1158f56c2d..7b207e5dd3 100644 --- a/packages/discord-ticket-api/src/server.ts +++ b/packages/discord-ticket-api/src/server.ts @@ -1,8 +1,8 @@ -import Fastify, { FastifyBaseLogger } from "fastify"; +import Fastify from "fastify"; import helmet from "@fastify/helmet"; import cors from "@fastify/cors"; import sensible from "@fastify/sensible"; -import { createPinoLogger } from "@uma/logger"; +import { createPinoConfig } from "@uma/logger"; import { loadEnv } from "./env.js"; import { createQueue } from "./queue.js"; import { TicketQueueService } from "./services/TicketService.js"; @@ -10,11 +10,12 @@ import { ticketsRoutes } from "./routes/tickets.js"; export async function buildServer(): Promise<{ app: ReturnType; start: () => Promise }> { const env = loadEnv(); - const logger = createPinoLogger({ - level: process.env.LOG_LEVEL || "info", - botIdentifier: process.env.BOT_IDENTIFIER || "ticketing-api", - }) as FastifyBaseLogger; - const app = Fastify({ loggerInstance: logger }); + const app = Fastify({ + logger: createPinoConfig({ + level: process.env.LOG_LEVEL || "info", + botIdentifier: process.env.BOT_IDENTIFIER || "ticketing-api", + }), + }); await app.register(helmet); await app.register(cors, { origin: true, credentials: true });