diff --git a/CHANGELOG.md b/CHANGELOG.md index 437a092..938a017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Bugs Fixed +- Fix duplicate Bunyan logs, missing HTTP duration metrics, duplicate request filtering, and incorrect performance-counter values. [#212](https://github.com/microsoft/opentelemetry-distro-javascript/pull/212) + ## [1.3.0] - 2026-08-03 ### Features Added diff --git a/src/azureMonitor/logs/handler.ts b/src/azureMonitor/logs/handler.ts index 85732be..c1d4818 100644 --- a/src/azureMonitor/logs/handler.ts +++ b/src/azureMonitor/logs/handler.ts @@ -2,15 +2,11 @@ // Licensed under the MIT License. import { AzureMonitorLogExporter } from "@azure/monitor-opentelemetry-exporter"; -import type { Instrumentation } from "@opentelemetry/instrumentation"; -import { BunyanInstrumentation } from "@opentelemetry/instrumentation-bunyan"; -import { WinstonInstrumentation } from "@opentelemetry/instrumentation-winston"; import type { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; import type { InternalConfig } from "../../shared/config.js"; import type { MetricHandler } from "../metrics/handler.js"; import { AzureLogRecordProcessor } from "./logRecordProcessor.js"; import { AzureBatchLogRecordProcessor } from "./batchLogRecordProcessor.js"; -import { logLevelToSeverityNumber } from "../utils/logUtils.js"; /** * Azure Monitor OpenTelemetry Log Handler @@ -21,7 +17,6 @@ export class LogHandler { private _azureBatchLogRecordProcessor: AzureBatchLogRecordProcessor; private _metricHandler: MetricHandler; private _config: InternalConfig; - private _instrumentations: Instrumentation[]; /** * Initializes a new instance of the LogHandler class. @@ -36,8 +31,6 @@ export class LogHandler { enableTraceBasedSamplingForLogs: this._config.enableTraceBasedSamplingForLogs, }); this._azureLogRecordProcessor = new AzureLogRecordProcessor(this._metricHandler); - this._instrumentations = []; - this._initializeInstrumentations(); } public getAzureLogRecordProcessor(): AzureLogRecordProcessor { @@ -47,32 +40,4 @@ export class LogHandler { public getBatchLogRecordProcessor(): BatchLogRecordProcessor { return this._azureBatchLogRecordProcessor; } - - public getInstrumentations(): Instrumentation[] { - return this._instrumentations; - } - - /** - * Start auto collection of telemetry - */ - private _initializeInstrumentations(): void { - const logLevelEnv = process.env.APPLICATIONINSIGHTS_INSTRUMENTATION_LOGGING_LEVEL; - - if (this._config.instrumentationOptions.bunyan?.enabled) { - this._instrumentations.push( - new BunyanInstrumentation({ - ...this._config.instrumentationOptions.bunyan, - logSeverity: logLevelEnv ? logLevelToSeverityNumber(logLevelEnv) : undefined, - }), - ); - } - if (this._config.instrumentationOptions.winston?.enabled) { - this._instrumentations.push( - new WinstonInstrumentation({ - ...this._config.instrumentationOptions.winston, - logSeverity: logLevelEnv ? logLevelToSeverityNumber(logLevelEnv) : undefined, - }), - ); - } - } } diff --git a/src/azureMonitor/metrics/performanceCounters.ts b/src/azureMonitor/metrics/performanceCounters.ts index 81c9a55..1e77c5d 100644 --- a/src/azureMonitor/metrics/performanceCounters.ts +++ b/src/azureMonitor/metrics/performanceCounters.ts @@ -50,7 +50,7 @@ export class PerformanceCounterMetrics { private processTimeGaugeCallback: ObservableCallback; private exceptionCountGauge: ObservableGauge; private exceptionCountGaugeCallback: ObservableCallback; - private lastExceptionRate: { count: number; time: number } = { count: 0, time: 0 }; + private lastExceptionRate: { count: number; time: number }; private totalCount: number = 0; private intervalExecutionTime = 0; private lastRequestRate: { count: number; time: number; executionInterval: number }; @@ -66,6 +66,14 @@ export class PerformanceCounterMetrics { speed: number; times: { user: number; nice: number; sys: number; idle: number; irq: number }; }[]; + // Both process CPU callbacks need independent sampling state. + private lastAppCpuUsageNormalized: { user: number; system: number }; + private lastHrtimeNormalized: number[]; + private lastCpusProcessNormalized: { + model: string; + speed: number; + times: { user: number; nice: number; sys: number; idle: number; irq: number }; + }[]; private totalExceptionCount: number = 0; /** @@ -79,12 +87,16 @@ export class PerformanceCounterMetrics { this.lastCpusProcess = os.cpus(); this.lastAppCpuUsage = process.cpuUsage(); this.lastHrtime = process.hrtime(); + this.lastCpusProcessNormalized = os.cpus(); + this.lastAppCpuUsageNormalized = process.cpuUsage(); + this.lastHrtimeNormalized = process.hrtime(); this.lastRequestRate = { count: this.totalCount, time: +new Date(), executionInterval: this.intervalExecutionTime, }; + this.lastExceptionRate = { count: this.totalExceptionCount, time: +new Date() }; this.azureExporter = new AzureMonitorMetricExporter( this.internalConfig.azureMonitorExporterOptions, @@ -100,8 +112,6 @@ export class PerformanceCounterMetrics { this.meterProvider = new MeterProvider(meterProviderConfig); this.meter = this.meterProvider.getMeter("AzureMonitorPerformanceCountersMeter"); - this.lastRequestRate = { count: 0, time: 0, executionInterval: 0 }; - // Create Instruments this.requestDurationHistogram = this.meter.createHistogram( PerformanceCounterMetricNames.REQUEST_DURATION, @@ -314,8 +324,8 @@ export class PerformanceCounterMetrics { if ( cpus && cpus.length && - this.lastCpusProcess && - cpus.length === this.lastCpusProcess.length + this.lastCpusProcessNormalized && + cpus.length === this.lastCpusProcessNormalized.length ) { // Calculate % of total cpu time (user + system) this App Process used (Only supported by node v6.1.0+) let appCpuPercent: number | undefined = undefined; @@ -323,26 +333,30 @@ export class PerformanceCounterMetrics { const hrtime = process.hrtime(); const totalApp = appCpuUsage.user - - this.lastAppCpuUsage.user + - (appCpuUsage.system - this.lastAppCpuUsage.system) || 0; + this.lastAppCpuUsageNormalized.user + + (appCpuUsage.system - this.lastAppCpuUsageNormalized.system) || 0; - if (typeof this.lastHrtime !== "undefined" && this.lastHrtime.length === 2) { + if ( + typeof this.lastHrtimeNormalized !== "undefined" && + this.lastHrtimeNormalized.length === 2 + ) { const elapsedTime = - (hrtime[0] - this.lastHrtime[0]) * 1e6 + (hrtime[1] - this.lastHrtime[1]) / 1e3 || 0; // convert to microseconds + (hrtime[0] - this.lastHrtimeNormalized[0]) * 1e6 + + (hrtime[1] - this.lastHrtimeNormalized[1]) / 1e3 || 0; // convert to microseconds appCpuPercent = (100 * totalApp) / (elapsedTime * cpus.length); } // Set previous - this.lastAppCpuUsage = appCpuUsage; - this.lastHrtime = hrtime; - const cpuTotals = this.getTotalCombinedCpu(cpus, this.lastCpusProcess); + this.lastAppCpuUsageNormalized = appCpuUsage; + this.lastHrtimeNormalized = hrtime; + const cpuTotals = this.getTotalCombinedCpu(cpus, this.lastCpusProcessNormalized); const value = appCpuPercent !== undefined ? appCpuPercent : (cpuTotals.totalUser / cpuTotals.combinedTotal) * 100; observableResult.observe(value); } - this.lastCpusProcess = cpus; + this.lastCpusProcessNormalized = cpus; } private getProcessTime(observableResult: ObservableResult): void { diff --git a/src/azureMonitor/traces/handler.ts b/src/azureMonitor/traces/handler.ts index 8128341..21334ba 100644 --- a/src/azureMonitor/traces/handler.ts +++ b/src/azureMonitor/traces/handler.ts @@ -1,26 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type { RequestOptions } from "node:http"; -import { createAzureSdkInstrumentation } from "@azure/opentelemetry-instrumentation-azure-sdk"; import { AzureMonitorTraceExporter } from "@azure/monitor-opentelemetry-exporter"; import type { BufferConfig } from "@opentelemetry/sdk-trace-base"; import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; -import type { - HttpInstrumentationConfig, - IgnoreOutgoingRequestFunction, -} from "@opentelemetry/instrumentation-http"; -import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; -import { MongoDBInstrumentation } from "@opentelemetry/instrumentation-mongodb"; -import { MySQLInstrumentation } from "@opentelemetry/instrumentation-mysql"; -import { PgInstrumentation } from "@opentelemetry/instrumentation-pg"; -import { RedisInstrumentation } from "@opentelemetry/instrumentation-redis"; import type { InternalConfig } from "../../shared/config.js"; import type { MetricHandler } from "../metrics/handler.js"; -import { ignoreOutgoingRequestHook } from "../utils/common.js"; import { AzureMonitorSpanProcessor } from "./spanProcessor.js"; -import type { Instrumentation } from "@opentelemetry/instrumentation"; /** * Azure Monitor OpenTelemetry Trace Handler @@ -29,19 +16,17 @@ export class TraceHandler { private _batchSpanProcessor: BatchSpanProcessor; private _azureSpanProcessor: AzureMonitorSpanProcessor; private _azureExporter: AzureMonitorTraceExporter; - private _instrumentations: Instrumentation[]; private _config: InternalConfig; private _metricHandler: MetricHandler; /** * Initializes a new instance of the TraceHandler class. - * @param _config - Configuration. - * @param _metricHandler - MetricHandler. + * @param config - Configuration. + * @param metricHandler - MetricHandler. */ constructor(config: InternalConfig, metricHandler: MetricHandler) { this._config = config; this._metricHandler = metricHandler; - this._instrumentations = []; this._azureExporter = new AzureMonitorTraceExporter(this._config.azureMonitorExporterOptions); const bufferConfig: BufferConfig = { maxExportBatchSize: 512, @@ -51,7 +36,6 @@ export class TraceHandler { }; this._batchSpanProcessor = new BatchSpanProcessor(this._azureExporter, bufferConfig); this._azureSpanProcessor = new AzureMonitorSpanProcessor(this._metricHandler); - this._initializeInstrumentations(); } public getBatchSpanProcessor(): BatchSpanProcessor { @@ -62,10 +46,6 @@ export class TraceHandler { return this._azureSpanProcessor; } - public getInstrumentations(): Instrumentation[] { - return this._instrumentations; - } - /** * Shutdown handler */ @@ -74,61 +54,4 @@ export class TraceHandler { await this._azureSpanProcessor.shutdown(); await this._azureExporter.shutdown(); } - - /** - * Start auto collection of telemetry - */ - private _initializeInstrumentations(): void { - if (this._config.instrumentationOptions.http?.enabled) { - const httpinstrumentationOptions = this._config.instrumentationOptions - .http as HttpInstrumentationConfig; - const providedIgnoreOutgoingRequestHook = - httpinstrumentationOptions.ignoreOutgoingRequestHook; - const mergedIgnoreOutgoingRequestHook: IgnoreOutgoingRequestFunction = ( - request: RequestOptions, - ) => { - const result = ignoreOutgoingRequestHook(request); - if (!result) { - // Not internal call - if (providedIgnoreOutgoingRequestHook) { - // Provided hook in config - return providedIgnoreOutgoingRequestHook(request); - } - } - return result; - }; - httpinstrumentationOptions.ignoreOutgoingRequestHook = mergedIgnoreOutgoingRequestHook; - this._instrumentations.push( - new HttpInstrumentation(this._config.instrumentationOptions.http), - ); - } - if (this._config.instrumentationOptions.azureSdk?.enabled) { - this._instrumentations.push( - createAzureSdkInstrumentation(this._config.instrumentationOptions.azureSdk), - ); - } - if (this._config.instrumentationOptions.mongoDb?.enabled) { - this._instrumentations.push( - new MongoDBInstrumentation(this._config.instrumentationOptions.mongoDb), - ); - } - if (this._config.instrumentationOptions.mySql?.enabled) { - this._instrumentations.push( - new MySQLInstrumentation(this._config.instrumentationOptions.mySql), - ); - } - if (this._config.instrumentationOptions.postgreSql?.enabled) { - this._instrumentations.push( - new PgInstrumentation(this._config.instrumentationOptions.postgreSql), - ); - } - if ( - this._config.instrumentationOptions.redis?.enabled || - this._config.instrumentationOptions.redis4?.enabled - ) { - this._instrumentations.push( - new RedisInstrumentation(this._config.instrumentationOptions.redis), - ); - } - } } diff --git a/test/internal/unit/logs/logHandler.test.ts b/test/internal/unit/logs/logHandler.test.ts index a0b906f..f613366 100644 --- a/test/internal/unit/logs/logHandler.test.ts +++ b/test/internal/unit/logs/logHandler.test.ts @@ -8,6 +8,7 @@ import { ExportResultCode } from "@opentelemetry/core"; import { LoggerProvider } from "@opentelemetry/sdk-logs"; import { LogHandler } from "../../../../src/azureMonitor/logs/index.js"; import { MetricHandler } from "../../../../src/azureMonitor/metrics/index.js"; +import { createInstrumentations } from "../../../../src/distro/instrumentations.js"; import { InternalConfig } from "../../../../src/shared/index.js"; import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; import { SemanticAttributes } from "@opentelemetry/semantic-conventions"; @@ -165,27 +166,60 @@ describe("LogHandler", () => { config.instrumentationOptions.bunyan = { enabled: true, }; - const logHandler = new LogHandler(config, metricHandler); - assert.isTrue(logHandler.getInstrumentations().length > 0, "Log instrumentations not added"); - assert.strictEqual( - logHandler.getInstrumentations()[0].instrumentationName, - "@opentelemetry/instrumentation-bunyan", + const instrumentations = createInstrumentations(config); + assert.isDefined( + instrumentations.find( + (instrumentation) => + instrumentation.instrumentationName === "@opentelemetry/instrumentation-bunyan", + ), "Bunyan instrumentation not added", ); }); - it("should add winston instrumentation", () => { + it("should not create a second copy of the bunyan instrumentation", () => { const config = new InternalConfig(); config.azureMonitorExporterOptions.connectionString = "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"; + config.instrumentationOptions.bunyan = { + enabled: true, + }; config.instrumentationOptions.winston = { enabled: true, }; + config.instrumentationOptions.console = { + enabled: true, + }; + const names = createInstrumentations(config).map( + (instrumentation) => instrumentation.instrumentationName, + ); + assert.deepStrictEqual( + names.filter((name, index) => names.indexOf(name) !== index), + [], + "createInstrumentations returned duplicate instrumentations", + ); + const logHandler = new LogHandler(config, metricHandler); - assert.isTrue(logHandler.getInstrumentations().length > 0, "Log instrumentations not added"); - assert.strictEqual( - logHandler.getInstrumentations()[0].instrumentationName, - "@opentelemetry/instrumentation-winston", + const held = Object.values(logHandler as unknown as Record) + .flatMap((value) => (Array.isArray(value) ? value : [value])) + .filter( + (value) => typeof value === "object" && value !== null && "instrumentationName" in value, + ); + assert.deepStrictEqual(held, [], "LogHandler must not create instrumentations"); + }); + + it("should add winston instrumentation", () => { + const config = new InternalConfig(); + config.azureMonitorExporterOptions.connectionString = + "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"; + config.instrumentationOptions.winston = { + enabled: true, + }; + const instrumentations = createInstrumentations(config); + assert.isDefined( + instrumentations.find( + (instrumentation) => + instrumentation.instrumentationName === "@opentelemetry/instrumentation-winston", + ), "Winston instrumentation not added", ); }); @@ -198,10 +232,12 @@ describe("LogHandler", () => { config.instrumentationOptions.bunyan = { enabled: true, }; - const logHandler = new LogHandler(config, metricHandler); + const bunyanInstrumentation = createInstrumentations(config).find( + (instrumentation) => + instrumentation.instrumentationName === "@opentelemetry/instrumentation-bunyan", + ); assert.strictEqual( - (logHandler.getInstrumentations()[0].getConfig() as BunyanInstrumentationConfig) - .logSeverity, + (bunyanInstrumentation!.getConfig() as BunyanInstrumentationConfig).logSeverity, SeverityNumber.DEBUG, ); }); @@ -214,10 +250,12 @@ describe("LogHandler", () => { config.instrumentationOptions.winston = { enabled: true, }; - const logHandler = new LogHandler(config, metricHandler); + const winstonInstrumentation = createInstrumentations(config).find( + (instrumentation) => + instrumentation.instrumentationName === "@opentelemetry/instrumentation-winston", + ); assert.strictEqual( - (logHandler.getInstrumentations()[0].getConfig() as WinstonInstrumentationConfig) - .logSeverity, + (winstonInstrumentation!.getConfig() as WinstonInstrumentationConfig).logSeverity, SeverityNumber.ERROR, ); }); diff --git a/test/internal/unit/metrics/performanceMetrics.test.ts b/test/internal/unit/metrics/performanceMetrics.test.ts index 2abf880..35733fb 100644 --- a/test/internal/unit/metrics/performanceMetrics.test.ts +++ b/test/internal/unit/metrics/performanceMetrics.test.ts @@ -3,6 +3,7 @@ import type { MockInstance } from "vitest"; import { afterEach, assert, beforeAll, afterAll, describe, it, vi } from "vitest"; import { SpanKind } from "@opentelemetry/api"; +import os from "node:os"; import { ExportResultCode } from "@opentelemetry/core"; import { PerformanceCounterMetrics } from "../../../../src/azureMonitor/metrics/performanceCounters.js"; import { @@ -50,6 +51,7 @@ describe("PerformanceCounterMetricsHandler", () => { const serverSpan: any = { kind: SpanKind.SERVER, duration: [654, 321000000], + events: [{ name: "exception" }], attributes: { "http.status_code": 200, }, @@ -58,6 +60,9 @@ describe("PerformanceCounterMetricsHandler", () => { describe("#Metrics", () => { it("should observe instruments during collection", async () => { + assert.closeTo(autoCollect["lastRequestRate"].time, Date.now(), 5000); + assert.closeTo(autoCollect["lastExceptionRate"].time, Date.now(), 5000); + for (let i = 0; i < 10; i++) { autoCollect.recordSpan(serverSpan); } @@ -94,7 +99,7 @@ describe("PerformanceCounterMetricsHandler", () => { ); assert.deepStrictEqual(metrics[1].descriptor.name, "Request_Rate"); - assert.isTrue((metrics[1].dataPoints[0].value as number) > 0, "Wrong request rate value"); + assert.isTrue((metrics[1].dataPoints[0].value as number) > 1, "Wrong request rate value"); assert.deepStrictEqual(metrics[2].descriptor.name, "Private_Bytes"); assert.isTrue((metrics[2].dataPoints[0].value as number) > 0, "Wrong private bytes value"); assert.deepStrictEqual(metrics[3].descriptor.name, "Available_Bytes"); @@ -119,6 +124,31 @@ describe("PerformanceCounterMetricsHandler", () => { ); assert.isFalse(Number.isNaN(metrics[6].dataPoints[0].value), "Value should not be NaN"); assert.deepStrictEqual(metrics[7].descriptor.name, "Exception_Rate"); + assert.isTrue((metrics[7].dataPoints[0].value as number) > 1, "Wrong exception rate value"); + }); + + it("uses independent state for standard and normalized process CPU", () => { + const cpus = os.cpus(); + autoCollect["lastCpusProcess"] = cpus; + autoCollect["lastCpusProcessNormalized"] = cpus; + autoCollect["lastAppCpuUsage"] = { user: 0, system: 0 }; + autoCollect["lastAppCpuUsageNormalized"] = { user: 0, system: 0 }; + autoCollect["lastHrtime"] = [0, 0]; + autoCollect["lastHrtimeNormalized"] = [0, 0]; + + vi.spyOn(process, "cpuUsage").mockReturnValue({ user: 120000, system: 0 }); + vi.spyOn(process, "hrtime").mockReturnValue([1, 0]); + + const standard: number[] = []; + const normalized: number[] = []; + autoCollect["getProcessTime"]({ observe: (value: number) => standard.push(value) }); + autoCollect["getNormalizedProcessTime"]({ + observe: (value: number) => normalized.push(value), + }); + + assert.strictEqual(standard.length, 1); + assert.strictEqual(normalized.length, 1); + assert.closeTo(normalized[0], standard[0] / cpus.length, 0.0001); }); }); }); diff --git a/test/internal/unit/traces/traceHandler.test.ts b/test/internal/unit/traces/traceHandler.test.ts index bc26f86..eea5951 100644 --- a/test/internal/unit/traces/traceHandler.test.ts +++ b/test/internal/unit/traces/traceHandler.test.ts @@ -5,7 +5,7 @@ import { TraceHandler } from "../../../../src/azureMonitor/traces/index.js"; import { MetricHandler } from "../../../../src/azureMonitor/metrics/index.js"; import { InternalConfig } from "../../../../src/shared/index.js"; import { ApplicationInsightsSampler } from "../../../../src/azureMonitor/traces/sampler.js"; -import { createSampler } from "../../../../src/distro/instrumentations.js"; +import { createSampler, createInstrumentations } from "../../../../src/distro/instrumentations.js"; import { HttpInstrumentation, type HttpInstrumentationConfig, @@ -171,10 +171,12 @@ describe("Library/TraceHandler", () => { _config.instrumentationOptions.http = httpConfig; metricHandler = new MetricHandler(_config); handler = new TraceHandler(_config, metricHandler); - handler.getInstrumentations().forEach((instrumentation) => { - instrumentation.enable(); - activeInstrumentations.push(instrumentation); - }); + createInstrumentations(_config, { filterAzureMonitorRequests: true }).forEach( + (instrumentation) => { + instrumentation.enable(); + activeInstrumentations.push(instrumentation); + }, + ); // Because the instrumentation is registered globally, its config is not updated // when the handler is created. We need to mock the getConfig method to return @@ -354,9 +356,71 @@ describe("Library/TraceHandler", () => { }; metricHandler = new MetricHandler(_config); handler = new TraceHandler(_config, metricHandler); - const instrumentations = handler.getInstrumentations(); + const instrumentations = createInstrumentations(_config); expect(instrumentations).toHaveLength(0); expect(instrumentations[0]).not.toBeInstanceOf(HttpInstrumentation); }); + + it("the trace handler does not create instrumentations", () => { + metricHandler = new MetricHandler(_config); + handler = new TraceHandler(_config, metricHandler); + const held = Object.values(handler as unknown as Record) + .flatMap((value) => (Array.isArray(value) ? value : [value])) + .filter( + (value) => typeof value === "object" && value !== null && "instrumentationName" in value, + ); + expect(held).toEqual([]); + }); + + it("applies the Azure Monitor outgoing request filter exactly once", () => { + const countingRequest = () => { + let reads = 0; + const request = {} as Http.RequestOptions; + Object.defineProperty(request, "headers", { + get() { + reads++; + return { "user-agent": "curl/8.0" }; + }, + }); + return { request, reads: () => reads }; + }; + + const buildHook = ( + withHandler: boolean, + userHook: HttpInstrumentationConfig["ignoreOutgoingRequestHook"], + ) => { + const config = new InternalConfig(); + config.azureMonitorExporterOptions.connectionString = + "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"; + config.instrumentationOptions.http = { + enabled: true, + ignoreOutgoingRequestHook: userHook, + } as HttpInstrumentationConfig; + createInstrumentations(config, { filterAzureMonitorRequests: true }); + if (withHandler) { + metricHandler = new MetricHandler(config); + handler = new TraceHandler(config, metricHandler); + } + return (config.instrumentationOptions.http as HttpInstrumentationConfig) + .ignoreOutgoingRequestHook!; + }; + + const baselineHook = buildHook(false, () => false); + const baseline = countingRequest(); + baselineHook(baseline.request); + + const userHook = vi.fn().mockReturnValue(false); + const hook = buildHook(true, userHook); + + expect( + hook({ headers: { "user-agent": "azsdk-js-monitor-opentelemetry-exporter/1.0" } }), + ).toBe(true); + expect(userHook).not.toHaveBeenCalled(); + + const actual = countingRequest(); + expect(hook(actual.request)).toBe(false); + expect(userHook).toHaveBeenCalledTimes(1); + expect(actual.reads()).toBe(baseline.reads()); + }); }); });