Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions packages/discord-ticket-api/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
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";
import { ticketsRoutes } from "./routes/tickets.js";

export async function buildServer(): Promise<{ app: ReturnType<typeof Fastify>; start: () => Promise<void> }> {
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 });
Expand Down
3 changes: 2 additions & 1 deletion packages/logger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
39 changes: 11 additions & 28 deletions packages/logger/src/logger/PagerDutyV2Transport.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Transport>[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<string,string>;
// logTransportErrors?: 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);
}
// Re-export types for backwards compatibility
export type { Severity, Action, Config };
export { createConfig };

export class PagerDutyV2Transport extends Transport {
private readonly integrationKey: string;
Expand All @@ -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<void> {
try {
Expand All @@ -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,
Expand Down
37 changes: 37 additions & 0 deletions packages/logger/src/pagerduty/SharedConfig.ts
Original file line number Diff line number Diff line change
@@ -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<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 (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";
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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";
import { createPinoTransports } from "./Transports";

export type { PinoLogger };
export type { PinoLoggerOptions };
Expand All @@ -17,7 +18,7 @@ export function createPinoLogger({
runIdentifier = process.env.RUN_IDENTIFIER || generateRandomRunId(),
level = "info",
}: Partial<CustomPinoLoggerOptions> = {}): PinoLogger {
return pino(createPinoConfig({ botIdentifier, runIdentifier, level }));
return pino(createPinoConfig({ botIdentifier, runIdentifier, level }), createPinoTransports({ level }));
}

export function createPinoConfig({
Expand Down
67 changes: 67 additions & 0 deletions packages/logger/src/pinoLogger/PagerDutyV2Transport.ts
Original file line number Diff line number Diff line change
@@ -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<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;

// 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",
}
);
}
46 changes: 46 additions & 0 deletions packages/logger/src/pinoLogger/Transports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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<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");
// 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 });
}