From 17dc3d3da97c56616a9aff1dd76cc8e20c660878 Mon Sep 17 00:00:00 2001 From: Jackson Weber Date: Mon, 23 Mar 2026 12:53:23 -0700 Subject: [PATCH 1/2] Fix memory leak: clean up listeners on repeated useAzureMonitor calls When useAzureMonitor() is called multiple times, old AutoCollectExceptions and AutoCollectLogs instances were silently overwritten without cleanup, causing process event listener accumulation and retaining old LogApi instances (and their downstream spans/connection metadata) in memory. This adds shutdown of previous instances before re-initialization and a test verifying listeners do not accumulate. Fixes: https://github.com/microsoft/ApplicationInsights-node.js/issues/1415 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 +++++ src/main.ts | 4 +++ test/unitTests/main.tests.ts | 51 ++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa1aa088..ca5699ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +### 3.15.0 (Unreleased) + +#### Bug Fixes + +- Fix memory leak caused by process event listener accumulation when `useAzureMonitor()` is called multiple times. ([#1415](https://github.com/microsoft/ApplicationInsights-node.js/issues/1415)) + ### 3.14.0 (2026-02-24) #### Other Changes diff --git a/src/main.ts b/src/main.ts index b3368487..0d0a4588 100644 --- a/src/main.ts +++ b/src/main.ts @@ -55,6 +55,10 @@ export function useAzureMonitor(options?: AzureMonitorOpenTelemetryOptions) { options.logRecordProcessors.push(otlpLogProcessor); } + // Clean up previous instances to prevent listener accumulation on repeated calls + autoCollectLogs?.shutdown(); + exceptions?.shutdown(); + distroUseAzureMonitor(options); const logApi = new LogApi(logs.getLogger("ApplicationInsightsLogger")); autoCollectLogs = new AutoCollectLogs(); diff --git a/test/unitTests/main.tests.ts b/test/unitTests/main.tests.ts index 059c33e5..ddafc95f 100644 --- a/test/unitTests/main.tests.ts +++ b/test/unitTests/main.tests.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import assert from "assert"; +import sinon from "sinon"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { trace, ProxyTracerProvider } from "@opentelemetry/api"; import { logs } from "@opentelemetry/api-logs"; @@ -154,4 +155,54 @@ describe("ApplicationInsightsClient", () => { }); assert.ok(hasOtlpProcessor, "Should have OTLP trace processor with custom config"); }); + + it("repeated useAzureMonitor calls should not accumulate process event listeners", () => { + const connString = "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"; + const options = { azureMonitorExporterOptions: { connectionString: connString } }; + + const uncaughtBefore = process.listenerCount("uncaughtException"); + const rejectionBefore = process.listenerCount("unhandledRejection"); + + useAzureMonitor(options); + const afterFirst = { + uncaught: process.listenerCount("uncaughtException"), + rejection: process.listenerCount("unhandledRejection"), + }; + + shutdownAzureMonitor(); + useAzureMonitor(options); + const afterSecond = { + uncaught: process.listenerCount("uncaughtException"), + rejection: process.listenerCount("unhandledRejection"), + }; + + assert.strictEqual( + afterSecond.uncaught, + afterFirst.uncaught, + "uncaughtException listeners should not accumulate across repeated useAzureMonitor calls" + ); + assert.strictEqual( + afterSecond.rejection, + afterFirst.rejection, + "unhandledRejection listeners should not accumulate across repeated useAzureMonitor calls" + ); + + // Also test calling useAzureMonitor again WITHOUT shutdown in between + useAzureMonitor(options); + const afterThird = { + uncaught: process.listenerCount("uncaughtException"), + rejection: process.listenerCount("unhandledRejection"), + }; + + assert.strictEqual( + afterThird.uncaught, + afterFirst.uncaught, + "uncaughtException listeners should not accumulate even without explicit shutdown between calls" + ); + assert.strictEqual( + afterThird.rejection, + afterFirst.rejection, + "unhandledRejection listeners should not accumulate even without explicit shutdown between calls" + ); + }); }); From 5ada042e64e0f6ebab84f9f8947fc0ac2fbdebed Mon Sep 17 00:00:00 2001 From: Jackson Weber Date: Mon, 23 Mar 2026 13:08:19 -0700 Subject: [PATCH 2/2] Fix flaky test infrstructure. --- .github/workflows/integration.yml | 6 +- .github/workflows/node.js-linux-arm64.yml | 48 +++++---------- .github/workflows/node.js-windows-arm64.yml | 60 +++++-------------- .github/workflows/node.js-windows-x86.yml | 66 --------------------- .github/workflows/node.js-windows.yml | 56 +++++++++++++++++ .github/workflows/node.js.yml | 6 +- 6 files changed, 93 insertions(+), 149 deletions(-) delete mode 100644 .github/workflows/node.js-windows-x86.yml create mode 100644 .github/workflows/node.js-windows.yml diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 408990b2..53514eb5 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -13,13 +13,13 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: openssl req -x509 -nodes -newkey rsa -keyout ./test/certs/server-key.pem -out ./test/certs/server-cert.pem -days 1 -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Root/OU=Test/CN=ca" - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm i diff --git a/.github/workflows/node.js-linux-arm64.yml b/.github/workflows/node.js-linux-arm64.yml index c082b093..343b0b95 100644 --- a/.github/workflows/node.js-linux-arm64.yml +++ b/.github/workflows/node.js-linux-arm64.yml @@ -8,43 +8,27 @@ on: jobs: build: - # Use a standard Ubuntu runner instead of requesting ARM64 hardware directly - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm strategy: matrix: - # Using the same Node versions as the main workflow - node-version: [18, 20, 22, 24] + node-version: [20.x, 22.x, 24.x] steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - with: - platforms: arm64 - - - name: Run tests in ARM64 Docker container + - uses: actions/checkout@v4 + + - name: Generate SSL Certificate run: | - # Generate SSL certificates first (outside container) mkdir -p ./test/certs openssl req -x509 -nodes -newkey rsa:2048 -keyout ./test/certs/server-key.pem -out ./test/certs/server-cert.pem -days 1 -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Root/OU=Test/CN=ca" - - # Set proper permissions for the mounted volume - chmod -R 777 . - - # Run the Node.js tests in ARM64 container - docker run --rm -v ${{ github.workspace }}:/app -w /app --platform linux/arm64 node:${{ matrix.node-version }}-alpine sh -c ' - # Install build tools needed for native modules - apk add --no-cache python3 make g++ - - # Clean out directory only (preserve node_modules for fresh install) - rm -rf ./out - - # Install dependencies and run tests - npm i - npm run build --if-present - npm run lint - npm test - ' + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - run: npm run clean + - run: npm i + - run: npm run build --if-present + - run: npm run lint + - run: npm test diff --git a/.github/workflows/node.js-windows-arm64.yml b/.github/workflows/node.js-windows-arm64.yml index 5d123829..c0295dfe 100644 --- a/.github/workflows/node.js-windows-arm64.yml +++ b/.github/workflows/node.js-windows-arm64.yml @@ -1,5 +1,14 @@ name: Node.js CI (Windows ARM64) +# NOTE: GitHub does not offer public Windows ARM64 runners. +# The previous version of this workflow used QEMU to emulate ARM64 Linux in +# Docker, which was neither testing Windows nor reliably passing due to +# emulation flakiness. Native ARM64 testing is now covered by the +# node.js-linux-arm64.yml workflow using ubuntu-24.04-arm runners. +# +# This workflow will be enabled once GitHub provides public Windows ARM64 +# runners (or a self-hosted runner is configured). + on: push: branches: [ main ] @@ -7,50 +16,11 @@ on: branches: [ main ] jobs: - build: - # Use the Linux runner instead as it has better Docker support + placeholder: runs-on: ubuntu-latest - - strategy: - matrix: - # Using the same Node versions as the main workflow but without the .x suffix for Docker images - node-version: [18, 20, 22, 24] - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - with: - platforms: arm64 - # Generate certificates using Linux openssl command - - name: Generate SSL Certificate - run: | - # Create certificates directory - mkdir -p ./test/certs - - # Generate SSL certificates - openssl req -x509 -nodes -newkey rsa:2048 -keyout ./test/certs/server-key.pem -out ./test/certs/server-cert.pem -days 1 -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Root/OU=Test/CN=ca" - - # Set permissions - chmod -R 777 . - - - name: Run Node.js ${{ matrix.node-version }} tests in ARM64 Docker container - run: | - # Run the tests in an ARM64 container - docker run --rm -v ${{ github.workspace }}:/app -w /app --platform linux/arm64 node:${{ matrix.node-version }}-alpine sh -c ' - echo "Running tests for Node.js ${{ matrix.node-version }} on ARM64 emulation (Windows-targeted tests)" - - # Install build dependencies for native modules - apk add --no-cache python3 make g++ - - # Clean out directory only (preserve node_modules for fresh install) - rm -rf ./out - - # Install dependencies and run tests - npm i - npm run build --if-present - npm run lint - npm test - ' + - name: Windows ARM64 testing not yet available + run: | + echo "Skipped: GitHub does not offer public Windows ARM64 runners." + echo "ARM64 testing is covered by the Linux ARM64 workflow (ubuntu-24.04-arm)." + echo "See: https://github.com/actions/runner-images#available-images" diff --git a/.github/workflows/node.js-windows-x86.yml b/.github/workflows/node.js-windows-x86.yml deleted file mode 100644 index 46e62b0c..00000000 --- a/.github/workflows/node.js-windows-x86.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Node.js CI (Windows x86) - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: windows-latest - - strategy: - matrix: - # Using the same Node versions as the main workflow - node-version: [18.x, 20.x, 22.x, 24.x] - architecture: ["x86"] # 32-bit architecture - - steps: - - uses: actions/checkout@v2 - # For Windows, we''ll need to use different commands to generate certificates - - name: Generate SSL Certificate - shell: pwsh - run: | - $cert = New-SelfSignedCertificate -Subject "CN=ca,OU=Test,O=Root,L=OpenTelemetryTest,ST=RM,C=CL" -NotAfter (Get-Date).AddDays(1) - $certPath = ".\test\certs\server-cert.pem" - $keyPath = ".\test\certs\server-key.pem" - - $certsDir = ".\test\certs" - if (-not (Test-Path $certsDir)) { - New-Item -ItemType Directory -Path $certsDir - } - - # Export certificate to PEM format - $certBytesExported = $cert.Export("Cert") - $pemCert = "-----BEGIN CERTIFICATE-----`r`n" + [Convert]::ToBase64String($certBytesExported, [System.Base64FormattingOptions]::InsertLineBreaks) + "`r`n-----END CERTIFICATE-----" - Set-Content -Path $certPath -Value $pemCert - - # For the key, we''ll output a placeholder PEM file - # Using secure random bytes for the key content rather than hardcoded text - $randomBytes = New-Object byte[] 32 - [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($randomBytes) - $randomKeyContent = [Convert]::ToBase64String($randomBytes) - Set-Content -Path $keyPath -Value "-----BEGIN PRIVATE KEY-----`r`n$randomKeyContent`r`n-----END PRIVATE KEY-----" - - - name: (Windows x86) on Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - architecture: ${{ matrix.architecture }} # Specify x86 architecture - - - run: npm run clean - - name: Install dependencies - run: | - npm i - # Verify diagnostic-channel-publishers is properly installed - if (!(Test-Path -Path node_modules/diagnostic-channel-publishers)) { - npm i diagnostic-channel-publishers --no-save - } - - run: npm run build --if-present - - run: npm run lint - - name: Run tests with mocks - run: | - # Run tests with mock setup to prevent any real network connections - npm run test:mocked diff --git a/.github/workflows/node.js-windows.yml b/.github/workflows/node.js-windows.yml new file mode 100644 index 00000000..bb6e948b --- /dev/null +++ b/.github/workflows/node.js-windows.yml @@ -0,0 +1,56 @@ +name: Node.js CI (Windows) + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: windows-latest + + strategy: + matrix: + node-version: [20.x, 22.x, 24.x] + + steps: + - uses: actions/checkout@v4 + - name: Generate SSL Certificate + shell: pwsh + run: | + $certsDir = ".\test\certs" + if (-not (Test-Path $certsDir)) { + New-Item -ItemType Directory -Path $certsDir + } + + $cert = New-SelfSignedCertificate -Subject "CN=ca,OU=Test,O=Root,L=OpenTelemetryTest,ST=RM,C=CL" -NotAfter (Get-Date).AddDays(1) + + # Export certificate to PEM format + $certBytes = $cert.Export("Cert") + $pemCert = "-----BEGIN CERTIFICATE-----`r`n" + [Convert]::ToBase64String($certBytes, [System.Base64FormattingOptions]::InsertLineBreaks) + "`r`n-----END CERTIFICATE-----" + Set-Content -Path "$certsDir\server-cert.pem" -Value $pemCert + + # Export private key placeholder + $randomBytes = New-Object byte[] 32 + [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($randomBytes) + $randomKeyContent = [Convert]::ToBase64String($randomBytes) + Set-Content -Path "$certsDir\server-key.pem" -Value "-----BEGIN PRIVATE KEY-----`r`n$randomKeyContent`r`n-----END PRIVATE KEY-----" + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - run: npm run clean + - name: Install dependencies + run: | + npm i + if (!(Test-Path -Path node_modules/diagnostic-channel-publishers)) { + npm i diagnostic-channel-publishers --no-save + } + - run: npm run build --if-present + - run: npm run lint + - name: Run tests with mocks + run: npm run test:mocked diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index a67aa889..e65e1fa2 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -14,13 +14,13 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node-version: [18.x, 20.x, 22.x, 24.x] + node-version: [20.x, 22.x, 24.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: openssl req -x509 -nodes -newkey rsa -keyout ./test/certs/server-key.pem -out ./test/certs/server-cert.pem -days 1 -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Root/OU=Test/CN=ca" - name: (${{ matrix.os }}) on Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm run clean