From 0e17e413cb6c38152e261f6728a5e1ed00be8967 Mon Sep 17 00:00:00 2001 From: yashkohli88 Date: Tue, 5 May 2026 11:28:57 +0530 Subject: [PATCH 1/3] fix(logging): prevent verbose/debug logs from leaking to Application Insights The logger.on('data') event listener bypassed Winston's transport-level filtering, forwarding verbose/debug logs to Application Insights even when the configured level was info. Replace with a custom ApplicationInsightsTransport extending winston-transport that participates in Winston's built-in level filtering. Extract shared format to logger level to avoid duplication. Refs: clearlydefined/crawler#739 --- providers/logging/winstonConfig.ts | 82 +++++++++++--------- test/providers/logging/winstonConfig.ts | 99 +++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 33 deletions(-) diff --git a/providers/logging/winstonConfig.ts b/providers/logging/winstonConfig.ts index bd2e0422..9131c95a 100644 --- a/providers/logging/winstonConfig.ts +++ b/providers/logging/winstonConfig.ts @@ -4,6 +4,8 @@ import appInsights from 'applicationinsights' import config from 'painless-config' import winston from 'winston' +import Transport from 'winston-transport' +import type { InsightsClient } from '../../lib/mockInsights.ts' import { MockInsights } from '../../lib/mockInsights.ts' const SENSITIVE_HEADERS = ['x-api-key', 'authorization', 'proxy-authorization', 'cookie'] @@ -103,6 +105,47 @@ function buildProperties(info: Record): Record { ) } +class ApplicationInsightsTransport extends Transport { + private aiClient: InsightsClient | null + + constructor(opts: { aiClient: InsightsClient | null; level?: string }) { + super({ level: opts.level }) + this.aiClient = opts.aiClient + } + + override log(info: any, callback: () => void): void { + setImmediate(() => this.emit('logged', info)) + + if (!this.aiClient || !info) { + callback() + return + } + + const properties = buildProperties(info) + if (info.level === 'error') { + if (info.stack) { + const exception = info.cause ? new Error(info.message, { cause: info.cause }) : new Error(info.message) + exception.stack = info.stack + this.aiClient.trackException({ exception, properties }) + } else { + this.aiClient.trackTrace({ + message: info.message, + severity: appInsights.KnownSeverityLevel.Error, + properties + }) + } + } else { + this.aiClient.trackTrace({ + message: info.message, + severity: mapLevel(info.level), + properties + }) + } + + callback() + } +} + /** * Factory function to create a Winston logger instance. */ @@ -115,20 +158,12 @@ function factory(options?: WinstonLoggerOptions): winston.Logger { } MockInsights.setup(realOptions.connectionString || 'mock', realOptions.echo) + const aiClient = MockInsights.getClient() - const logFormat = winston.format.combine( - sanitizeMeta(), - winston.format.timestamp(), - winston.format.errors({ stack: true, cause: true }), - winston.format.printf(({ timestamp, level, message, ...meta }) => { - const metaKeys = Object.keys(meta) - const metaString = metaKeys.length ? `\n${JSON.stringify(meta, null, 2)}` : '' - return `${timestamp} [${level}]: ${message}${metaString}` - }) - ) + const logFormat = winston.format.combine(sanitizeMeta(), winston.format.errors({ stack: true, cause: true })) const consoleFormat = winston.format.combine( - sanitizeMeta(), + winston.format.timestamp(), winston.format.colorize(), winston.format.printf(({ timestamp, level, message, ...meta }) => { const metaKeys = Object.keys(meta) @@ -144,32 +179,13 @@ function factory(options?: WinstonLoggerOptions): winston.Logger { new winston.transports.Console({ format: consoleFormat, silent: !realOptions.echo + }), + new ApplicationInsightsTransport({ + aiClient }) ] }) - const aiClient = MockInsights.getClient() - - // Pipe Winston logs to Application Insights - logger.on('data', info => { - const properties = buildProperties(info) - if (info.level === 'error') { - if (info.stack) { - const exception = info.cause ? new Error(info.message, { cause: info.cause }) : new Error(info.message) - exception.stack = info.stack - aiClient.trackException({ exception, properties }) - } else { - aiClient.trackTrace({ - message: info.message, - severity: appInsights.KnownSeverityLevel.Error, - properties - }) - } - } else { - aiClient.trackTrace({ message: info.message, severity: mapLevel(info.level), properties }) - } - }) - return logger } diff --git a/test/providers/logging/winstonConfig.ts b/test/providers/logging/winstonConfig.ts index 9b758c3e..4ad2e164 100644 --- a/test/providers/logging/winstonConfig.ts +++ b/test/providers/logging/winstonConfig.ts @@ -442,4 +442,103 @@ describe('winstonFactory', () => { expect(exceptionTelemetry.exception.cause).to.equal(undefined) expect(exceptionTelemetry.exception.stack).to.include('no cause error') }) + + it('does not forward debug or verbose when logger level is info', async () => { + const aiClient = { + trackException: sinon.stub(), + trackTrace: sinon.stub() + } + sinon.stub(MockInsights, 'setup') + sinon.stub(MockInsights, 'getClient').returns(aiClient as any) + + const logger = winstonFactory({ + connectionString: 'mock', + echo: false, + level: 'info' + }) + + logger.debug('debug should not be sent') + logger.verbose('verbose should not be sent') + logger.info('info should be sent') + + await new Promise(resolve => setImmediate(resolve)) + + expect(aiClient.trackException.called).to.be.false + expect(aiClient.trackTrace.callCount).to.equal(1) + expect(aiClient.trackTrace.firstCall.args[0].message).to.include('info should be sent') + expect(aiClient.trackTrace.firstCall.args[0].severity).to.equal('Information') + }) + + it('routes errors with stack to trackException', async () => { + const aiClient = { + trackException: sinon.stub(), + trackTrace: sinon.stub() + } + sinon.stub(MockInsights, 'setup') + sinon.stub(MockInsights, 'getClient').returns(aiClient as any) + + const logger = winstonFactory({ + connectionString: 'mock', + echo: false, + level: 'error' + }) + + const error = new Error('something broke') + logger.error(error) + + await new Promise(resolve => setImmediate(resolve)) + + expect(aiClient.trackException.callCount).to.equal(1) + expect(aiClient.trackTrace.callCount).to.equal(0) + const telemetry = aiClient.trackException.firstCall.args[0] + expect(telemetry.exception.message).to.equal('something broke') + expect(telemetry.exception.stack).to.include('something broke') + }) + + it('routes errors without stack to trackTrace with Error severity', async () => { + const aiClient = { + trackException: sinon.stub(), + trackTrace: sinon.stub() + } + sinon.stub(MockInsights, 'setup') + sinon.stub(MockInsights, 'getClient').returns(aiClient as any) + + const logger = winstonFactory({ + connectionString: 'mock', + echo: false, + level: 'error' + }) + + logger.error('plain error message') + + await new Promise(resolve => setImmediate(resolve)) + + expect(aiClient.trackException.callCount).to.equal(0) + expect(aiClient.trackTrace.callCount).to.equal(1) + const telemetry = aiClient.trackTrace.firstCall.args[0] + expect(telemetry.message).to.include('plain error message') + expect(telemetry.severity).to.equal('Error') + }) + + it('maps warn level to Warning severity', async () => { + const aiClient = { + trackException: sinon.stub(), + trackTrace: sinon.stub() + } + sinon.stub(MockInsights, 'setup') + sinon.stub(MockInsights, 'getClient').returns(aiClient as any) + + const logger = winstonFactory({ + connectionString: 'mock', + echo: false, + level: 'warn' + }) + + logger.warn('a warning') + + await new Promise(resolve => setImmediate(resolve)) + + expect(aiClient.trackTrace.callCount).to.equal(1) + expect(aiClient.trackTrace.firstCall.args[0].severity).to.equal('Warning') + }) }) From eec745c140901e506ebb5356f74a0b50b2272835 Mon Sep 17 00:00:00 2001 From: yashkohli88 Date: Tue, 19 May 2026 17:42:32 +0530 Subject: [PATCH 2/3] fix(logging): ensure callback runs if Application Insights throws Wrap AI tracking calls in try/finally so callback() always executes. Prevents Winston from hanging on exceptions. --- providers/logging/winstonConfig.ts | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/providers/logging/winstonConfig.ts b/providers/logging/winstonConfig.ts index 9131c95a..f0db3766 100644 --- a/providers/logging/winstonConfig.ts +++ b/providers/logging/winstonConfig.ts @@ -121,28 +121,30 @@ class ApplicationInsightsTransport extends Transport { return } - const properties = buildProperties(info) - if (info.level === 'error') { - if (info.stack) { - const exception = info.cause ? new Error(info.message, { cause: info.cause }) : new Error(info.message) - exception.stack = info.stack - this.aiClient.trackException({ exception, properties }) + try { + const properties = buildProperties(info) + if (info.level === 'error') { + if (info.stack) { + const exception = info.cause ? new Error(info.message, { cause: info.cause }) : new Error(info.message) + exception.stack = info.stack + this.aiClient.trackException({ exception, properties }) + } else { + this.aiClient.trackTrace({ + message: info.message, + severity: appInsights.KnownSeverityLevel.Error, + properties + }) + } } else { this.aiClient.trackTrace({ message: info.message, - severity: appInsights.KnownSeverityLevel.Error, + severity: mapLevel(info.level), properties }) } - } else { - this.aiClient.trackTrace({ - message: info.message, - severity: mapLevel(info.level), - properties - }) + } finally { + callback() } - - callback() } } From b2d72abc5136408e2a986143d56d7c8a91017b3d Mon Sep 17 00:00:00 2001 From: Yash Kohli Date: Wed, 3 Jun 2026 19:00:43 +0530 Subject: [PATCH 3/3] fix(logging): catch AI client errors and test cases added --- providers/logging/winstonConfig.ts | 2 ++ test/providers/logging/winstonConfig.ts | 36 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/providers/logging/winstonConfig.ts b/providers/logging/winstonConfig.ts index f0db3766..367fe72e 100644 --- a/providers/logging/winstonConfig.ts +++ b/providers/logging/winstonConfig.ts @@ -142,6 +142,8 @@ class ApplicationInsightsTransport extends Transport { properties }) } + } catch (err) { + console.error('ApplicationInsights telemetry failed', err) } finally { callback() } diff --git a/test/providers/logging/winstonConfig.ts b/test/providers/logging/winstonConfig.ts index 4ad2e164..81cfd9db 100644 --- a/test/providers/logging/winstonConfig.ts +++ b/test/providers/logging/winstonConfig.ts @@ -541,4 +541,40 @@ describe('winstonFactory', () => { expect(aiClient.trackTrace.callCount).to.equal(1) expect(aiClient.trackTrace.firstCall.args[0].severity).to.equal('Warning') }) + + it('still calls callback if trackTrace throws', async () => { + const aiClient = { + trackException: sinon.stub(), + trackTrace: sinon.stub().throws(new Error('AI unavailable')) + } + sinon.stub(MockInsights, 'setup') + sinon.stub(MockInsights, 'getClient').returns(aiClient as any) + const consoleError = sinon.stub(console, 'error') + + const logger = winstonFactory({ connectionString: 'mock', echo: false, level: 'info' }) + logger.info('test message') + + await new Promise(resolve => setImmediate(resolve)) + + expect(aiClient.trackTrace.callCount).to.equal(1) + consoleError.restore() + }) + + it('still calls callback if trackException throws', async () => { + const aiClient = { + trackException: sinon.stub().throws(new Error('AI unavailable')), + trackTrace: sinon.stub() + } + sinon.stub(MockInsights, 'setup') + sinon.stub(MockInsights, 'getClient').returns(aiClient as any) + const consoleError = sinon.stub(console, 'error') + + const logger = winstonFactory({ connectionString: 'mock', echo: false, level: 'error' }) + logger.error(new Error('boom')) + + await new Promise(resolve => setImmediate(resolve)) + + expect(aiClient.trackException.callCount).to.equal(1) + consoleError.restore() + }) })