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
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
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,65 @@ useMicrosoftOpenTelemetry();

That's it — traces, metrics, and logs are collected automatically with built-in instrumentations for HTTP, databases, and more.

### Multiple instances

`useMicrosoftOpenTelemetry()` configures a single, global SDK. If you need several **isolated** SDKs in one process — for example one exporting to Azure Monitor and another to A365, each with its own instrumentations and sampling — use `createMicrosoftOpenTelemetryInstance()` instead. It accepts the same `MicrosoftOpenTelemetryOptions` and returns a handle:

```typescript
import { createMicrosoftOpenTelemetryInstance } from "@microsoft/opentelemetry";

// Instance A -> Azure Monitor, HTTP instrumentation on
const azmon = createMicrosoftOpenTelemetryInstance({
azureMonitor: {
azureMonitorExporterOptions: {
connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
},
},
instrumentationOptions: { http: { enabled: true } },
samplingRatio: 1.0,
});

// Instance B -> A365, HTTP instrumentation off, different sampling
const a365 = createMicrosoftOpenTelemetryInstance({
a365: { enabled: true, tokenResolver: (agentId, tenantId) => getToken(agentId, tenantId) },
instrumentationOptions: { http: { enabled: false } },
samplingRatio: 0.25,
});
```

Each instance owns its own exporter, instrumentation set, and sampler; telemetry from one never reaches another.

#### Routing telemetry to an instance

You do **not** manage OpenTelemetry context yourself. There are two automatic paths and one opt-in path:

1. **Use the handle** — `instance.getTracer(name)`, `instance.getMeter(name)`, and `instance.getLogger(name)` are bound to that instance's pipeline. No context work.
2. **Instrumentations auto-route** — instrumentations you enable on an instance are bound to it at creation, so their telemetry always flows to that instance automatically.
3. **`runWithInstance(fn)`** — only needed when code uses the *global* OpenTelemetry API (e.g. `trace.getTracer(...)` from `@opentelemetry/api`), such as a shared library or a globally-registered third-party instrumentation. Wrap it to choose the target instance:

```typescript
import { trace } from "@opentelemetry/api";

// Global-API code has no handle; steer it to a chosen instance.
azmon.runWithInstance(() => trace.getTracer("shared-lib").startSpan("work").end());
```

The first instance created is the default target for global-API code that isn't wrapped; pass `{ makeDefault: true }` as the second argument to override. The standalone `runWithMicrosoftOpenTelemetryInstance(instance.id, fn)` function is equivalent to the `runWithInstance` method.

> **Metrics note:** synchronous instruments (counter, histogram, etc.) route per measurement, so `.add()`/`.record()` follow the ambient instance. Observable instruments are collected asynchronously outside any `runWithInstance` scope, so they bind to the instance current when the observable was created — create them via that instance's `getMeter(...)` or inside `runWithInstance`.

#### Instance handle

| Member | Description |
| ----------------------------- | ----------------------------------------------------------------------------- |
| `id` | Stable identifier for this instance. |
| `getTracer(name, version?)` | Tracer bound to this instance's pipeline. |
| `getMeter(name, version?)` | Meter bound to this instance's pipeline. |
| `getLogger(name, version?)` | Logger bound to this instance's pipeline. |
| `runWithInstance(fn)` | Run `fn` with this instance bound as the ambient target for the global API. |
| `forceFlush()` | Flush this instance's pipeline. |
| `shutdown()` | Shut down and detach only this instance, leaving other instances active. |

## Configuration

### `MicrosoftOpenTelemetryOptions`
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
Loading
Loading