diff --git a/dev-packages/node-integration-tests/suites/winston/subject.ts b/dev-packages/node-integration-tests/suites/winston/subject.ts index 1047f2f1cd47..2c9d88456cce 100644 --- a/dev-packages/node-integration-tests/suites/winston/subject.ts +++ b/dev-packages/node-integration-tests/suites/winston/subject.ts @@ -9,6 +9,7 @@ Sentry.init({ environment: 'test', enableLogs: true, transport: loggingTransport, + debug: true, }); async function run(): Promise { @@ -64,6 +65,81 @@ async function run(): Promise { }); } + if (process.env.WITH_FILTER === 'true') { + const FilteredSentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport, { + levels: ['error'], + }); + const filteredLogger = winston.createLogger({ + transports: [new FilteredSentryWinstonTransport()], + }); + + filteredLogger.info('Ignored message'); + filteredLogger.error('Test error message'); + } + + // If unmapped custom level is requested (tests debug line for unknown levels) + if (process.env.UNMAPPED_CUSTOM_LEVEL === 'true') { + const customLevels = { + levels: { + myUnknownLevel: 0, + error: 1, + }, + }; + + // Create transport WITHOUT customLevelMap for myUnknownLevel + // myUnknownLevel will default to 'info', but we only capture 'error' + const UnmappedSentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport, { + levels: ['error'], + }); + + const unmappedLogger = winston.createLogger({ + levels: customLevels.levels, + level: 'error', + transports: [new UnmappedSentryWinstonTransport()], + }); + + // This should NOT be captured (unknown level defaults to 'info', which is not in levels) + // @ts-ignore - custom levels are not part of the winston logger + unmappedLogger.myUnknownLevel('This unknown level message should be skipped'); + // This SHOULD be captured + unmappedLogger.error('This error message should be captured'); + } + + // If custom level mapping is requested + if (process.env.CUSTOM_LEVEL_MAPPING === 'true') { + const customLevels = { + levels: { + customCritical: 0, + customWarning: 1, + customNotice: 2, + }, + }; + + const SentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport, { + customLevelMap: { + customCritical: 'fatal', + customWarning: 'warn', + customNotice: 'info', + }, + }); + + const mappedLogger = winston.createLogger({ + levels: customLevels.levels, + // https://github.com/winstonjs/winston/issues/1491 + // when custom levels are set with a transport, + // the level must be set on the logger + level: 'customNotice', + transports: [new SentryWinstonTransport()], + }); + + // @ts-ignore - custom levels are not part of the winston logger + mappedLogger.customCritical('This is a critical message'); + // @ts-ignore - custom levels are not part of the winston logger + mappedLogger.customWarning('This is a warning message'); + // @ts-ignore - custom levels are not part of the winston logger + mappedLogger.customNotice('This is a notice message'); + } + await Sentry.flush(); } diff --git a/dev-packages/node-integration-tests/suites/winston/test.ts b/dev-packages/node-integration-tests/suites/winston/test.ts index 777b1149c871..1b359cc20f80 100644 --- a/dev-packages/node-integration-tests/suites/winston/test.ts +++ b/dev-packages/node-integration-tests/suites/winston/test.ts @@ -123,6 +123,71 @@ describe('winston integration', () => { await runner.completed(); }); + test("should capture winston logs with filter but don't show custom level warnings", async () => { + const runner = createRunner(__dirname, 'subject.ts') + .withEnv({ WITH_FILTER: 'true' }) + .expect({ + log: { + items: [ + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + + const logs = runner.getLogs(); + + const warning = logs.find(log => log.includes('Winston log level info is not captured by Sentry.')); + + expect(warning).not.toBeDefined(); + }); + test('should capture winston logs with metadata', async () => { const runner = createRunner(__dirname, 'subject.ts') .withEnv({ WITH_METADATA: 'true' }) @@ -183,4 +248,162 @@ describe('winston integration', () => { await runner.completed(); }); + + test('should skip unmapped custom levels when not in the levels option', async () => { + const runner = createRunner(__dirname, 'subject.ts') + .withEnv({ UNMAPPED_CUSTOM_LEVEL: 'true' }) + .expect({ + log: { + items: [ + // First, the default logger captures info and error + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + // Then the unmapped logger only captures error (myUnknownLevel defaults to info, which is skipped) + { + timestamp: expect.any(Number), + level: 'error', + body: 'This error message should be captured', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + + const logs = runner.getLogs(); + + const warning = logs.find(log => log.includes('Winston log level myUnknownLevel is not captured by Sentry.')); + + expect(warning).toBeDefined(); + }); + + test('should map custom winston levels to Sentry severity levels', async () => { + const runner = createRunner(__dirname, 'subject.ts') + .withEnv({ CUSTOM_LEVEL_MAPPING: 'true' }) + .expect({ + log: { + items: [ + // First, the default logger captures info and error + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + // Then the mapped logger uses custom level mappings + { + timestamp: expect.any(Number), + level: 'fatal', // 'critical' maps to 'fatal' + body: 'This is a critical message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'warn', // 'warning' maps to 'warn' + body: 'This is a warning message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', // 'notice' maps to 'info' + body: 'This is a notice message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); }); diff --git a/packages/node-core/src/integrations/winston.ts b/packages/node-core/src/integrations/winston.ts index bea0fa584bf7..a461d8797338 100644 --- a/packages/node-core/src/integrations/winston.ts +++ b/packages/node-core/src/integrations/winston.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import type { LogSeverityLevel } from '@sentry/core'; +import { debug } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; import { captureLog } from '../logs/capture'; const DEFAULT_CAPTURED_LEVELS: Array = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; @@ -25,6 +27,21 @@ interface WinstonTransportOptions { * ``` */ levels?: Array; + + /** + * Use this option to map custom levels to Sentry log severity levels. + * + * @example + * ```ts + * const SentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport, { + * customLevelMap: { + * myCustomLevel: 'info', + * customError: 'error', + * }, + * }); + * ``` + */ + customLevelMap?: Record; } /** @@ -85,12 +102,20 @@ export function createSentryWinstonTransport