Skip to content

Commit f2b2a19

Browse files
Fix memory leak: clean up listeners on repeated useAzureMonitor calls (#1493)
* 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: #1415 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix flaky test infrstructure. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8af259e commit f2b2a19

9 files changed

Lines changed: 154 additions & 149 deletions

File tree

.github/workflows/integration.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
node-version: [18.x]
16+
node-version: [20.x]
1717

1818
steps:
19-
- uses: actions/checkout@v2
19+
- uses: actions/checkout@v4
2020
- 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"
2121
- name: Use Node.js ${{ matrix.node-version }}
22-
uses: actions/setup-node@v1
22+
uses: actions/setup-node@v4
2323
with:
2424
node-version: ${{ matrix.node-version }}
2525
- run: npm i

.github/workflows/node.js-linux-arm64.yml

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,27 @@ on:
88

99
jobs:
1010
build:
11-
# Use a standard Ubuntu runner instead of requesting ARM64 hardware directly
12-
runs-on: ubuntu-latest
11+
runs-on: ubuntu-24.04-arm
1312

1413
strategy:
1514
matrix:
16-
# Using the same Node versions as the main workflow
17-
node-version: [18, 20, 22, 24]
15+
node-version: [20.x, 22.x, 24.x]
1816

1917
steps:
20-
- name: Checkout code
21-
uses: actions/checkout@v3
22-
23-
- name: Set up QEMU
24-
uses: docker/setup-qemu-action@v2
25-
with:
26-
platforms: arm64
27-
28-
- name: Run tests in ARM64 Docker container
18+
- uses: actions/checkout@v4
19+
20+
- name: Generate SSL Certificate
2921
run: |
30-
# Generate SSL certificates first (outside container)
3122
mkdir -p ./test/certs
3223
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"
33-
34-
# Set proper permissions for the mounted volume
35-
chmod -R 777 .
36-
37-
# Run the Node.js tests in ARM64 container
38-
docker run --rm -v ${{ github.workspace }}:/app -w /app --platform linux/arm64 node:${{ matrix.node-version }}-alpine sh -c '
39-
# Install build tools needed for native modules
40-
apk add --no-cache python3 make g++
41-
42-
# Clean out directory only (preserve node_modules for fresh install)
43-
rm -rf ./out
44-
45-
# Install dependencies and run tests
46-
npm i
47-
npm run build --if-present
48-
npm run lint
49-
npm test
50-
'
24+
25+
- name: Use Node.js ${{ matrix.node-version }}
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: ${{ matrix.node-version }}
29+
30+
- run: npm run clean
31+
- run: npm i
32+
- run: npm run build --if-present
33+
- run: npm run lint
34+
- run: npm test
Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,26 @@
11
name: Node.js CI (Windows ARM64)
22

3+
# NOTE: GitHub does not offer public Windows ARM64 runners.
4+
# The previous version of this workflow used QEMU to emulate ARM64 Linux in
5+
# Docker, which was neither testing Windows nor reliably passing due to
6+
# emulation flakiness. Native ARM64 testing is now covered by the
7+
# node.js-linux-arm64.yml workflow using ubuntu-24.04-arm runners.
8+
#
9+
# This workflow will be enabled once GitHub provides public Windows ARM64
10+
# runners (or a self-hosted runner is configured).
11+
312
on:
413
push:
514
branches: [ main ]
615
pull_request:
716
branches: [ main ]
817

918
jobs:
10-
build:
11-
# Use the Linux runner instead as it has better Docker support
19+
placeholder:
1220
runs-on: ubuntu-latest
13-
14-
strategy:
15-
matrix:
16-
# Using the same Node versions as the main workflow but without the .x suffix for Docker images
17-
node-version: [18, 20, 22, 24]
18-
1921
steps:
20-
- name: Checkout code
21-
uses: actions/checkout@v3
22-
23-
- name: Set up QEMU
24-
uses: docker/setup-qemu-action@v2
25-
with:
26-
platforms: arm64
27-
# Generate certificates using Linux openssl command
28-
- name: Generate SSL Certificate
29-
run: |
30-
# Create certificates directory
31-
mkdir -p ./test/certs
32-
33-
# Generate SSL certificates
34-
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"
35-
36-
# Set permissions
37-
chmod -R 777 .
38-
39-
- name: Run Node.js ${{ matrix.node-version }} tests in ARM64 Docker container
40-
run: |
41-
# Run the tests in an ARM64 container
42-
docker run --rm -v ${{ github.workspace }}:/app -w /app --platform linux/arm64 node:${{ matrix.node-version }}-alpine sh -c '
43-
echo "Running tests for Node.js ${{ matrix.node-version }} on ARM64 emulation (Windows-targeted tests)"
44-
45-
# Install build dependencies for native modules
46-
apk add --no-cache python3 make g++
47-
48-
# Clean out directory only (preserve node_modules for fresh install)
49-
rm -rf ./out
50-
51-
# Install dependencies and run tests
52-
npm i
53-
npm run build --if-present
54-
npm run lint
55-
npm test
56-
'
22+
- name: Windows ARM64 testing not yet available
23+
run: |
24+
echo "Skipped: GitHub does not offer public Windows ARM64 runners."
25+
echo "ARM64 testing is covered by the Linux ARM64 workflow (ubuntu-24.04-arm)."
26+
echo "See: https://github.com/actions/runner-images#available-images"

.github/workflows/node.js-windows-x86.yml

Lines changed: 0 additions & 66 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Node.js CI (Windows)
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build:
11+
12+
runs-on: windows-latest
13+
14+
strategy:
15+
matrix:
16+
node-version: [20.x, 22.x, 24.x]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
- name: Generate SSL Certificate
21+
shell: pwsh
22+
run: |
23+
$certsDir = ".\test\certs"
24+
if (-not (Test-Path $certsDir)) {
25+
New-Item -ItemType Directory -Path $certsDir
26+
}
27+
28+
$cert = New-SelfSignedCertificate -Subject "CN=ca,OU=Test,O=Root,L=OpenTelemetryTest,ST=RM,C=CL" -NotAfter (Get-Date).AddDays(1)
29+
30+
# Export certificate to PEM format
31+
$certBytes = $cert.Export("Cert")
32+
$pemCert = "-----BEGIN CERTIFICATE-----`r`n" + [Convert]::ToBase64String($certBytes, [System.Base64FormattingOptions]::InsertLineBreaks) + "`r`n-----END CERTIFICATE-----"
33+
Set-Content -Path "$certsDir\server-cert.pem" -Value $pemCert
34+
35+
# Export private key placeholder
36+
$randomBytes = New-Object byte[] 32
37+
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($randomBytes)
38+
$randomKeyContent = [Convert]::ToBase64String($randomBytes)
39+
Set-Content -Path "$certsDir\server-key.pem" -Value "-----BEGIN PRIVATE KEY-----`r`n$randomKeyContent`r`n-----END PRIVATE KEY-----"
40+
41+
- name: Use Node.js ${{ matrix.node-version }}
42+
uses: actions/setup-node@v4
43+
with:
44+
node-version: ${{ matrix.node-version }}
45+
46+
- run: npm run clean
47+
- name: Install dependencies
48+
run: |
49+
npm i
50+
if (!(Test-Path -Path node_modules/diagnostic-channel-publishers)) {
51+
npm i diagnostic-channel-publishers --no-save
52+
}
53+
- run: npm run build --if-present
54+
- run: npm run lint
55+
- name: Run tests with mocks
56+
run: npm run test:mocked

.github/workflows/node.js.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ jobs:
1414
strategy:
1515
matrix:
1616
os: [ubuntu-latest]
17-
node-version: [18.x, 20.x, 22.x, 24.x]
17+
node-version: [20.x, 22.x, 24.x]
1818

1919
steps:
20-
- uses: actions/checkout@v2
20+
- uses: actions/checkout@v4
2121
- 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"
2222
- name: (${{ matrix.os }}) on Node.js ${{ matrix.node-version }}
23-
uses: actions/setup-node@v1
23+
uses: actions/setup-node@v4
2424
with:
2525
node-version: ${{ matrix.node-version }}
2626
- run: npm run clean

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release History
22

3+
### 3.15.0 (Unreleased)
4+
5+
#### Bug Fixes
6+
7+
- 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))
8+
39
### 3.14.0 (2026-02-24)
410

511
#### Other Changes

src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export function useAzureMonitor(options?: AzureMonitorOpenTelemetryOptions) {
5555
options.logRecordProcessors.push(otlpLogProcessor);
5656
}
5757

58+
// Clean up previous instances to prevent listener accumulation on repeated calls
59+
autoCollectLogs?.shutdown();
60+
exceptions?.shutdown();
61+
5862
distroUseAzureMonitor(options);
5963
const logApi = new LogApi(logs.getLogger("ApplicationInsightsLogger"));
6064
autoCollectLogs = new AutoCollectLogs();

test/unitTests/main.tests.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for details.
33
import assert from "assert";
4+
import sinon from "sinon";
45
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
56
import { trace, ProxyTracerProvider } from "@opentelemetry/api";
67
import { logs } from "@opentelemetry/api-logs";
@@ -154,4 +155,54 @@ describe("ApplicationInsightsClient", () => {
154155
});
155156
assert.ok(hasOtlpProcessor, "Should have OTLP trace processor with custom config");
156157
});
158+
159+
it("repeated useAzureMonitor calls should not accumulate process event listeners", () => {
160+
const connString = "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333";
161+
const options = { azureMonitorExporterOptions: { connectionString: connString } };
162+
163+
const uncaughtBefore = process.listenerCount("uncaughtException");
164+
const rejectionBefore = process.listenerCount("unhandledRejection");
165+
166+
useAzureMonitor(options);
167+
const afterFirst = {
168+
uncaught: process.listenerCount("uncaughtException"),
169+
rejection: process.listenerCount("unhandledRejection"),
170+
};
171+
172+
shutdownAzureMonitor();
173+
useAzureMonitor(options);
174+
const afterSecond = {
175+
uncaught: process.listenerCount("uncaughtException"),
176+
rejection: process.listenerCount("unhandledRejection"),
177+
};
178+
179+
assert.strictEqual(
180+
afterSecond.uncaught,
181+
afterFirst.uncaught,
182+
"uncaughtException listeners should not accumulate across repeated useAzureMonitor calls"
183+
);
184+
assert.strictEqual(
185+
afterSecond.rejection,
186+
afterFirst.rejection,
187+
"unhandledRejection listeners should not accumulate across repeated useAzureMonitor calls"
188+
);
189+
190+
// Also test calling useAzureMonitor again WITHOUT shutdown in between
191+
useAzureMonitor(options);
192+
const afterThird = {
193+
uncaught: process.listenerCount("uncaughtException"),
194+
rejection: process.listenerCount("unhandledRejection"),
195+
};
196+
197+
assert.strictEqual(
198+
afterThird.uncaught,
199+
afterFirst.uncaught,
200+
"uncaughtException listeners should not accumulate even without explicit shutdown between calls"
201+
);
202+
assert.strictEqual(
203+
afterThird.rejection,
204+
afterFirst.rejection,
205+
"unhandledRejection listeners should not accumulate even without explicit shutdown between calls"
206+
);
207+
});
157208
});

0 commit comments

Comments
 (0)