@logtape/sentry
Version:
LogTape Sentry sink
247 lines (245 loc) • 8.46 kB
JavaScript
import { compareLogLevel, getLogger } from "@logtape/logtape";
import * as SentryCore from "@sentry/core";
import { inspect } from "#util";
//#region src/mod.ts
/**
* Converts a LogTape {@link LogRecord} into a Sentry {@link ParameterizedString}.
*
* This preserves the template structure for better message grouping in Sentry,
* allowing similar messages with different values to be grouped together.
*
* @param record The log record to convert.
* @returns A parameterized string with template and values.
*/
function getParameterizedString(record) {
let result = "";
let tplString = "";
const tplValues = [];
for (let i = 0; i < record.message.length; i++) if (i % 2 === 0) {
result += record.message[i];
tplString += String(record.message[i]).replaceAll("%", "%%");
} else {
const value = inspect(record.message[i]);
result += value;
tplString += `%s`;
tplValues.push(value);
}
const paramStr = new String(result);
paramStr.__sentry_template_string__ = tplString;
paramStr.__sentry_template_values__ = tplValues;
return paramStr;
}
function mapLevelForEvents(level) {
switch (level) {
case "trace": return "debug";
default: return level;
}
}
function mapLevelForLogs(level) {
switch (level) {
case "trace": return "debug";
case "warning": return "warn";
case "debug":
case "info":
case "error":
case "fatal": return level;
default: return "info";
}
}
const defaultErrorPropertyNames = ["error", "err"];
function getErrorProperty(properties, propertyNames) {
for (const property of propertyNames) if (properties[property] instanceof Error) return [property, properties[property]];
return void 0;
}
/**
* Gets a LogTape sink that sends logs to Sentry.
*
* This sink uses Sentry's global capture functions from `@sentry/core` by
* default, following Sentry v8+ best practices. Simply call `Sentry.init()`
* before creating the sink, and it will automatically use your initialized
* client when both packages resolve the same Sentry module instance.
*
* @param optionsOrClient Optional configuration. Can be:
* - Omitted: Uses global Sentry functions (recommended)
* - Object with options: Configure sink behavior
* - Object with `sentry`: Use an application-provided Sentry SDK namespace
* - Sentry client instance: Backward compatibility (deprecated)
* @returns A LogTape sink that sends logs to Sentry.
*
* @example Recommended usage - no parameters
* ```typescript
* import { configure } from "@logtape/logtape";
* import { getSentrySink } from "@logtape/sentry";
* import * as Sentry from "@sentry/node";
*
* Sentry.init({ dsn: process.env.SENTRY_DSN });
*
* await configure({
* sinks: {
* sentry: getSentrySink(), // That's it!
* },
* loggers: [
* { category: [], sinks: ["sentry"], lowestLevel: "error" },
* ],
* });
* ```
*
* @example With an application-provided Sentry namespace
* ```typescript
* import { configure } from "@logtape/logtape";
* import { getSentrySink } from "@logtape/sentry";
* import * as Sentry from "@sentry/nextjs";
*
* Sentry.init({ dsn: process.env.SENTRY_DSN });
*
* await configure({
* sinks: {
* sentry: getSentrySink({ sentry: Sentry }),
* },
* loggers: [
* { category: [], sinks: ["sentry"], lowestLevel: "error" },
* ],
* });
* ```
*
* @example With options
* ```typescript
* import * as Sentry from "@sentry/node";
* Sentry.init({ dsn: process.env.SENTRY_DSN });
*
* await configure({
* sinks: {
* sentry: getSentrySink({
* breadcrumbs: true,
* }),
* },
* loggers: [
* { category: [], sinks: ["sentry"], lowestLevel: "info" },
* ],
* });
* ```
*
* @example Edge functions - must flush before termination
* ```typescript
* // Cloudflare Workers
* export default {
* async fetch(request, env, ctx) {
* logger.error("Something happened");
* ctx.waitUntil(Sentry.flush(2000)); // Don't block response
* return new Response("OK");
* }
* };
* ```
*
* @example Legacy usage (v1.1.x - deprecated)
* ```typescript
* import { getClient } from "@sentry/node";
* const client = getClient();
* getSentrySink(client); // Still works but shows deprecation warning
* ```
*
* @since 1.0.0
*/
function getSentrySink(optionsOrClient) {
let legacyClient;
let options = {};
if (optionsOrClient == null) {} else if (typeof optionsOrClient === "object" && "captureMessage" in optionsOrClient && typeof optionsOrClient.captureMessage === "function") {
getLogger([
"logtape",
"meta",
"sentry"
]).warn("Passing a client directly is deprecated. Use getSentrySink({ sentry: Sentry }) instead.");
legacyClient = optionsOrClient;
} else if (typeof optionsOrClient === "object") options = optionsOrClient;
else throw new Error(`[@logtape/sentry] Invalid parameter (type: ${typeof optionsOrClient}).\n\nExpected one of:
getSentrySink() // Recommended
getSentrySink({ options }) // With options
getSentrySink({ sentry }) // With a Sentry SDK namespace
getSentrySink(client) // Deprecated (v1.1.x compat)
`);
const sentry = options.sentry ?? SentryCore;
const captureMessage = legacyClient ? (msg, ctx) => legacyClient.captureMessage(String(msg), ctx) : sentry.captureMessage;
const captureException = legacyClient ? (exception, hint) => legacyClient.captureException(exception, hint) : sentry.captureException;
return (record) => {
try {
const { category } = record;
if (category[0] === "logtape" && category[1] === "meta" && category[2] === "sentry") return;
const transformed = options.beforeSend ? options.beforeSend(record) : record;
if (transformed == null) return;
const paramMessage = getParameterizedString(transformed);
const message = paramMessage.toString();
const eventLevel = mapLevelForEvents(transformed.level);
const attributes = {
...transformed.properties,
"sentry.origin": "auto.logging.logtape",
category: transformed.category.join("."),
timestamp: transformed.timestamp
};
const activeSpan = sentry.getActiveSpan();
if (activeSpan) {
const spanCtx = activeSpan.spanContext();
attributes.trace_id = spanCtx.traceId;
attributes.span_id = spanCtx.spanId;
if ("parentSpanId" in spanCtx) attributes.parent_span_id = spanCtx.parentSpanId;
}
const client = sentry.getClient();
if (client && shouldSendToLogs(transformed, options.logs)) {
const { enableLogs, _experiments } = client.getOptions();
const loggingEnabled = enableLogs ?? _experiments?.enableLogs;
const sentryLogger = sentry.logger;
if (loggingEnabled && sentryLogger != null) {
const logLevel = mapLevelForLogs(transformed.level);
const logFn = sentryLogger[logLevel];
if (typeof logFn === "function") logFn(paramMessage, attributes);
}
}
const isErrorLevel = compareLogLevel(transformed.level, "error") >= 0;
const errorProperty = getErrorProperty(transformed.properties, options.errorPropertyNames ?? defaultErrorPropertyNames);
if (isErrorLevel && errorProperty != null) {
const [property, error] = errorProperty;
const rest = { ...attributes };
delete rest[property];
captureException(error, {
level: eventLevel,
extra: {
message,
...rest
}
});
} else if (isErrorLevel) captureMessage(paramMessage, {
level: eventLevel,
extra: attributes
});
else if (shouldAddBreadcrumb(transformed, options)) {
const isolationScope = sentry.getIsolationScope();
isolationScope?.addBreadcrumb({
category: transformed.category.join("."),
level: eventLevel,
message,
timestamp: transformed.timestamp / 1e3,
data: attributes
});
}
} catch (error) {
getLogger([
"logtape",
"meta",
"sentry"
]).error("Failed to send log events to Sentry", { error });
}
};
}
function shouldSendToLogs(record, options) {
return options?.level == null || compareLogLevel(record.level, options.level) >= 0;
}
function shouldAddBreadcrumb(record, options) {
const breadcrumbOptions = options.breadcrumbs;
if (breadcrumbOptions === false) return false;
if (breadcrumbOptions === true) return true;
if (breadcrumbOptions == null) return options.enableBreadcrumbs === true;
if (breadcrumbOptions.level != null && compareLogLevel(record.level, breadcrumbOptions.level) < 0) return false;
return breadcrumbOptions.maxLevel == null || compareLogLevel(record.level, breadcrumbOptions.maxLevel) <= 0;
}
//#endregion
export { getSentrySink };
//# sourceMappingURL=mod.js.map