Skip to content

Instantly share code, notes, and snippets.

@micahjon
Created August 24, 2025 04:23
Show Gist options
  • Save micahjon/9c7aeaa9246cd6f1095214d16428ab57 to your computer and use it in GitHub Desktop.
Save micahjon/9c7aeaa9246cd6f1095214d16428ab57 to your computer and use it in GitHub Desktop.
Workaround for Pino Logs -> Sentry
import pino from 'pino';
const level = process.env.LOG_LEVEL || 'info';
export const logger = pino({
level, // Must be set to minimum level used by all transports
transport: {
targets: [
{
// Alas, logs to console are not consistent. If they aren't present, just try again.
// For some reason they're more consistent when running a file that imports this logger
// vs running this file directly...
target: 'pino-pretty',
options: {
translateTime: 'SYS:standard',
destination: 1,
},
level, // Must be set here or defaults to 'info'
},
{
// See https://github.com/getsentry/sentry-javascript/issues/16723
target: './sentry-pino-transport.ts',
level, // Must be set here or defaults to 'info'
},
],
},
});
console.log(`Logger initialized with level: ${logger.level}`);
// Capture uncaught exceptions
process.on('uncaughtException', (err) => {
logger.fatal({ err }, 'Uncaught Exception');
process.exit(1);
});
// Capture unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
logger.error({ reason, promise }, 'Unhandled Promise Rejection');
});
import type { LogSeverityLevel } from '@sentry/core';
import { _INTERNAL_captureLog, isPrimitive, normalize } from '@sentry/core';
import type buildType from 'pino-abstract-transport';
import * as pinoAbstractTransport from 'pino-abstract-transport';
//
// Adapted from Sentry PR #16667
// https://github.com/getsentry/sentry-javascript/pull/16667/files?w=1#diff-ec4890592490487c3b44d8f0de6357f6e9b3c4daebff393024474bb8551a1f0d
//
// Handle both CommonJS and ES module exports
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
const build = (pinoAbstractTransport as any).default || pinoAbstractTransport;
/**
* The default log levels that will be captured by the Sentry Pino transport.
*/
const DEFAULT_CAPTURED_LEVELS: Array<LogSeverityLevel> = [
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
];
/**
* Options for the Sentry Pino transport.
*/
export interface SentryPinoTransportOptions {
/**
* Use this option to filter which levels should be captured as logs.
* By default, all levels are captured as logs.
*
* @example
* ```ts
* const logger = pino({
* transport: {
* target: '@sentry/pino-transport',
* options: {
* logLevels: ['error', 'warn'], // Only capture error and warn logs
* },
* },
* });
* ```
*/
logLevels?: Array<LogSeverityLevel>;
}
/**
* Pino source configuration passed to the transport.
* This interface represents the configuration options that Pino provides to transports.
*/
interface PinoSourceConfig {
/**
* Custom levels configuration from Pino.
* Contains the mapping of custom level names to numeric values.
*
* @default undefined
* @example { values: { critical: 55, notice: 35 } }
*/
levels?: unknown;
/**
* The property name used for the log message.
* Pino allows customizing which property contains the main log message.
*
* @default 'msg'
* @example 'message' when configured with messageKey: 'message'
* @see https://getpino.io/#/docs/api?id=messagekey-string
*/
messageKey?: string;
/**
* The property name used for error objects.
* Pino allows customizing which property contains error information.
*
* @default 'err'
* @example 'error' when configured with errorKey: 'error'
* @see https://getpino.io/#/docs/api?id=errorkey-string
*/
errorKey?: string;
/**
* The property name used to nest logged objects to avoid conflicts.
* When set, Pino nests all logged objects under this key to prevent
* conflicts with Pino's internal properties (level, time, pid, etc.).
* The transport flattens these nested properties using dot notation.
*
* @default undefined (no nesting)
* @example 'payload' - objects logged will be nested under { payload: {...} }
* @see https://getpino.io/#/docs/api?id=nestedkey-string
*/
nestedKey?: string;
}
/**
* Creates a new Sentry Pino transport that forwards logs to Sentry. Requires `_experiments.enableLogs` to be enabled.
*
* Supports Pino v8 and v9.
*
* @param options - Options for the transport.
* @returns A Pino transport that forwards logs to Sentry.
*
* @experimental This method will experience breaking changes. This is not yet part of
* the stable Sentry SDK API and can be changed or removed without warning.
*/
export function createSentryPinoTransport(
options?: SentryPinoTransportOptions
): ReturnType<typeof buildType> {
const capturedLogLevels = new Set(options?.logLevels ?? DEFAULT_CAPTURED_LEVELS);
return build(
async function (source: AsyncIterable<unknown> & PinoSourceConfig) {
for await (const log of source) {
try {
if (!isObject(log)) {
continue;
}
// Use Pino's messageKey if available, fallback to 'msg'
const messageKey = source.messageKey || 'msg';
const message = log[messageKey];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [messageKey]: _, level, time, ...attributes } = log;
// Handle nestedKey flattening if configured
if (
source.nestedKey &&
attributes[source.nestedKey] &&
isObject(attributes[source.nestedKey])
) {
const nestedObject = attributes[source.nestedKey] as Record<
string,
unknown
>;
// Remove the nested object and flatten its properties
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete attributes[source.nestedKey];
// Flatten nested properties with dot notation
for (const [key, value] of Object.entries(nestedObject)) {
attributes[`${source.nestedKey}.${key}`] = value;
}
}
const logSeverityLevel = mapPinoLevelToSentryLevel(log.level, source.levels);
if (capturedLogLevels.has(logSeverityLevel)) {
const logAttributes: Record<string, unknown> = {
...attributes,
'sentry.origin': 'auto.logging.pino',
};
// Attach custom level as an attribute if it's a string (custom level)
if (typeof log.level === 'string') {
logAttributes['sentry.pino.level'] = log.level;
}
_INTERNAL_captureLog({
level: logSeverityLevel,
message: formatMessage(message),
attributes: logAttributes,
});
}
} catch {
// Silently ignore errors to prevent breaking the logging pipeline
}
}
},
{
expectPinoConfig: true,
}
);
}
function formatMessage(message: unknown): string {
if (message === undefined) {
return '';
}
if (isPrimitive(message)) {
return String(message);
}
return JSON.stringify(normalize(message));
}
/**
* Maps a Pino log level (numeric or custom string) to a Sentry log severity level.
*
* Handles both standard and custom levels, including when `useOnlyCustomLevels` is enabled.
* Uses range-based mapping for numeric levels to handle custom values (e.g., 11 -> trace).
*/
function mapPinoLevelToSentryLevel(
level: unknown,
levelsConfig?: unknown
): LogSeverityLevel {
// Handle numeric levels
if (typeof level === 'number') {
return mapNumericLevelToSentryLevel(level);
}
// Handle custom string levels
if (
typeof level === 'string' &&
isObject(levelsConfig) &&
'values' in levelsConfig &&
isObject(levelsConfig.values)
) {
// Map custom string levels to numeric then to Sentry levels
const numericLevel = levelsConfig.values[level];
if (typeof numericLevel === 'number') {
return mapNumericLevelToSentryLevel(numericLevel);
}
}
// Default fallback
return 'info';
}
/**
* Maps a numeric level to the closest Sentry severity level using range-based mapping.
* Handles both standard Pino levels and custom numeric levels.
*
* - `0-19` -> `trace`
* - `20-29` -> `debug`
* - `30-39` -> `info`
* - `40-49` -> `warn`
* - `50-59` -> `error`
* - `60+` -> `fatal`
*
* @see https://github.com/pinojs/pino/blob/116b1b17935630b97222fbfd1c053d199d18ca4b/lib/constants.js#L6-L13
*/
function mapNumericLevelToSentryLevel(numericLevel: number): LogSeverityLevel {
// 0-19 -> trace
if (numericLevel < 20) {
return 'trace';
}
// 20-29 -> debug
if (numericLevel < 30) {
return 'debug';
}
// 30-39 -> info
if (numericLevel < 40) {
return 'info';
}
// 40-49 -> warn
if (numericLevel < 50) {
return 'warn';
}
// 50-59 -> error
if (numericLevel < 60) {
return 'error';
}
// 60+ -> fatal
return 'fatal';
}
/**
* Type guard to check if a value is an object.
*/
function isObject(value: unknown): value is Record<string | number, unknown> {
return typeof value === 'object' && value != null;
}
export default createSentryPinoTransport;
import * as Sentry from '@sentry/bun';
// See https://github.com/getsentry/sentry-javascript/issues/16723
// TODO: replace once this issue is resolved: https://github.com/getsentry/sentry-javascript/issues/15952
const sentryEnvName = process.env.SENTRY_ENV_NAME || 'unknown';
Sentry.init({
dsn: 'STICK DSN HERE...',
environment: sentryEnvName,
enableLogs: true,
// debug: true,
});
import { createSentryPinoTransport } from './sentry-pino-transport-package-source';
export default createSentryPinoTransport;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment