diff --git a/src/agent/agentLoader.ts b/src/agent/agentLoader.ts index 0998523c..ecddff37 100644 --- a/src/agent/agentLoader.ts +++ b/src/agent/agentLoader.ts @@ -120,6 +120,8 @@ export class AgentLoader { console.log(msg); return; } + // Detect and report OpenTelemetry globals before attempting to load the agent + this._detectOpenTelemetryGlobals(); if (this._validate()) { try { // Set environment variable to auto attach so the distro is aware of the attach state @@ -195,6 +197,73 @@ export class AgentLoader { } } + private _detectOpenTelemetryGlobals(): void { + try { + const detectedProviders: string[] = []; + + // Check for OpenTelemetry globals directly on the global object + // The OpenTelemetry API stores globals using Symbol.for('opentelemetry.js.api.') + // This avoids calling the API methods which could have side effects + // Try v1 first, then fallback to v2 for future compatibility + const otelSymbolV1 = Symbol.for('opentelemetry.js.api.1'); + const otelSymbolV2 = Symbol.for('opentelemetry.js.api.2'); + const otelGlobal = (global as any)[otelSymbolV1] || (global as any)[otelSymbolV2]; + + if (otelGlobal) { + // Check for registered TracerProvider + if (otelGlobal["trace"]) { + const traceProviderName = otelGlobal["trace"]?.constructor?.name; + // ProxyTracerProvider wraps the real provider - check the delegate + if (traceProviderName === 'ProxyTracerProvider') { + const delegateName = otelGlobal["trace"]?._delegate?.constructor?.name; + if (delegateName && delegateName !== 'NoopTracerProvider') { + detectedProviders.push('TracerProvider'); + } + } else if (traceProviderName && traceProviderName !== 'NoopTracerProvider') { + detectedProviders.push('TracerProvider'); + } + } + + // Check for registered MeterProvider + if (otelGlobal["metrics"] && otelGlobal["metrics"]?.constructor?.name !== 'ProxyMeterProvider' && otelGlobal["metrics"].constructor.name !== 'NoopMeterProvider') { + detectedProviders.push('MeterProvider'); + } + } + + // Check for registered LoggerProvider - uses a different symbol and stores a getter function + const logsSymbol = Symbol.for('io.opentelemetry.js.api.logs'); + const logsGlobal = (global as any)[logsSymbol]; + if (typeof logsGlobal === 'function') { + // logsGlobal is a getter function that takes a version number and returns the provider + // Try both API compatibility versions (1 and 2) to support different @opentelemetry/api-logs versions + let logsProvider = logsGlobal(1); // Try v1 first + if (!logsProvider || logsProvider.constructor?.name === 'NoopLoggerProvider') { + logsProvider = logsGlobal(2); // Try v2 if v1 returns NOOP + } + const loggerProviderName = logsProvider?.constructor?.name; + if ( + loggerProviderName && + loggerProviderName !== 'ProxyLoggerProvider' && + loggerProviderName !== 'NoopLoggerProvider' + ) { + detectedProviders.push('LoggerProvider'); + } + } + + if (detectedProviders.length > 0 && this._diagnosticLogger) { + const msg = `OpenTelemetry global providers detected while using Application Insights auto-attach: ${detectedProviders.join(', ')}. `; + const diagnosticLog = { + message: msg, + messageId: DiagnosticMessageId.openTelemetryConflict + } as IDiagnosticLog; + this._diagnosticLogger.logMessage(diagnosticLog); + } + } + catch (err: any) { + console.log("Error detecting OpenTelemetry globals: " + err); + } + } + private _getAuthenticationCredential(): any { let credential = undefined; // Try to add AAD Token Credential diff --git a/src/agent/types.ts b/src/agent/types.ts index add557f6..0b6fbe4c 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -46,4 +46,5 @@ export const DiagnosticMessageId = { "prefixFailed": "3004", "aadEnabled": "3005", "unknownError": "3006", + "openTelemetryConflict": "3007", } diff --git a/test/unitTests/agent/agentLoader.tests.ts b/test/unitTests/agent/agentLoader.tests.ts index 925c19a7..c7b9c0a9 100644 --- a/test/unitTests/agent/agentLoader.tests.ts +++ b/test/unitTests/agent/agentLoader.tests.ts @@ -7,10 +7,18 @@ import sinon from "sinon"; import { AgentLoader } from "../../../src/agent/agentLoader"; import * as azureMonitor from "@azure/monitor-opentelemetry"; import { DiagnosticMessageId } from "../../../src/agent/types"; +import { trace, metrics } from "@opentelemetry/api"; +import { BasicTracerProvider } from "@opentelemetry/sdk-trace-node"; +import { MeterProvider } from "@opentelemetry/sdk-metrics"; +import { logs as otelLogs } from "@opentelemetry/api-logs"; +import { LoggerProvider } from "@opentelemetry/sdk-logs"; describe("agent/agentLoader", () => { let originalEnv: NodeJS.ProcessEnv; let sandbox: sinon.SinonSandbox; + let originalOtelGlobalV1: any; + let originalOtelGlobalV2: any; + let originalLogsGlobal: any; const defaultConfig = { azureMonitorExporterOptions: { @@ -51,11 +59,32 @@ describe("agent/agentLoader", () => { beforeEach(() => { originalEnv = process.env; + const otelSymbolV1 = Symbol.for('opentelemetry.js.api.1'); + const otelSymbolV2 = Symbol.for('opentelemetry.js.api.2'); + const logsSymbol = Symbol.for('io.opentelemetry.js.api.logs'); + originalOtelGlobalV1 = (global as any)[otelSymbolV1]; + originalOtelGlobalV2 = (global as any)[otelSymbolV2]; + originalLogsGlobal = (global as any)[logsSymbol]; }); afterEach(() => { process.env = originalEnv; sandbox.restore(); + const otelSymbolV1 = Symbol.for('opentelemetry.js.api.1'); + const otelSymbolV2 = Symbol.for('opentelemetry.js.api.2'); + const logsSymbol = Symbol.for('io.opentelemetry.js.api.logs'); + (global as any)[otelSymbolV1] = originalOtelGlobalV1; + (global as any)[otelSymbolV2] = originalOtelGlobalV2; + (global as any)[logsSymbol] = originalLogsGlobal; + if (typeof trace.disable === "function") { + trace.disable(); + } + if (typeof metrics.disable === "function") { + metrics.disable(); + } + if (typeof otelLogs.disable === "function") { + otelLogs.disable(); + } }); it("should initialize constructor", () => { @@ -179,4 +208,45 @@ describe("agent/agentLoader", () => { agent["_validate"](); assert.deepEqual(statusLoggerStub.args[0][0].AgentInitializedSuccessfully, false); }); + + it("should log detected OpenTelemetry tracer and meter providers", () => { + const env = { + ["APPLICATIONINSIGHTS_CONNECTION_STRING"]: "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333", + }; + process.env = env; + const tracerProvider = new BasicTracerProvider(); + trace.setGlobalTracerProvider(tracerProvider); + const meterProvider = new MeterProvider(); + metrics.setGlobalMeterProvider(meterProvider as any); + + const agent = new AgentLoader(); + const diagnosticLoggerStub = sandbox.stub(agent["_diagnosticLogger"], "logMessage"); + + (agent as any)._detectOpenTelemetryGlobals(); + + assert.ok(diagnosticLoggerStub.calledOnce); + const logged = diagnosticLoggerStub.args[0][0]; + assert.strictEqual(logged.messageId, DiagnosticMessageId.openTelemetryConflict); + assert.ok((logged.message as string).includes("TracerProvider")); + assert.ok((logged.message as string).includes("MeterProvider")); + }); + + it("should log detected OpenTelemetry logger provider via logs symbol getter", () => { + const env = { + ["APPLICATIONINSIGHTS_CONNECTION_STRING"]: "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333", + }; + process.env = env; + const loggerProvider = new LoggerProvider(); + (otelLogs as any).setGlobalLoggerProvider(loggerProvider); + + const agent = new AgentLoader(); + const diagnosticLoggerStub = sandbox.stub(agent["_diagnosticLogger"], "logMessage"); + + (agent as any)._detectOpenTelemetryGlobals(); + + assert.ok(diagnosticLoggerStub.calledOnce); + const logged = diagnosticLoggerStub.args[0][0]; + assert.strictEqual(logged.messageId, DiagnosticMessageId.openTelemetryConflict); + assert.ok((logged.message as string).includes("LoggerProvider")); + }); });