-
Notifications
You must be signed in to change notification settings - Fork 202
fix(logger): add Pino PagerDuty V2 transport support #4914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e7106a3
4a52056
6acb1b1
a496d25
928064e
86e36e7
349e9f4
12ca0c6
e7f3b8e
ff09b1a
1cdbd38
a292bff
01edd52
10c160b
7a82824
4f2f9fb
ef55b58
9f838c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Transform & build.OnUnknown> { | ||
| 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); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<typeof transport> { | ||
| 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"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Eventually adding a test with an invalid config could be nice. |
||
| // 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 }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<typeof Config>; | ||
|
|
||
| // 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"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I notice Winston was returning an error if if (!level), but agree it makes more sense to return info here |
||
|
|
||
| // 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<void> { | ||
| // 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, | ||
| }, | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add more context here like: