From 6f5b7d3854a496db1753c6c213509f015f94de97 Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 7 Nov 2025 13:50:28 +0530 Subject: [PATCH 1/8] fix(pino): pino thread-stream added in test --- .../test/tracing/logging/pino/app.js | 21 ++++++++- .../test/tracing/logging/pino/test.js | 45 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/collector/test/tracing/logging/pino/app.js b/packages/collector/test/tracing/logging/pino/app.js index d22c6227a4..f5646f182f 100644 --- a/packages/collector/test/tracing/logging/pino/app.js +++ b/packages/collector/test/tracing/logging/pino/app.js @@ -14,7 +14,10 @@ process.on('SIGTERM', () => { }); require('./mockVersion'); -require('../../../..')(); + +if (!process.env.NODE_OPTIONS || !process.env.NODE_OPTIONS.includes('src/immediate')) { + require('../../../..')(); +} const fetch = require('node-fetch-v2'); const bodyParser = require('body-parser'); @@ -212,6 +215,22 @@ app.get('/express-pino-error-random-object-and-string', (req, res) => { finish(res); }); +app.get('/thread-stream-test', (req, res) => { + try { + const mode = process.env.PINO_WORKER_MODE || 'transport'; + + const logger = + mode === 'transport' + ? pino({ transport: { target: 'pino-pretty', options: { destination: 1 } } }) + : pino({ destination: 1 }); + logger.error('Pino worker test error log'); + + res.sendStatus(200); + } catch (e) { + res.status(500).send(`Failed: ${e.message}`); + } +}); + function finish(res) { fetch(`http://127.0.0.1:${agentPort}`).then(() => { res.sendStatus(200); diff --git a/packages/collector/test/tracing/logging/pino/test.js b/packages/collector/test/tracing/logging/pino/test.js index a78b9e3c9a..0d69ff3c6f 100644 --- a/packages/collector/test/tracing/logging/pino/test.js +++ b/packages/collector/test/tracing/logging/pino/test.js @@ -102,6 +102,51 @@ describe('tracing/logging/pino', function () { }); }); }); + describe('pino thread-stream worker', function () { + let controls; + + before(async () => { + controls = new ProcessControls({ + dirname: __dirname, + useGlobalAgent: true, + env: { + PINO_WORKER_MODE: 'transport', + PINO_EXPRESS: 'false', + NODE_OPTIONS: `--require ${path.join(__dirname, '../../../..', 'src', 'immediate.js')}` + } + }); + + await controls.startAndWaitForAgentConnection(); + }); + + beforeEach(async () => { + await agentControls.clearReceivedTraceData(); + }); + + after(async () => { + await controls.stop(); + }); + + it('must trace without errors', async () => { + await controls.sendRequest({ + method: 'GET', + path: '/thread-stream-test' + }); + await testUtils.delay(1000); + const spans = await agentControls.getSpans(); + + const logSpan = spans.find(s => s.n === 'log.pino'); + expect(logSpan).to.exist; + expect(logSpan.data.log.message).to.equal('Pino worker test error log'); + + const httpSpan = spans.find(s => s.n === 'node.http.server'); + expect(httpSpan).to.exist; + expect(httpSpan.data.http.path_tpl).to.equal('/pino-worker-test'); + expect(httpSpan.data.http.status).to.equal(200); + + expect(spans).to.have.lengthOf(2); + }); + }); function runTests(pinoVersion, useExpressPino) { const suffix = useExpressPino ? ' (express-pino)' : ''; From 9c6b4d0588c45e82e20393604a090418e08772f9 Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 7 Nov 2025 14:11:58 +0530 Subject: [PATCH 2/8] test: fix --- packages/collector/test/tracing/logging/pino/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/collector/test/tracing/logging/pino/test.js b/packages/collector/test/tracing/logging/pino/test.js index 0d69ff3c6f..49d44540d0 100644 --- a/packages/collector/test/tracing/logging/pino/test.js +++ b/packages/collector/test/tracing/logging/pino/test.js @@ -141,7 +141,7 @@ describe('tracing/logging/pino', function () { const httpSpan = spans.find(s => s.n === 'node.http.server'); expect(httpSpan).to.exist; - expect(httpSpan.data.http.path_tpl).to.equal('/pino-worker-test'); + expect(httpSpan.data.http.path_tpl).to.equal('/thread-stream-test'); expect(httpSpan.data.http.status).to.equal(200); expect(spans).to.have.lengthOf(2); From d2712cc62153a3ce5298ab67a30c5ecadefa4375 Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 7 Nov 2025 14:48:45 +0530 Subject: [PATCH 3/8] fix: skip thread stream --- packages/collector/src/index.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/collector/src/index.js b/packages/collector/src/index.js index 3b5c251630..dab3072328 100644 --- a/packages/collector/src/index.js +++ b/packages/collector/src/index.js @@ -41,6 +41,25 @@ try { // Worker threads are not available, so we know that this is the main thread. } +// Skip Instana instrumentation in Pino thread-stream workers. +// Pino uses internal worker threads for its "thread-stream" transport. +// These threads are not part of the main application logic and should +// not be instrumented. +// +// This prevents errors when the collector is loaded via NODE_OPTIONS in worker threads. +const { workerData } = require('worker_threads'); +if (!isMainThread && workerData && typeof workerData === 'object') { + const dataString = Object.values(workerData).join(' ').toLowerCase(); + if (dataString.includes('thread-stream')) { + // eslint-disable-next-line no-console + console.warn('[Instana] Skipping instrumentation in thread-stream worker.'); + module.exports = function noOp() {}; + module.exports.default = function noOp() {}; + // @ts-ignore + return; + } +} + const path = require('path'); const instanaNodeJsCore = require('@instana/core'); const instanaSharedMetrics = require('@instana/shared-metrics'); From 026a25bf33dba2d29af942b660327eedd3593789 Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 7 Nov 2025 15:10:50 +0530 Subject: [PATCH 4/8] fix: skip thread stream --- packages/collector/src/index.js | 34 ++++++++----------- packages/collector/src/util/skipPinoWorker.js | 30 ++++++++++++++++ 2 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 packages/collector/src/util/skipPinoWorker.js diff --git a/packages/collector/src/index.js b/packages/collector/src/index.js index dab3072328..f554be6d21 100644 --- a/packages/collector/src/index.js +++ b/packages/collector/src/index.js @@ -33,31 +33,27 @@ if (isNodeJsTooOld()) { // @ts-ignore TS1108 (return can only be used within a function body) return; } - -let isMainThread = true; -try { - isMainThread = require('worker_threads').isMainThread; -} catch (err) { - // Worker threads are not available, so we know that this is the main thread. -} - +const skipPinoWorker = require('./util/skipPinoWorker'); // Skip Instana instrumentation in Pino thread-stream workers. // Pino uses internal worker threads for its "thread-stream" transport. // These threads are not part of the main application logic and should // not be instrumented. // // This prevents errors when the collector is loaded via NODE_OPTIONS in worker threads. -const { workerData } = require('worker_threads'); -if (!isMainThread && workerData && typeof workerData === 'object') { - const dataString = Object.values(workerData).join(' ').toLowerCase(); - if (dataString.includes('thread-stream')) { - // eslint-disable-next-line no-console - console.warn('[Instana] Skipping instrumentation in thread-stream worker.'); - module.exports = function noOp() {}; - module.exports.default = function noOp() {}; - // @ts-ignore - return; - } +if (skipPinoWorker()) { + // eslint-disable-next-line no-console + console.warn('[Instana] Skipping instrumentation in Pino thread-stream worker.'); + module.exports = function noOp() {}; + module.exports.default = function noOp() {}; + // @ts-ignore + return; +} + +let isMainThread = true; +try { + isMainThread = require('worker_threads').isMainThread; +} catch (err) { + // Worker threads are not available, so we know that this is the main thread. } const path = require('path'); diff --git a/packages/collector/src/util/skipPinoWorker.js b/packages/collector/src/util/skipPinoWorker.js new file mode 100644 index 0000000000..b11f4a8a5f --- /dev/null +++ b/packages/collector/src/util/skipPinoWorker.js @@ -0,0 +1,30 @@ +/* + * (c) Copyright IBM Corp. 2025 + */ + +'use strict'; + +/** + * Detects whether the current process is a Pino thread-stream worker thread. + * Returns true if we should skip instrumentation for this worker. + */ +function skipPinoWorker() { + try { + const { isMainThread, workerData } = require('worker_threads'); + if (isMainThread) return false; + + // Check workerData properties for pino thread-stream + if (workerData && typeof workerData === 'object') { + const dataString = Object.values(workerData).join(' ').toLowerCase(); + if (dataString.includes('thread-stream')) { + return true; + } + } + + return false; + } catch { + return false; + } +} + +module.exports = skipPinoWorker; From fcc9e4a7f4cb62705f8b795a1ee8ada5ad49392f Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 7 Nov 2025 15:51:39 +0530 Subject: [PATCH 5/8] fix: skip thread stream --- packages/collector/src/index.js | 16 ------- packages/collector/src/util/skipPinoWorker.js | 30 ------------- .../src/util/excludedFromInstrumentation.js | 42 ++++++++++++++++++- 3 files changed, 40 insertions(+), 48 deletions(-) delete mode 100644 packages/collector/src/util/skipPinoWorker.js diff --git a/packages/collector/src/index.js b/packages/collector/src/index.js index f554be6d21..06c4c12ac4 100644 --- a/packages/collector/src/index.js +++ b/packages/collector/src/index.js @@ -33,22 +33,6 @@ if (isNodeJsTooOld()) { // @ts-ignore TS1108 (return can only be used within a function body) return; } -const skipPinoWorker = require('./util/skipPinoWorker'); -// Skip Instana instrumentation in Pino thread-stream workers. -// Pino uses internal worker threads for its "thread-stream" transport. -// These threads are not part of the main application logic and should -// not be instrumented. -// -// This prevents errors when the collector is loaded via NODE_OPTIONS in worker threads. -if (skipPinoWorker()) { - // eslint-disable-next-line no-console - console.warn('[Instana] Skipping instrumentation in Pino thread-stream worker.'); - module.exports = function noOp() {}; - module.exports.default = function noOp() {}; - // @ts-ignore - return; -} - let isMainThread = true; try { isMainThread = require('worker_threads').isMainThread; diff --git a/packages/collector/src/util/skipPinoWorker.js b/packages/collector/src/util/skipPinoWorker.js deleted file mode 100644 index b11f4a8a5f..0000000000 --- a/packages/collector/src/util/skipPinoWorker.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * (c) Copyright IBM Corp. 2025 - */ - -'use strict'; - -/** - * Detects whether the current process is a Pino thread-stream worker thread. - * Returns true if we should skip instrumentation for this worker. - */ -function skipPinoWorker() { - try { - const { isMainThread, workerData } = require('worker_threads'); - if (isMainThread) return false; - - // Check workerData properties for pino thread-stream - if (workerData && typeof workerData === 'object') { - const dataString = Object.values(workerData).join(' ').toLowerCase(); - if (dataString.includes('thread-stream')) { - return true; - } - } - - return false; - } catch { - return false; - } -} - -module.exports = skipPinoWorker; diff --git a/packages/core/src/util/excludedFromInstrumentation.js b/packages/core/src/util/excludedFromInstrumentation.js index 1d139185da..8b565e4edf 100644 --- a/packages/core/src/util/excludedFromInstrumentation.js +++ b/packages/core/src/util/excludedFromInstrumentation.js @@ -13,13 +13,50 @@ // no value and also creates confusing log output, so we exclude them here explicitly. const excludePattern = /^.*\/(?:npm(?:\.js)?|npm-cli(?:\.js)?|yarn(?:\.js)?|yarn\/lib\/cli(?:\.js)?)$/i; +/** + * Check if the current process is a Pino thread-stream worker thread + * @returns {boolean} + */ +function isPinoThreadStreamWorker() { + try { + const { isMainThread, workerData } = require('worker_threads'); + if (isMainThread) return false; + + if (workerData && typeof workerData === 'object') { + const nested = workerData.workerData; + const isPinoWorker = + typeof workerData.filename === 'string' && + nested && + nested.$context && + typeof nested.$context.threadStreamVersion === 'string'; + + if (isPinoWorker) { + return true; + } + } + return false; + } catch { + return false; + } +} + /** * @type {Function} * @returns {boolean} */ module.exports = exports = function isExcludedFromInstrumentation() { const mainModule = process.argv[1]; - const excludedFromInstrumentation = typeof mainModule === 'string' && excludePattern.test(mainModule); + let excludedFromInstrumentation = typeof mainModule === 'string' && excludePattern.test(mainModule); + let exclusionReason = 'npm or yarn'; + + // Skip Instana instrumentation in Pino thread-stream workers. + // Pino uses internal worker threads for its "thread-stream" transport. + // These threads are not part of the main application logic and should + // not be instrumented. + if (!excludedFromInstrumentation && isPinoThreadStreamWorker()) { + excludedFromInstrumentation = true; + exclusionReason = 'Pino thread-stream worker'; + } if (excludedFromInstrumentation) { const logLevelIsDebugOrInfo = @@ -32,7 +69,8 @@ module.exports = exports = function isExcludedFromInstrumentation() { // eslint-disable-next-line no-console console.log( `[Instana] INFO: Not instrumenting process ${process.pid}: ${process.argv[0]} ${mainModule}` + - ' - this Node.js process seems to be npm or yarn. A child process started via "npm start" or "yarn start" ' + + // eslint-disable-next-line max-len + ` - this Node.js process seems to be ${exclusionReason}. A child process started via "npm start" or "yarn start" ` + '_will_ be instrumented, but not npm or yarn itself.' ); } From f3acfba6b250fbc7486ed71b16d80d1c2e7b4cda Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 7 Nov 2025 16:42:59 +0530 Subject: [PATCH 6/8] chore: fine tuning --- .../src/util/excludedFromInstrumentation.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/core/src/util/excludedFromInstrumentation.js b/packages/core/src/util/excludedFromInstrumentation.js index 8b565e4edf..d08e96d147 100644 --- a/packages/core/src/util/excludedFromInstrumentation.js +++ b/packages/core/src/util/excludedFromInstrumentation.js @@ -14,7 +14,8 @@ const excludePattern = /^.*\/(?:npm(?:\.js)?|npm-cli(?:\.js)?|yarn(?:\.js)?|yarn\/lib\/cli(?:\.js)?)$/i; /** - * Check if the current process is a Pino thread-stream worker thread + * Determines if the current process is a Pino thread-stream worker. + * Pino uses background worker threads for "thread-stream" logging, which should not be instrumented. * @returns {boolean} */ function isPinoThreadStreamWorker() { @@ -47,30 +48,31 @@ function isPinoThreadStreamWorker() { module.exports = exports = function isExcludedFromInstrumentation() { const mainModule = process.argv[1]; let excludedFromInstrumentation = typeof mainModule === 'string' && excludePattern.test(mainModule); - let exclusionReason = 'npm or yarn'; + let reason = 'npm-yarn'; - // Skip Instana instrumentation in Pino thread-stream workers. - // Pino uses internal worker threads for its "thread-stream" transport. - // These threads are not part of the main application logic and should - // not be instrumented. if (!excludedFromInstrumentation && isPinoThreadStreamWorker()) { excludedFromInstrumentation = true; - exclusionReason = 'Pino thread-stream worker'; + reason = 'pino-thread-stream'; } - if (excludedFromInstrumentation) { - const logLevelIsDebugOrInfo = - process.env.INSTANA_DEBUG || - (process.env.INSTANA_LOG_LEVEL && - (process.env.INSTANA_LOG_LEVEL.toLowerCase() === 'info' || - process.env.INSTANA_LOG_LEVEL.toLowerCase() === 'debug')); + const logEnabled = + process.env.INSTANA_DEBUG || + (process.env.INSTANA_LOG_LEVEL && ['info', 'debug'].includes(process.env.INSTANA_LOG_LEVEL.toLowerCase())); - if (logLevelIsDebugOrInfo) { + if (excludedFromInstrumentation && logEnabled) { + if (reason === 'pino-thread-stream') { + // eslint-disable-next-line no-console + console.log( + // eslint-disable-next-line max-len + `[Instana] INFO: Skipping instrumentation for process ${process.pid} - detected as a Pino thread-stream worker. ` + + 'Logging threads do not require instrumentation.' + ); + } else { // eslint-disable-next-line no-console console.log( `[Instana] INFO: Not instrumenting process ${process.pid}: ${process.argv[0]} ${mainModule}` + // eslint-disable-next-line max-len - ` - this Node.js process seems to be ${exclusionReason}. A child process started via "npm start" or "yarn start" ` + + ' - this Node.js process seems to be npm or yarn. A child process started via "npm start" or "yarn start" ' + '_will_ be instrumented, but not npm or yarn itself.' ); } From 23636c27587ec068eb54a103e01abbaea9ff6cc7 Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 7 Nov 2025 16:50:07 +0530 Subject: [PATCH 7/8] chore: fine tuning --- packages/collector/src/index.js | 1 + packages/core/src/util/excludedFromInstrumentation.js | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/collector/src/index.js b/packages/collector/src/index.js index 06c4c12ac4..3b5c251630 100644 --- a/packages/collector/src/index.js +++ b/packages/collector/src/index.js @@ -33,6 +33,7 @@ if (isNodeJsTooOld()) { // @ts-ignore TS1108 (return can only be used within a function body) return; } + let isMainThread = true; try { isMainThread = require('worker_threads').isMainThread; diff --git a/packages/core/src/util/excludedFromInstrumentation.js b/packages/core/src/util/excludedFromInstrumentation.js index d08e96d147..b29740a1a9 100644 --- a/packages/core/src/util/excludedFromInstrumentation.js +++ b/packages/core/src/util/excludedFromInstrumentation.js @@ -71,7 +71,6 @@ module.exports = exports = function isExcludedFromInstrumentation() { // eslint-disable-next-line no-console console.log( `[Instana] INFO: Not instrumenting process ${process.pid}: ${process.argv[0]} ${mainModule}` + - // eslint-disable-next-line max-len ' - this Node.js process seems to be npm or yarn. A child process started via "npm start" or "yarn start" ' + '_will_ be instrumented, but not npm or yarn itself.' ); From 4db557b866ccf336933b2209096f05b7e1789aa1 Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 7 Nov 2025 17:00:02 +0530 Subject: [PATCH 8/8] chore: unit tests --- .../util/excludedFromInstrumentation_test.js | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/packages/core/test/util/excludedFromInstrumentation_test.js b/packages/core/test/util/excludedFromInstrumentation_test.js index b6a3fbddbf..e371247804 100644 --- a/packages/core/test/util/excludedFromInstrumentation_test.js +++ b/packages/core/test/util/excludedFromInstrumentation_test.js @@ -66,6 +66,165 @@ describe('util.excludedFromInstrumentation', () => { expect(isExcluded('/usr/local/npmx.js')).to.be.false; }); + describe('pino thread-stream worker detection', () => { + let originalRequire; + + beforeEach(() => { + originalRequire = require.cache[require.resolve('worker_threads')]; + }); + + afterEach(() => { + if (originalRequire) { + require.cache[require.resolve('worker_threads')] = originalRequire; + } else { + delete require.cache[require.resolve('worker_threads')]; + } + delete require.cache[require.resolve('../../src/util/excludedFromInstrumentation')]; + }); + + it('should exclude Pino thread-stream worker with valid structure', () => { + mockWorkerThreads({ + isMainThread: false, + workerData: { + filename: '/path/to/worker.js', + workerData: { + $context: { + threadStreamVersion: '2.0.0' + }, + target: 'pino-pretty' + } + } + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.true; + }); + + it('should exclude Pino thread-stream worker with targets property', () => { + mockWorkerThreads({ + isMainThread: false, + workerData: { + filename: '/path/to/worker.js', + workerData: { + $context: { + threadStreamVersion: '2.1.0' + }, + targets: [{ target: 'pino-pretty' }] + } + } + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.true; + }); + + it('should not exclude main thread', () => { + mockWorkerThreads({ + isMainThread: true, + workerData: { + filename: '/path/to/worker.js', + workerData: { + $context: { + threadStreamVersion: '2.0.0' + } + } + } + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.false; + }); + + it('should not exclude worker without workerData', () => { + mockWorkerThreads({ + isMainThread: false, + workerData: null + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.false; + }); + + it('should not exclude worker without filename', () => { + mockWorkerThreads({ + isMainThread: false, + workerData: { + workerData: { + $context: { + threadStreamVersion: '2.0.0' + } + } + } + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.false; + }); + + it('should not exclude worker without nested workerData', () => { + mockWorkerThreads({ + isMainThread: false, + workerData: { + filename: '/path/to/worker.js' + } + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.false; + }); + + it('should not exclude worker without $context', () => { + mockWorkerThreads({ + isMainThread: false, + workerData: { + filename: '/path/to/worker.js', + workerData: { + someOtherProperty: 'value' + } + } + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.false; + }); + + it('should not exclude worker without threadStreamVersion', () => { + mockWorkerThreads({ + isMainThread: false, + workerData: { + filename: '/path/to/worker.js', + workerData: { + $context: { + someOtherProperty: 'value' + } + } + } + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.false; + }); + + it('should not exclude non-Pino worker thread', () => { + mockWorkerThreads({ + isMainThread: false, + workerData: { + customData: 'some other worker' + } + }); + + const checkExclusion = require('../../src/util/excludedFromInstrumentation'); + expect(checkExclusion()).to.be.false; + }); + + function mockWorkerThreads(mockData) { + const mockModule = { + exports: mockData + }; + require.cache[require.resolve('worker_threads')] = mockModule; + } + }); + function isExcluded(argv1) { process.argv[1] = argv1; return isExcludedFromInstrumentation();