Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/agent/agentLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.<major>')
// 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
Expand Down
1 change: 1 addition & 0 deletions src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ export const DiagnosticMessageId = {
"prefixFailed": "3004",
"aadEnabled": "3005",
"unknownError": "3006",
"openTelemetryConflict": "3007",
}
70 changes: 70 additions & 0 deletions test/unitTests/agent/agentLoader.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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"));
});
});
Loading