Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
- Bump `@azure/monitor-opentelemetry-exporter` floor to `1.0.0-beta.43`
- Bump `@opentelemetry/*` core/SDK packages to `2.8.0` / `0.219.0`

### Features Added
- Support multiple isolated SDK instances, each with its own instrumentations, settings, and exporter (e.g. different instrumentations for Azure Monitor vs. A365)

## [1.1.0] - 2026-05-29

### Features Added
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@microsoft/applicationinsights-web-snippet": "^1.2.3",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.219.0",
"@opentelemetry/context-async-hooks": "^2.8.0",
"@opentelemetry/core": "^2.8.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.219.0",
Expand Down
74 changes: 4 additions & 70 deletions src/distro/distro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,85 +53,19 @@ import {
SdkStatsFeature,
} from "../types.js";
import { createInstrumentations, createSampler, createViews } from "./instrumentations.js";
import { _applyA365InstrumentationDefaults } from "./instrumentations.js";
import { Logger } from "../shared/logging/index.js";

// Re-export to preserve the existing import path used by tests/consumers.
export { _applyA365InstrumentationDefaults };

process.env["AZURE_MONITOR_DISTRO_VERSION"] = AZURE_MONITOR_OPENTELEMETRY_VERSION;
process.env["MICROSOFT_OPENTELEMETRY_VERSION"] = MICROSOFT_OPENTELEMETRY_VERSION;

let sdk: NodeSDK;
let disposeAzureMonitor: (() => void) | undefined;
let isShutdown = false;

const A365_DISABLED_INSTRUMENTATIONS_BY_DEFAULT: ReadonlyArray<keyof InstrumentationOptions> = [
"http",
"azureSdk",
"mongoDb",
"mySql",
"postgreSql",
"redis",
"redis4",
"bunyan",
"winston",
];

/**
* Redis and redis4 share the same underlying instrumentation. If a caller
* explicitly configures either key, treat both as explicitly configured so
* the other is not inadvertently disabled.
*/
const REDIS_LINKED_KEYS: ReadonlyArray<keyof InstrumentationOptions> = ["redis", "redis4"];

/**
* When A365 export is enabled, default to GenAI-focused telemetry by disabling
* non-GenAI instrumentations unless callers explicitly configure them.
*
* @internal
*/
export function _applyA365InstrumentationDefaults(
instrumentationOptions: InstrumentationOptions,
userInstrumentationOptions: unknown,
a365Enabled: boolean,
): void {
if (!a365Enabled) {
return;
}

const userOptionsRecord =
userInstrumentationOptions && typeof userInstrumentationOptions === "object"
? (userInstrumentationOptions as Record<string, unknown>)
: undefined;

// Pre-compute whether any Redis-linked key was explicitly configured so
// that configuring `redis4` alone does not inadvertently disable `redis`
// (and vice-versa), which would break the underlying shared instrumentation.
const redisLinkedExplicit =
!!userOptionsRecord &&
REDIS_LINKED_KEYS.some((k) => Object.prototype.hasOwnProperty.call(userOptionsRecord, k));

for (const instrumentationKey of A365_DISABLED_INSTRUMENTATIONS_BY_DEFAULT) {
const isExplicitlyConfigured =
!!userOptionsRecord &&
Object.prototype.hasOwnProperty.call(userOptionsRecord, instrumentationKey);

// Treat redis/redis4 as a linked pair: if either was set by the caller,
// skip disabling both keys.
if (
isExplicitlyConfigured ||
(redisLinkedExplicit &&
REDIS_LINKED_KEYS.includes(instrumentationKey as (typeof REDIS_LINKED_KEYS)[number]))
) {
continue;
}

const currentValue = instrumentationOptions[instrumentationKey];
if (currentValue && typeof currentValue === "object") {
(currentValue as Record<string, unknown>).enabled = false;
} else {
instrumentationOptions[instrumentationKey] = { enabled: false };
}
}
}

/**
* Initialize Microsoft OpenTelemetry.
*
Expand Down
5 changes: 5 additions & 0 deletions src/distro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export type {
BrowserSdkLoaderOptions,
A365Options,
} from "./types.js";
export type { MicrosoftOpenTelemetryInstance } from "../types.js";
export { MICROSOFT_OPENTELEMETRY_VERSION } from "./types.js";

export { useMicrosoftOpenTelemetry, shutdownMicrosoftOpenTelemetry } from "./distro.js";
export {
createMicrosoftOpenTelemetryInstance,
runWithMicrosoftOpenTelemetryInstance,
} from "./multiInstance/index.js";
73 changes: 73 additions & 0 deletions src/distro/instrumentations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,83 @@ import type { Instrumentation } from "@opentelemetry/instrumentation";
import type { ViewOptions } from "@opentelemetry/sdk-metrics";

import type { InternalConfig } from "../shared/config.js";
import type { InstrumentationOptions } from "../types.js";
import { ignoreOutgoingRequestHook } from "../azureMonitor/utils/common.js";
import { ApplicationInsightsSampler } from "../azureMonitor/traces/sampler.js";
import { logLevelToSeverityNumber } from "../azureMonitor/utils/logUtils.js";

// ── A365 instrumentation defaults ───────────────────────────────────

const A365_DISABLED_INSTRUMENTATIONS_BY_DEFAULT: ReadonlyArray<keyof InstrumentationOptions> = [
"http",
"azureSdk",
"mongoDb",
"mySql",
"postgreSql",
"redis",
"redis4",
"bunyan",
"winston",
];

/**
* Redis and redis4 share the same underlying instrumentation. If a caller
* explicitly configures either key, treat both as explicitly configured so
* the other is not inadvertently disabled.
*/
const REDIS_LINKED_KEYS: ReadonlyArray<keyof InstrumentationOptions> = ["redis", "redis4"];

/**
* When A365 export is enabled, default to GenAI-focused telemetry by disabling
* non-GenAI instrumentations unless callers explicitly configure them.
*
* @internal
*/
export function _applyA365InstrumentationDefaults(
instrumentationOptions: InstrumentationOptions,
userInstrumentationOptions: unknown,
a365Enabled: boolean,
): void {
if (!a365Enabled) {
return;
}

const userOptionsRecord =
userInstrumentationOptions && typeof userInstrumentationOptions === "object"
? (userInstrumentationOptions as Record<string, unknown>)
: undefined;

// Pre-compute whether any Redis-linked key was explicitly configured so
// that configuring `redis4` alone does not inadvertently disable `redis`
// (and vice-versa), which would break the underlying shared instrumentation.
const redisLinkedExplicit =
!!userOptionsRecord &&
REDIS_LINKED_KEYS.some((k) => Object.prototype.hasOwnProperty.call(userOptionsRecord, k));

for (const instrumentationKey of A365_DISABLED_INSTRUMENTATIONS_BY_DEFAULT) {
const isExplicitlyConfigured =
!!userOptionsRecord &&
Object.prototype.hasOwnProperty.call(userOptionsRecord, instrumentationKey);

// Treat redis/redis4 as a linked pair: if either was set by the caller,
// skip disabling both keys.
if (
isExplicitlyConfigured ||
(redisLinkedExplicit &&
REDIS_LINKED_KEYS.includes(instrumentationKey as (typeof REDIS_LINKED_KEYS)[number]))
) {
continue;
}

const currentValue = instrumentationOptions[instrumentationKey];
if (currentValue && typeof currentValue === "object") {
(currentValue as Record<string, unknown>).enabled = false;
} else {
instrumentationOptions[instrumentationKey] = { enabled: false };
}
}
}

// ── Instrumentations ────────────────────────────────────────────────

/**
Expand Down
168 changes: 168 additions & 0 deletions src/distro/multiInstance/delegatingProviders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type {
Tracer,
TracerProvider,
Span,
SpanOptions,
Context,
Meter,
MeterProvider,
MeterOptions,
MetricOptions,
BatchObservableCallback,
Observable,
Counter,
UpDownCounter,
Gauge,
Histogram,
ObservableGauge,
ObservableCounter,
ObservableUpDownCounter,
} from "@opentelemetry/api";
import { createNoopMeter, ProxyTracerProvider } from "@opentelemetry/api";
import type { Logger, LoggerProvider, LoggerOptions, LogRecord } from "@opentelemetry/api-logs";
import { createNoopLogger } from "@opentelemetry/api-logs";

import { resolveInstanceProviders } from "./instanceRegistry.js";

// Shared fallbacks used when no instance is registered/resolved yet. They are
// no-ops so that early or out-of-band global API access never throws.
const NOOP_TRACER_PROVIDER = new ProxyTracerProvider();
const NOOP_METER = createNoopMeter();
const NOOP_LOGGER = createNoopLogger();

/**
* A Tracer that resolves the current instance's tracer on every call. Resolution
* MUST be per-call (never cached) because the ambient instance changes with the
* active context.
*/
class DelegatingTracer implements Tracer {
constructor(
private readonly name: string,
private readonly version?: string,
private readonly options?: { schemaUrl?: string },
) {}

private delegate(): Tracer {
const providers = resolveInstanceProviders();
const provider: TracerProvider = providers?.tracerProvider ?? NOOP_TRACER_PROVIDER;
return provider.getTracer(this.name, this.version, this.options);
}

startSpan(name: string, options?: SpanOptions, context?: Context): Span {
return this.delegate().startSpan(name, options, context);
}

// The api defines several overloads for startActiveSpan; forward all args.
startActiveSpan<F extends (span: Span) => unknown>(name: string, fn: F): ReturnType<F>;
startActiveSpan<F extends (span: Span) => unknown>(
name: string,
options: SpanOptions,
fn: F,
): ReturnType<F>;
startActiveSpan<F extends (span: Span) => unknown>(
name: string,
options: SpanOptions,
context: Context,
fn: F,
): ReturnType<F>;
startActiveSpan(name: string, ...args: unknown[]): unknown {
return (this.delegate().startActiveSpan as (...a: unknown[]) => unknown)(name, ...args);
}
}

/**
* Global parent TracerProvider registered once. It owns no pipeline itself; it
* delegates to the resolved child instance's TracerProvider.
*/
export class ParentTracerProvider implements TracerProvider {
getTracer(name: string, version?: string, options?: { schemaUrl?: string }): Tracer {
return new DelegatingTracer(name, version, options);
}
}

/** A Meter that resolves the current instance's meter on every instrument call. */
class DelegatingMeter implements Meter {
constructor(
private readonly name: string,
private readonly version?: string,
private readonly options?: MeterOptions,
) {}

private delegate(): Meter {
const providers = resolveInstanceProviders();
const provider: MeterProvider | undefined = providers?.meterProvider;
return provider ? provider.getMeter(this.name, this.version, this.options) : NOOP_METER;
}

createGauge(name: string, options?: MetricOptions): Gauge {
return this.delegate().createGauge(name, options);
}
createHistogram(name: string, options?: MetricOptions): Histogram {
return this.delegate().createHistogram(name, options);
}
createCounter(name: string, options?: MetricOptions): Counter {
return this.delegate().createCounter(name, options);
}
createUpDownCounter(name: string, options?: MetricOptions): UpDownCounter {
return this.delegate().createUpDownCounter(name, options);
}
createObservableGauge(name: string, options?: MetricOptions): ObservableGauge {
return this.delegate().createObservableGauge(name, options);
}
createObservableCounter(name: string, options?: MetricOptions): ObservableCounter {
return this.delegate().createObservableCounter(name, options);
}
createObservableUpDownCounter(name: string, options?: MetricOptions): ObservableUpDownCounter {
return this.delegate().createObservableUpDownCounter(name, options);
}
addBatchObservableCallback(callback: BatchObservableCallback, observables: Observable[]): void {
this.delegate().addBatchObservableCallback(callback, observables);
}
removeBatchObservableCallback(
callback: BatchObservableCallback,
observables: Observable[],
): void {
this.delegate().removeBatchObservableCallback(callback, observables);
}
Comment thread
JacksonWeber marked this conversation as resolved.
Outdated
}

/** Global parent MeterProvider registered once; delegates to the resolved child. */
export class ParentMeterProvider implements MeterProvider {
getMeter(name: string, version?: string, options?: MeterOptions): Meter {
return new DelegatingMeter(name, version, options);
}
}

/** A Logger that resolves the current instance's logger on every emit. */
class DelegatingLogger implements Logger {
constructor(
private readonly name: string,
private readonly version?: string,
private readonly options?: LoggerOptions,
) {}

private delegate(): Logger {
const providers = resolveInstanceProviders();
return providers
? providers.loggerProvider.getLogger(this.name, this.version, this.options)
: NOOP_LOGGER;
}

emit(logRecord: LogRecord): void {
this.delegate().emit(logRecord);
}

enabled(options?: Parameters<Logger["enabled"]>[0]): boolean {
return this.delegate().enabled(options);
}
}

/** Global parent LoggerProvider registered once; delegates to the resolved child. */
export class ParentLoggerProvider implements LoggerProvider {
getLogger(name: string, version?: string, options?: LoggerOptions): Logger {
return new DelegatingLogger(name, version, options);
}
}
Loading
Loading