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 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" + ); + }); });