From ff628666ba243c5ca0d4b1c18a2c3ba3a6cc80ee Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Mar 2025 17:48:06 -0400 Subject: [PATCH 1/3] feat(node): Add logging public APIs to Node SDKs --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/core/src/client.ts | 13 + packages/core/src/logs/index.ts | 12 +- packages/core/src/server-runtime-client.ts | 68 +++++ packages/core/src/types-hoist/log.ts | 2 +- packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 4 + packages/node/src/log.ts | 268 ++++++++++++++++++ packages/node/test/log.test.ts | 243 ++++++++++++++++ packages/remix/src/server/index.ts | 1 + packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 14 files changed, 610 insertions(+), 7 deletions(-) create mode 100644 packages/node/src/log.ts create mode 100644 packages/node/test/log.test.ts diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 6f9647d0134e..6467affd841c 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -127,6 +127,7 @@ export { withScope, zodErrorsIntegration, profiler, + logger, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 51848530712b..473b87c793d9 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -113,6 +113,7 @@ export { profiler, amqplibIntegration, vercelAIIntegration, + logger, } from '@sentry/node'; export { diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 52b3d9fa4c42..c2aff0f1ca52 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -132,6 +132,7 @@ export { profiler, amqplibIntegration, vercelAIIntegration, + logger, } from '@sentry/node'; export { diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index af13ea2d691f..07b5224e5928 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -16,6 +16,7 @@ import type { FeedbackEvent, FetchBreadcrumbHint, Integration, + Log, MonitorConfig, Outcome, ParameterizedString, @@ -621,6 +622,13 @@ export abstract class Client { */ public on(hook: 'close', callback: () => void): () => void; + /** + * A hook that is called before a log is captured + * + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'beforeCaptureLog', callback: (log: Log) => void): () => void; + /** * Register a hook on this client. */ @@ -768,6 +776,11 @@ export abstract class Client { */ public emit(hook: 'close'): void; + /** + * Emit a hook event for client before capturing a log + */ + public emit(hook: 'beforeCaptureLog', log: Log): void; + /** * Emit a hook that was previously registered via `on()`. */ diff --git a/packages/core/src/logs/index.ts b/packages/core/src/logs/index.ts index 6c6c9b580ee9..56de1b0bdc15 100644 --- a/packages/core/src/logs/index.ts +++ b/packages/core/src/logs/index.ts @@ -110,14 +110,14 @@ export function _INTERNAL_captureLog(log: Log, client = getClient(), scope = get const logBuffer = CLIENT_TO_LOG_BUFFER_MAP.get(client); if (logBuffer === undefined) { CLIENT_TO_LOG_BUFFER_MAP.set(client, [serializedLog]); - // Every time we initialize a new log buffer, we start a new interval to flush the buffer - return; + } else { + logBuffer.push(serializedLog); + if (logBuffer.length > MAX_LOG_BUFFER_SIZE) { + _INTERNAL_flushLogsBuffer(client, logBuffer); + } } - logBuffer.push(serializedLog); - if (logBuffer.length > MAX_LOG_BUFFER_SIZE) { - _INTERNAL_flushLogsBuffer(client, logBuffer); - } + client.emit('beforeCaptureLog', log); } /** diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 61db792b901e..9f160d7ee25e 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -4,8 +4,10 @@ import type { ClientOptions, Event, EventHint, + Log, MonitorConfig, ParameterizedString, + Primitive, SerializedCheckIn, SeverityLevel, } from './types-hoist'; @@ -20,6 +22,8 @@ import { eventFromMessage, eventFromUnknownInput } from './utils-hoist/eventbuil import { logger } from './utils-hoist/logger'; import { uuid4 } from './utils-hoist/misc'; import { resolvedSyncPromise } from './utils-hoist/syncpromise'; +import { _INTERNAL_flushLogsBuffer } from './logs'; +import { isPrimitive } from './utils-hoist'; export interface ServerRuntimeClientOptions extends ClientOptions { platform?: string; @@ -33,6 +37,8 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends Client { + private _logWeight: number; + /** * Creates a new Edge SDK instance. * @param options Configuration options for this SDK. @@ -42,6 +48,26 @@ export class ServerRuntimeClient< registerSpanErrorInstrumentation(); super(options); + + this._logWeight = 0; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const client = this; + this.on('flush', () => { + _INTERNAL_flushLogsBuffer(client); + }); + + this.on('beforeCaptureLog', log => { + client._logWeight += estimateLogSizeInBytes(log); + + // We flush the logs buffer if it exceeds 0.8 MB + // The log weight is a rough estimate, so we flush way before + // the payload gets too big. + if (client._logWeight > 800_000) { + _INTERNAL_flushLogsBuffer(client); + client._logWeight = 0; + } + }); } /** @@ -196,3 +222,45 @@ function setCurrentRequestSessionErroredOrCrashed(eventHint?: EventHint): void { } } } + +/** + * Estimate the size of a log in bytes. + * + * @param log - The log to estimate the size of. + * @returns The estimated size of the log in bytes. + */ +function estimateLogSizeInBytes(log: Log): number { + let weight = 0; + + // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. + if (log.message) { + weight += log.message.length * 2; + } + + if (log.attributes) { + Object.values(log.attributes).forEach(value => { + if (Array.isArray(value)) { + weight += value.length * estimatePrimitiveSizeInBytes(value[0]); + } else if (isPrimitive(value)) { + weight += estimatePrimitiveSizeInBytes(value); + } else { + // For objects values, we estimate the size of the object as 100 bytes + weight += 100; + } + }); + } + + return weight; +} + +function estimatePrimitiveSizeInBytes(value: Primitive): number { + if (typeof value === 'string') { + return value.length * 2; + } else if (typeof value === 'number') { + return 8; + } else if (typeof value === 'boolean') { + return 4; + } + + return 0; +} diff --git a/packages/core/src/types-hoist/log.ts b/packages/core/src/types-hoist/log.ts index a313b493306c..45172c44adc0 100644 --- a/packages/core/src/types-hoist/log.ts +++ b/packages/core/src/types-hoist/log.ts @@ -41,7 +41,7 @@ export interface Log { /** * Arbitrary structured data that stores information about the log - e.g., userId: 100. */ - attributes?: Record>; + attributes?: Record; /** * The severity number - generally higher severity are levels like 'error' and lower are levels like 'debug' diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 9505ef6dd248..83a18f62c6df 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -113,6 +113,7 @@ export { amqplibIntegration, childProcessIntegration, vercelAIIntegration, + logger, } from '@sentry/node'; export { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index bdc8d6405217..decfbd578c68 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -150,3 +150,7 @@ export type { User, Span, } from '@sentry/core'; + +import * as logger from './log'; + +export { logger }; diff --git a/packages/node/src/log.ts b/packages/node/src/log.ts new file mode 100644 index 000000000000..e6644e6cf739 --- /dev/null +++ b/packages/node/src/log.ts @@ -0,0 +1,268 @@ +import { format } from 'node:util'; + +import type { LogSeverityLevel, Log } from '@sentry/core'; +import { _INTERNAL_captureLog } from '@sentry/core'; + +/** + * Capture a log with the given level. + * + * @param level - The level of the log. + * @param message - The message to log. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + */ +function captureLog(level: LogSeverityLevel, message: string, attributes?: Log['attributes']): void { + _INTERNAL_captureLog({ level, message, attributes }); +} + +/** + * @summary Capture a log with the `trace` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.trace('Hello world', { userId: 100 }); + * ``` + */ +export function trace(message: string, attributes?: Log['attributes']): void { + captureLog('trace', message, attributes); +} + +/** + * @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.debug('Hello world', { userId: 100 }); + * ``` + */ +export function debug(message: string, attributes?: Log['attributes']): void { + captureLog('debug', message, attributes); +} + +/** + * @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.info('Hello world', { userId: 100 }); + * ``` + */ +export function info(message: string, attributes?: Log['attributes']): void { + captureLog('info', message, attributes); +} + +/** + * @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.warn('Hello world', { userId: 100 }); + * ``` + */ +export function warn(message: string, attributes?: Log['attributes']): void { + captureLog('warn', message, attributes); +} + +/** + * @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.error('Hello world', { userId: 100 }); + * ``` + */ +export function error(message: string, attributes?: Log['attributes']): void { + captureLog('error', message, attributes); +} + +/** + * @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.fatal('Hello world', { userId: 100 }); + * ``` + */ +export function fatal(message: string, attributes?: Log['attributes']): void { + captureLog('fatal', message, attributes); +} + +/** + * @summary Capture a log with the `critical` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.critical('Hello world', { userId: 100 }); + * ``` + */ +export function critical(message: string, attributes?: Log['attributes']): void { + captureLog('critical', message, attributes); +} + +/** + * Capture a formatted log with the given level. + * + * @param level - The level of the log. + * @param message - The message template. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + */ +function captureLogFmt( + level: LogSeverityLevel, + messageTemplate: string, + params: Array, + logAttributes: Log['attributes'] = {}, +): void { + const attributes = { ...logAttributes }; + attributes['sentry.message.template'] = messageTemplate; + params.forEach((param, index) => { + attributes[`sentry.message.param.${index}`] = param; + }); + const message = format(messageTemplate, ...params); + _INTERNAL_captureLog({ level, message, attributes }); +} + +/** + * @summary Capture a formatted log with the `trace` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param params - The parameters to interpolate into the message. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.traceFmt('Hello world %s', ['foo'], { userId: 100 }); + * ``` + */ +export function traceFmt(message: string, params: Array, attributes: Log['attributes'] = {}): void { + captureLogFmt('trace', message, params, attributes); +} + +/** + * @summary Capture a formatted log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param params - The parameters to interpolate into the message. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.debugFmt('Hello world %s', ['foo'], { userId: 100 }); + * ``` + */ +export function debugFmt(message: string, params: Array, attributes: Log['attributes'] = {}): void { + captureLogFmt('debug', message, params, attributes); +} + +/** + * @summary Capture a formatted log with the `info` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param params - The parameters to interpolate into the message. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.infoFmt('Hello world %s', ['foo'], { userId: 100 }); + * ``` + */ +export function infoFmt(message: string, params: Array, attributes?: Log['attributes']): void { + captureLogFmt('info', message, params, attributes); +} + +/** + * @summary Capture a formatted log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param params - The parameters to interpolate into the message. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.warnFmt('Hello world %s', ['foo'], { userId: 100 }); + * ``` + */ +export function warnFmt(message: string, params: Array, attributes?: Log['attributes']): void { + captureLogFmt('warn', message, params, attributes); +} + +/** + * @summary Capture a formatted log with the `error` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param params - The parameters to interpolate into the message. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.errorFmt('Hello world %s', ['foo'], { userId: 100 }); + * ``` + */ +export function errorFmt(message: string, params: Array, attributes?: Log['attributes']): void { + captureLogFmt('error', message, params, attributes); +} + +/** + * @summary Capture a formatted log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param params - The parameters to interpolate into the message. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.fatalFmt('Hello world %s', ['foo'], { userId: 100 }); + * ``` + */ +export function fatalFmt(message: string, params: Array, attributes?: Log['attributes']): void { + captureLogFmt('fatal', message, params, attributes); +} + +/** + * @summary Capture a formatted log with the `critical` level. Requires `_experiments.enableLogs` to be enabled. + * + * @param message - The message to log. + * @param params - The parameters to interpolate into the message. + * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * + * @example + * + * ``` + * Sentry.logger.criticalFmt('Hello world %s', ['foo'], { userId: 100 }); + * ``` + */ +export function criticalFmt(message: string, params: Array, attributes?: Log['attributes']): void { + captureLogFmt('critical', message, params, attributes); +} diff --git a/packages/node/test/log.test.ts b/packages/node/test/log.test.ts new file mode 100644 index 000000000000..1f4f44986486 --- /dev/null +++ b/packages/node/test/log.test.ts @@ -0,0 +1,243 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as sentryCore from '@sentry/core'; +import * as nodeLogger from '../src/log'; + +// Mock the core functions +vi.mock('@sentry/core', async () => { + const actual = await vi.importActual('@sentry/core'); + return { + ...actual, + _INTERNAL_captureLog: vi.fn(), + }; +}); + +describe('Node Logger', () => { + // Use the mocked function + const mockCaptureLog = vi.mocked(sentryCore._INTERNAL_captureLog); + + beforeEach(() => { + // Reset mocks + mockCaptureLog.mockClear(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe('Basic logging methods', () => { + it('should export all log methods', () => { + expect(nodeLogger.trace).toBeTypeOf('function'); + expect(nodeLogger.debug).toBeTypeOf('function'); + expect(nodeLogger.info).toBeTypeOf('function'); + expect(nodeLogger.warn).toBeTypeOf('function'); + expect(nodeLogger.error).toBeTypeOf('function'); + expect(nodeLogger.fatal).toBeTypeOf('function'); + expect(nodeLogger.critical).toBeTypeOf('function'); + }); + + it('should call _INTERNAL_captureLog with trace level', () => { + nodeLogger.trace('Test trace message', { key: 'value' }); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'trace', + message: 'Test trace message', + attributes: { key: 'value' }, + }); + }); + + it('should call _INTERNAL_captureLog with debug level', () => { + nodeLogger.debug('Test debug message', { key: 'value' }); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'debug', + message: 'Test debug message', + attributes: { key: 'value' }, + }); + }); + + it('should call _INTERNAL_captureLog with info level', () => { + nodeLogger.info('Test info message', { key: 'value' }); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'info', + message: 'Test info message', + attributes: { key: 'value' }, + }); + }); + + it('should call _INTERNAL_captureLog with warn level', () => { + nodeLogger.warn('Test warn message', { key: 'value' }); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'warn', + message: 'Test warn message', + attributes: { key: 'value' }, + }); + }); + + it('should call _INTERNAL_captureLog with error level', () => { + nodeLogger.error('Test error message', { key: 'value' }); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'error', + message: 'Test error message', + attributes: { key: 'value' }, + }); + }); + + it('should call _INTERNAL_captureLog with fatal level', () => { + nodeLogger.fatal('Test fatal message', { key: 'value' }); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'fatal', + message: 'Test fatal message', + attributes: { key: 'value' }, + }); + }); + + it('should call _INTERNAL_captureLog with critical level', () => { + nodeLogger.critical('Test critical message', { key: 'value' }); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'critical', + message: 'Test critical message', + attributes: { key: 'value' }, + }); + }); + }); + + describe('Formatted logging methods', () => { + it('should export all formatted log methods', () => { + expect(nodeLogger.traceFmt).toBeTypeOf('function'); + expect(nodeLogger.debugFmt).toBeTypeOf('function'); + expect(nodeLogger.infoFmt).toBeTypeOf('function'); + expect(nodeLogger.warnFmt).toBeTypeOf('function'); + expect(nodeLogger.errorFmt).toBeTypeOf('function'); + expect(nodeLogger.fatalFmt).toBeTypeOf('function'); + expect(nodeLogger.criticalFmt).toBeTypeOf('function'); + }); + + it('should format the message with trace level', () => { + nodeLogger.traceFmt('Hello %s', ['world'], { key: 'value' }); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'trace', + message: 'Hello world', + attributes: { + key: 'value', + 'sentry.message.template': 'Hello %s', + 'sentry.message.param.0': 'world', + }, + }); + }); + + it('should format the message with debug level', () => { + nodeLogger.debugFmt('Count: %d', [42], { key: 'value' }); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'debug', + message: 'Count: 42', + attributes: { + key: 'value', + 'sentry.message.template': 'Count: %d', + 'sentry.message.param.0': 42, + }, + }); + }); + + it('should format the message with info level', () => { + nodeLogger.infoFmt('User %s logged in from %s', ['John', 'Paris'], { userId: 123 }); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'info', + message: 'User John logged in from Paris', + attributes: { + userId: 123, + 'sentry.message.template': 'User %s logged in from %s', + 'sentry.message.param.0': 'John', + 'sentry.message.param.1': 'Paris', + }, + }); + }); + + it('should format the message with warn level', () => { + nodeLogger.warnFmt('Usage at %d%%', [95], { resource: 'CPU' }); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'warn', + message: 'Usage at 95%', + attributes: { + resource: 'CPU', + 'sentry.message.template': 'Usage at %d%%', + 'sentry.message.param.0': 95, + }, + }); + }); + + it('should format the message with error level', () => { + nodeLogger.errorFmt('Failed to process %s: %s', ['payment', 'timeout'], { orderId: 'ORD-123' }); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'error', + message: 'Failed to process payment: timeout', + attributes: { + orderId: 'ORD-123', + 'sentry.message.template': 'Failed to process %s: %s', + 'sentry.message.param.0': 'payment', + 'sentry.message.param.1': 'timeout', + }, + }); + }); + + it('should format the message with fatal level', () => { + nodeLogger.fatalFmt('System crash in module %s', ['auth'], { shutdown: true }); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'fatal', + message: 'System crash in module auth', + attributes: { + shutdown: true, + 'sentry.message.template': 'System crash in module %s', + 'sentry.message.param.0': 'auth', + }, + }); + }); + + it('should format the message with critical level', () => { + nodeLogger.criticalFmt('Database %s is down for %d minutes', ['customers', 30], { impact: 'high' }); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'critical', + message: 'Database customers is down for 30 minutes', + attributes: { + impact: 'high', + 'sentry.message.template': 'Database %s is down for %d minutes', + 'sentry.message.param.0': 'customers', + 'sentry.message.param.1': 30, + }, + }); + }); + + it('should handle complex formatting with multiple types', () => { + const obj = { name: 'test' }; + nodeLogger.infoFmt('Values: %s, %d, %j', ['string', 42, obj]); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'info', + message: `Values: string, 42, ${JSON.stringify(obj)}`, + attributes: { + 'sentry.message.template': 'Values: %s, %d, %j', + 'sentry.message.param.0': 'string', + 'sentry.message.param.1': 42, + 'sentry.message.param.2': obj, + }, + }); + }); + + it('should use empty object as default for attributes', () => { + nodeLogger.infoFmt('Hello %s', ['world']); + + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'info', + message: 'Hello world', + attributes: { + 'sentry.message.template': 'Hello %s', + 'sentry.message.param.0': 'world', + }, + }); + }); + }); +}); diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 4160a871d165..7809455ce3fa 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -112,6 +112,7 @@ export { withMonitor, withScope, zodErrorsIntegration, + logger, } from '@sentry/node'; // Keeping the `*` exports for backwards compatibility and types diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 948c3c746d0c..a36b7fbfaad1 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -115,6 +115,7 @@ export { withMonitor, withScope, zodErrorsIntegration, + logger, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index f8844c1e264d..9df3ddd688c8 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -117,6 +117,7 @@ export { withMonitor, withScope, zodErrorsIntegration, + logger, } from '@sentry/node'; // We can still leave this for the carrier init and type exports From 3447bb6d1d9de049dca13341e7b04ddd5348bb74 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 24 Mar 2025 12:25:08 -0400 Subject: [PATCH 2/3] unify capture log methods --- packages/node/src/log.ts | 267 ++++++++++++++------------------- packages/node/test/log.test.ts | 139 ++--------------- 2 files changed, 124 insertions(+), 282 deletions(-) diff --git a/packages/node/src/log.ts b/packages/node/src/log.ts index e6644e6cf739..9bad4895ceb6 100644 --- a/packages/node/src/log.ts +++ b/packages/node/src/log.ts @@ -3,6 +3,10 @@ import { format } from 'node:util'; import type { LogSeverityLevel, Log } from '@sentry/core'; import { _INTERNAL_captureLog } from '@sentry/core'; +type CaptureLogArgs = + | [message: string, attributes?: Log['attributes']] + | [messageTemplate: string, messageParams: Array, attributes?: Log['attributes']]; + /** * Capture a log with the given level. * @@ -10,259 +14,210 @@ import { _INTERNAL_captureLog } from '@sentry/core'; * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. */ -function captureLog(level: LogSeverityLevel, message: string, attributes?: Log['attributes']): void { - _INTERNAL_captureLog({ level, message, attributes }); +function captureLog(level: LogSeverityLevel, ...args: CaptureLogArgs): void { + const [messageOrMessageTemplate, paramsOrAttributes, maybeAttributes] = args; + if (Array.isArray(paramsOrAttributes)) { + const attributes = { ...maybeAttributes }; + attributes['sentry.message.template'] = messageOrMessageTemplate; + paramsOrAttributes.forEach((param, index) => { + attributes[`sentry.message.param.${index}`] = param; + }); + const message = format(messageOrMessageTemplate, ...paramsOrAttributes); + _INTERNAL_captureLog({ level, message, attributes }); + } else { + _INTERNAL_captureLog({ level, message: messageOrMessageTemplate, attributes: paramsOrAttributes }); + } } /** * @summary Capture a log with the `trace` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * You can either pass a message and attributes or a message template, params and attributes. * * @example * * ``` - * Sentry.logger.trace('Hello world', { userId: 100 }); + * Sentry.logger.trace('Starting database connection', { + * database: 'users', + * connectionId: 'conn_123' + * }); * ``` - */ -export function trace(message: string, attributes?: Log['attributes']): void { - captureLog('trace', message, attributes); -} - -/** - * @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. - * - * @param message - The message to log. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. * - * @example + * @example With template strings * * ``` - * Sentry.logger.debug('Hello world', { userId: 100 }); + * Sentry.logger.trace('Database connection %s established for %s', + * ['successful', 'users'], + * { connectionId: 'conn_123' } + * ); * ``` */ -export function debug(message: string, attributes?: Log['attributes']): void { - captureLog('debug', message, attributes); +export function trace(...args: CaptureLogArgs): void { + captureLog('trace', ...args); } /** - * @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * You can either pass a message and attributes or a message template, params and attributes. * * @example * * ``` - * Sentry.logger.info('Hello world', { userId: 100 }); + * Sentry.logger.debug('Cache miss for user profile', { + * userId: 'user_123', + * cacheKey: 'profile:user_123' + * }); * ``` - */ -export function info(message: string, attributes?: Log['attributes']): void { - captureLog('info', message, attributes); -} - -/** - * @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. - * - * @example + * @example With template strings * * ``` - * Sentry.logger.warn('Hello world', { userId: 100 }); + * Sentry.logger.debug('Cache %s for %s: %s', + * ['miss', 'user profile', 'key not found'], + * { userId: 'user_123' } + * ); * ``` */ -export function warn(message: string, attributes?: Log['attributes']): void { - captureLog('warn', message, attributes); +export function debug(...args: CaptureLogArgs): void { + captureLog('debug', ...args); } /** - * @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * You can either pass a message and attributes or a message template, params and attributes. * * @example * * ``` - * Sentry.logger.error('Hello world', { userId: 100 }); + * Sentry.logger.info('User profile updated', { + * userId: 'user_123', + * updatedFields: ['email', 'preferences'] + * }); * ``` - */ -export function error(message: string, attributes?: Log['attributes']): void { - captureLog('error', message, attributes); -} - -/** - * @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. - * - * @example + * @example With template strings * * ``` - * Sentry.logger.fatal('Hello world', { userId: 100 }); + * Sentry.logger.info('User %s updated their %s', + * ['John Doe', 'profile settings'], + * { userId: 'user_123' } + * ); * ``` */ -export function fatal(message: string, attributes?: Log['attributes']): void { - captureLog('fatal', message, attributes); +export function info(...args: CaptureLogArgs): void { + captureLog('info', ...args); } /** - * @summary Capture a log with the `critical` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * You can either pass a message and attributes or a message template, params and attributes. * * @example * * ``` - * Sentry.logger.critical('Hello world', { userId: 100 }); + * Sentry.logger.warn('Rate limit approaching', { + * endpoint: '/api/users', + * currentRate: '95/100', + * resetTime: '2024-03-20T10:00:00Z' + * }); * ``` - */ -export function critical(message: string, attributes?: Log['attributes']): void { - captureLog('critical', message, attributes); -} - -/** - * Capture a formatted log with the given level. - * - * @param level - The level of the log. - * @param message - The message template. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. - */ -function captureLogFmt( - level: LogSeverityLevel, - messageTemplate: string, - params: Array, - logAttributes: Log['attributes'] = {}, -): void { - const attributes = { ...logAttributes }; - attributes['sentry.message.template'] = messageTemplate; - params.forEach((param, index) => { - attributes[`sentry.message.param.${index}`] = param; - }); - const message = format(messageTemplate, ...params); - _INTERNAL_captureLog({ level, message, attributes }); -} - -/** - * @summary Capture a formatted log with the `trace` level. Requires `_experiments.enableLogs` to be enabled. - * - * @param message - The message to log. - * @param params - The parameters to interpolate into the message. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. * - * @example + * @example With template strings * * ``` - * Sentry.logger.traceFmt('Hello world %s', ['foo'], { userId: 100 }); + * Sentry.logger.warn('Rate limit %s for %s: %s', + * ['approaching', '/api/users', '95/100 requests'], + * { resetTime: '2024-03-20T10:00:00Z' } + * ); * ``` */ -export function traceFmt(message: string, params: Array, attributes: Log['attributes'] = {}): void { - captureLogFmt('trace', message, params, attributes); +export function warn(...args: CaptureLogArgs): void { + captureLog('warn', ...args); } /** - * @summary Capture a formatted log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param params - The parameters to interpolate into the message. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * You can either pass a message and attributes or a message template, params and attributes. * * @example * * ``` - * Sentry.logger.debugFmt('Hello world %s', ['foo'], { userId: 100 }); + * Sentry.logger.error('Failed to process payment', { + * orderId: 'order_123', + * errorCode: 'PAYMENT_FAILED', + * amount: 99.99 + * }); * ``` - */ -export function debugFmt(message: string, params: Array, attributes: Log['attributes'] = {}): void { - captureLogFmt('debug', message, params, attributes); -} - -/** - * @summary Capture a formatted log with the `info` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param params - The parameters to interpolate into the message. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. - * - * @example + * @example With template strings * * ``` - * Sentry.logger.infoFmt('Hello world %s', ['foo'], { userId: 100 }); + * Sentry.logger.error('Payment processing failed for order %s: %s', + * ['order_123', 'insufficient funds'], + * { amount: 99.99 } + * ); * ``` */ -export function infoFmt(message: string, params: Array, attributes?: Log['attributes']): void { - captureLogFmt('info', message, params, attributes); +export function error(...args: CaptureLogArgs): void { + captureLog('error', ...args); } /** - * @summary Capture a formatted log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param params - The parameters to interpolate into the message. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * You can either pass a message and attributes or a message template, params and attributes. * * @example * * ``` - * Sentry.logger.warnFmt('Hello world %s', ['foo'], { userId: 100 }); + * Sentry.logger.fatal('Database connection pool exhausted', { + * database: 'users', + * activeConnections: 100, + * maxConnections: 100 + * }); * ``` - */ -export function warnFmt(message: string, params: Array, attributes?: Log['attributes']): void { - captureLogFmt('warn', message, params, attributes); -} - -/** - * @summary Capture a formatted log with the `error` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param params - The parameters to interpolate into the message. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. - * - * @example + * @example With template strings * * ``` - * Sentry.logger.errorFmt('Hello world %s', ['foo'], { userId: 100 }); + * Sentry.logger.fatal('Database %s: %s connections active', + * ['connection pool exhausted', '100/100'], + * { database: 'users' } + * ); * ``` */ -export function errorFmt(message: string, params: Array, attributes?: Log['attributes']): void { - captureLogFmt('error', message, params, attributes); +export function fatal(...args: CaptureLogArgs): void { + captureLog('fatal', ...args); } /** - * @summary Capture a formatted log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `critical` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param params - The parameters to interpolate into the message. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. + * You can either pass a message and attributes or a message template, params and attributes. * * @example * * ``` - * Sentry.logger.fatalFmt('Hello world %s', ['foo'], { userId: 100 }); + * Sentry.logger.critical('Service health check failed', { + * service: 'payment-gateway', + * status: 'DOWN', + * lastHealthy: '2024-03-20T09:55:00Z' + * }); * ``` - */ -export function fatalFmt(message: string, params: Array, attributes?: Log['attributes']): void { - captureLogFmt('fatal', message, params, attributes); -} - -/** - * @summary Capture a formatted log with the `critical` level. Requires `_experiments.enableLogs` to be enabled. * - * @param message - The message to log. - * @param params - The parameters to interpolate into the message. - * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. - * - * @example + * @example With template strings * * ``` - * Sentry.logger.criticalFmt('Hello world %s', ['foo'], { userId: 100 }); + * Sentry.logger.critical('Service %s is %s', + * ['payment-gateway', 'DOWN'], + * { lastHealthy: '2024-03-20T09:55:00Z' } + * ); * ``` */ -export function criticalFmt(message: string, params: Array, attributes?: Log['attributes']): void { - captureLogFmt('critical', message, params, attributes); +export function critical(...args: CaptureLogArgs): void { + captureLog('critical', ...args); } diff --git a/packages/node/test/log.test.ts b/packages/node/test/log.test.ts index 1f4f44986486..4064d7c1f3f1 100644 --- a/packages/node/test/log.test.ts +++ b/packages/node/test/log.test.ts @@ -99,143 +99,30 @@ describe('Node Logger', () => { }); }); - describe('Formatted logging methods', () => { - it('should export all formatted log methods', () => { - expect(nodeLogger.traceFmt).toBeTypeOf('function'); - expect(nodeLogger.debugFmt).toBeTypeOf('function'); - expect(nodeLogger.infoFmt).toBeTypeOf('function'); - expect(nodeLogger.warnFmt).toBeTypeOf('function'); - expect(nodeLogger.errorFmt).toBeTypeOf('function'); - expect(nodeLogger.fatalFmt).toBeTypeOf('function'); - expect(nodeLogger.criticalFmt).toBeTypeOf('function'); - }); - - it('should format the message with trace level', () => { - nodeLogger.traceFmt('Hello %s', ['world'], { key: 'value' }); - - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'trace', - message: 'Hello world', - attributes: { - key: 'value', - 'sentry.message.template': 'Hello %s', - 'sentry.message.param.0': 'world', - }, - }); - }); - - it('should format the message with debug level', () => { - nodeLogger.debugFmt('Count: %d', [42], { key: 'value' }); - - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'debug', - message: 'Count: 42', - attributes: { - key: 'value', - 'sentry.message.template': 'Count: %d', - 'sentry.message.param.0': 42, - }, - }); - }); - - it('should format the message with info level', () => { - nodeLogger.infoFmt('User %s logged in from %s', ['John', 'Paris'], { userId: 123 }); - + describe('Template string logging', () => { + it('should handle template strings with parameters', () => { + nodeLogger.info('Hello %s, your balance is %d', ['John', 100], { userId: 123 }); expect(mockCaptureLog).toHaveBeenCalledWith({ level: 'info', - message: 'User John logged in from Paris', + message: 'Hello John, your balance is 100', attributes: { userId: 123, - 'sentry.message.template': 'User %s logged in from %s', + 'sentry.message.template': 'Hello %s, your balance is %d', 'sentry.message.param.0': 'John', - 'sentry.message.param.1': 'Paris', - }, - }); - }); - - it('should format the message with warn level', () => { - nodeLogger.warnFmt('Usage at %d%%', [95], { resource: 'CPU' }); - - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'warn', - message: 'Usage at 95%', - attributes: { - resource: 'CPU', - 'sentry.message.template': 'Usage at %d%%', - 'sentry.message.param.0': 95, - }, - }); - }); - - it('should format the message with error level', () => { - nodeLogger.errorFmt('Failed to process %s: %s', ['payment', 'timeout'], { orderId: 'ORD-123' }); - - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'error', - message: 'Failed to process payment: timeout', - attributes: { - orderId: 'ORD-123', - 'sentry.message.template': 'Failed to process %s: %s', - 'sentry.message.param.0': 'payment', - 'sentry.message.param.1': 'timeout', - }, - }); - }); - - it('should format the message with fatal level', () => { - nodeLogger.fatalFmt('System crash in module %s', ['auth'], { shutdown: true }); - - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'fatal', - message: 'System crash in module auth', - attributes: { - shutdown: true, - 'sentry.message.template': 'System crash in module %s', - 'sentry.message.param.0': 'auth', - }, - }); - }); - - it('should format the message with critical level', () => { - nodeLogger.criticalFmt('Database %s is down for %d minutes', ['customers', 30], { impact: 'high' }); - - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'critical', - message: 'Database customers is down for 30 minutes', - attributes: { - impact: 'high', - 'sentry.message.template': 'Database %s is down for %d minutes', - 'sentry.message.param.0': 'customers', - 'sentry.message.param.1': 30, - }, - }); - }); - - it('should handle complex formatting with multiple types', () => { - const obj = { name: 'test' }; - nodeLogger.infoFmt('Values: %s, %d, %j', ['string', 42, obj]); - - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'info', - message: `Values: string, 42, ${JSON.stringify(obj)}`, - attributes: { - 'sentry.message.template': 'Values: %s, %d, %j', - 'sentry.message.param.0': 'string', - 'sentry.message.param.1': 42, - 'sentry.message.param.2': obj, + 'sentry.message.param.1': 100, }, }); }); - it('should use empty object as default for attributes', () => { - nodeLogger.infoFmt('Hello %s', ['world']); - + it('should handle template strings without additional attributes', () => { + nodeLogger.debug('User %s logged in from %s', ['Alice', 'mobile']); expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'info', - message: 'Hello world', + level: 'debug', + message: 'User Alice logged in from mobile', attributes: { - 'sentry.message.template': 'Hello %s', - 'sentry.message.param.0': 'world', + 'sentry.message.template': 'User %s logged in from %s', + 'sentry.message.param.0': 'Alice', + 'sentry.message.param.1': 'mobile', }, }); }); From 3c573d578bc6970a24521d8c3a206abdec42b6da Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 24 Mar 2025 12:38:25 -0400 Subject: [PATCH 3/3] fix type exports in metaframeworks --- packages/astro/src/index.types.ts | 3 +++ packages/nextjs/src/index.types.ts | 2 ++ packages/nuxt/src/index.types.ts | 3 +++ packages/react-router/src/index.types.ts | 2 ++ packages/remix/src/index.types.ts | 2 ++ packages/solidstart/src/index.types.ts | 2 ++ packages/sveltekit/src/index.types.ts | 2 ++ packages/tanstackstart-react/src/index.types.ts | 2 ++ 8 files changed, 18 insertions(+) diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index eeadf11fa3d5..ec6713098f11 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -10,6 +10,7 @@ import type { NodeOptions } from '@sentry/node'; import type { Client, Integration, Options, StackParser } from '@sentry/core'; import type * as clientSdk from './index.client'; +import type * as serverSdk from './index.server'; import sentryAstro from './index.server'; /** Initializes Sentry Astro SDK */ @@ -26,4 +27,6 @@ export declare function flush(timeout?: number | undefined): PromiseLike Integration[]; export declare const defaultStackParser: StackParser; + +export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; diff --git a/packages/react-router/src/index.types.ts b/packages/react-router/src/index.types.ts index 4d7fb425f5ee..cf62bf717794 100644 --- a/packages/react-router/src/index.types.ts +++ b/packages/react-router/src/index.types.ts @@ -15,3 +15,5 @@ export declare const contextLinesIntegration: typeof clientSdk.contextLinesInteg export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const defaultStackParser: StackParser; export declare const getDefaultIntegrations: (options: Options) => Integration[]; + +export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index 763a2747f69e..2d1ae40da8e5 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -21,6 +21,8 @@ export declare const defaultStackParser: StackParser; export declare function captureRemixServerException(err: unknown, name: string, request: Request): Promise; +export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + // This variable is not a runtime variable but just a type to tell typescript that the methods below can either come // from the client SDK or from the server SDK. TypeScript is smart enough to understand that these resolve to the same // methods from `@sentry/core`. diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts index 54a5ec6d6a3c..d243bd371241 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -22,3 +22,5 @@ export declare const defaultStackParser: StackParser; export declare function close(timeout?: number | undefined): PromiseLike; export declare function flush(timeout?: number | undefined): PromiseLike; export declare function lastEventId(): string | undefined; + +export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index bf2edbfb0a0f..161e7098de11 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -53,3 +53,5 @@ export declare function flush(timeout?: number | undefined): PromiseLike; + +export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts index 84a987788d17..0e3bbca37bf9 100644 --- a/packages/tanstackstart-react/src/index.types.ts +++ b/packages/tanstackstart-react/src/index.types.ts @@ -25,3 +25,5 @@ export declare const ErrorBoundary: typeof clientSdk.ErrorBoundary; export declare const createReduxEnhancer: typeof clientSdk.createReduxEnhancer; export declare const showReportDialog: typeof clientSdk.showReportDialog; export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary; + +export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger;