Skip to content
Merged
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
388 changes: 388 additions & 0 deletions A365_IMPLEMENTATION_PLAN.md

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions src/_a365/configuration/A365Configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type {
A365Options,
ClusterCategory,
A365BaggageOptions,
A365HostingOptions,
} from "./A365ConfigurationOptions.js";
import { getEnv } from "../utils/utils.js";
import { Logger } from "../../shared/logging/index.js";
import { JsonConfig } from "../../shared/jsonConfig.js";

/**
* Environment variable names for A365 configuration.
* These follow the MICROSOFT_OTEL_A365_* convention defined in PLANNING.md.
*/
export const A365_ENV_VARS = {
EXPORTER_ENABLED: "MICROSOFT_OTEL_A365_EXPORTER_ENABLED",
PER_REQUEST_EXPORT: "MICROSOFT_OTEL_A365_PER_REQUEST_EXPORT",
AUTH_SCOPES: "MICROSOFT_OTEL_A365_AUTH_SCOPES",
DOMAIN: "MICROSOFT_OTEL_A365_DOMAIN",
CLUSTER_CATEGORY: "MICROSOFT_OTEL_A365_CLUSTER_CATEGORY",
} as const;

const DEFAULT_AUTH_SCOPE = "https://api.powerplatform.com/.default";

const VALID_CLUSTER_CATEGORIES: ReadonlySet<string> = new Set([
"local", "dev", "test", "preprod", "firstrelease",
"prod", "gov", "high", "dod", "mooncake", "ex", "rx",
]);

/**
* Resolved A365 configuration.
*
* Merges values from four sources (lowest to highest precedence):
* 1. Defaults
* 2. Programmatic options (`A365Options`)
* 3. JSON config (`applicationinsights.json` → `a365` key)
* 4. Environment variables (`MICROSOFT_OTEL_A365_*`)
*/
export class A365Configuration {
/** Whether A365 observability is enabled. */
public readonly enabled: boolean;

/** Token resolver callback for A365 service authentication. */
public readonly tokenResolver?: (agentId: string, tenantId: string) => string | Promise<string>;

/** Cluster category. */
public readonly clusterCategory: ClusterCategory;

/** Domain override for the A365 observability service. */
public readonly domainOverride?: string;

/** OAuth scopes for A365 service authentication. */
public readonly authScopes: string[];

/** Whether to use per-request export mode. */
public readonly perRequestExport: boolean;

/** Baggage options. */
public readonly baggage: Required<A365BaggageOptions>;

/** Hosting options. */
public readonly hosting: Required<A365HostingOptions>;

constructor(options?: A365Options) {
// 1. Set defaults
let enabled = false;
let clusterCategory: ClusterCategory = "prod";
let domainOverride: string | undefined = options?.domainOverride;
let authScopes: string[] = options?.authScopes ?? [DEFAULT_AUTH_SCOPE];
let perRequestExport = false;

// 2. Apply programmatic options
if (options) {
enabled = options.enabled ?? enabled;
clusterCategory = options.clusterCategory ?? clusterCategory;
perRequestExport = options.perRequestExport ?? perRequestExport;
}

// 3. Apply JSON config (takes precedence over programmatic options)
const jsonA365 = JsonConfig.getInstance().a365;
if (jsonA365) {
enabled = jsonA365.enabled ?? enabled;
clusterCategory = jsonA365.clusterCategory ?? clusterCategory;
domainOverride = jsonA365.domainOverride ?? domainOverride;
perRequestExport = jsonA365.perRequestExport ?? perRequestExport;
if (jsonA365.authScopes) {
authScopes = jsonA365.authScopes;
}
}

// 4. Apply environment variable overrides (highest precedence)
if (getEnv(A365_ENV_VARS.EXPORTER_ENABLED) === "true") {
enabled = true;
} else if (getEnv(A365_ENV_VARS.EXPORTER_ENABLED) === "false") {
enabled = false;
}

if (getEnv(A365_ENV_VARS.PER_REQUEST_EXPORT) === "true") {
perRequestExport = true;
} else if (getEnv(A365_ENV_VARS.PER_REQUEST_EXPORT) === "false") {
perRequestExport = false;
}

const envScopes = getEnv(A365_ENV_VARS.AUTH_SCOPES);
if (envScopes) {
authScopes = envScopes.split(",").map((s) => s.trim()).filter(Boolean);
}

const envDomain = getEnv(A365_ENV_VARS.DOMAIN);
if (envDomain) {
domainOverride = envDomain;
}

const envCluster = getEnv(A365_ENV_VARS.CLUSTER_CATEGORY);
if (envCluster && VALID_CLUSTER_CATEGORIES.has(envCluster)) {
clusterCategory = envCluster as ClusterCategory;
}

// Assign resolved values
this.enabled = enabled;
this.tokenResolver = options?.tokenResolver;
this.clusterCategory = clusterCategory;
this.domainOverride = domainOverride;
this.authScopes = authScopes;
this.perRequestExport = perRequestExport;

this.baggage = {
propagationEnabled: jsonA365?.baggage?.propagationEnabled
?? options?.baggage?.propagationEnabled ?? true,
enrichSpans: jsonA365?.baggage?.enrichSpans
?? options?.baggage?.enrichSpans ?? true,
};

this.hosting = {
enabled: jsonA365?.hosting?.enabled ?? options?.hosting?.enabled ?? false,
};

// Warn when A365-scoped options are set but A365 is not enabled
if (!this.enabled) {
this._warnIfOptionsSetButDisabled(options);
}
}

private _warnIfOptionsSetButDisabled(options?: A365Options): void {
if (!options) return;

const hasNonTrivialOptions =
options.tokenResolver !== undefined ||
options.domainOverride !== undefined ||
options.perRequestExport !== undefined ||
options.hosting?.enabled === true;

if (hasNonTrivialOptions) {
Logger.getInstance().warn(
"A365 configuration options are set but A365 is not enabled. " +
"Set `a365.enabled: true` or `MICROSOFT_OTEL_A365_EXPORTER_ENABLED=true` to enable.",
);
}
}
}
77 changes: 77 additions & 0 deletions src/_a365/configuration/A365ConfigurationOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Cluster categories for A365 service endpoint resolution.
* Mirrors the ClusterCategory enum from Agent365-nodejs.
*/
export type ClusterCategory =
| "local"
| "dev"
| "test"
| "preprod"
| "firstrelease"
| "prod"
| "gov"
| "high"
| "dod"
| "mooncake"
| "ex"
| "rx";

/**
* A365 observability configuration options.
*
* These are the public-facing options passed via the `a365` scope
* of {@link MicrosoftOpenTelemetryOptions}.
*/
export interface A365Options {
/** Enable A365 observability. When false, no A365 components are created. */
enabled?: boolean;

/**
* Token resolver for authenticating with the A365 observability service.
* Called with (agentId, tenantId) extracted from span attributes.
* Must return a bearer token string or a promise resolving to one.
*/
tokenResolver?: (agentId: string, tenantId: string) => string | Promise<string>;

/** Cluster category for the A365 service endpoint. */
clusterCategory?: ClusterCategory;

/** Override the A365 observability service domain. */
domainOverride?: string;

/** OAuth scopes for A365 service authentication. */
authScopes?: string[];

/**
* Use per-request export mode (buffer spans per trace, export on root completion).
* When false, uses standard batch export.
* @default false
*/
perRequestExport?: boolean;

/** Baggage propagation and span enrichment options. */
baggage?: A365BaggageOptions;

/** Hosting middleware options (requires @microsoft/agents-hosting). */
hosting?: A365HostingOptions;
}

/** Baggage propagation and span enrichment options. */
export interface A365BaggageOptions {
/** Enable baggage propagation from request headers to span context. */
propagationEnabled?: boolean;
/** Copy baggage items to span attributes. */
enrichSpans?: boolean;
}

/** Hosting middleware options. */
export interface A365HostingOptions {
/**
* Enable hosting middleware integration (baggage middleware, output logging, etc.).
* Requires `@microsoft/agents-hosting` as an optional peer dependency.
*/
enabled?: boolean;
}
10 changes: 10 additions & 0 deletions src/_a365/configuration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export { A365Configuration, A365_ENV_VARS } from "./A365Configuration.js";
export type {
A365Options,
ClusterCategory,
A365BaggageOptions,
A365HostingOptions,
} from "./A365ConfigurationOptions.js";
10 changes: 10 additions & 0 deletions src/_a365/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export { A365Configuration, A365_ENV_VARS } from "./configuration/index.js";
export type {
A365Options,
ClusterCategory,
A365BaggageOptions,
A365HostingOptions,
} from "./configuration/index.js";
11 changes: 11 additions & 0 deletions src/_a365/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Read an environment variable, trimmed and lowercased.
* Returns undefined if not set or empty.
*/
export function getEnv(name: string): string | undefined {
const value = process.env[name]?.trim().toLowerCase();
return value || undefined;
}
1 change: 1 addition & 0 deletions src/distro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type {
MicrosoftOpenTelemetryOptions,
InstrumentationOptions,
BrowserSdkLoaderOptions,
A365Options,
} from "./types.js";
export { MICROSOFT_OPENTELEMETRY_VERSION } from "./types.js";

Expand Down
6 changes: 5 additions & 1 deletion src/distro/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import type {
InstrumentationOptions,
BrowserSdkLoaderOptions,
} from "../types.js";
import type { A365Options } from "../_a365/index.js";

export type { InstrumentationOptions, BrowserSdkLoaderOptions };
export type { InstrumentationOptions, BrowserSdkLoaderOptions, A365Options };

/**
* Microsoft OpenTelemetry distribution version.
Expand Down Expand Up @@ -49,4 +50,7 @@ export interface MicrosoftOpenTelemetryOptions {

/** Azure Monitor configuration. When provided, Azure Monitor export is enabled. */
azureMonitor?: AzureMonitorOpenTelemetryOptions;

/** A365 observability configuration. When provided with `enabled: true`, A365 export is enabled. */
a365?: A365Options;
}
15 changes: 15 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,23 @@ export type {
MicrosoftOpenTelemetryOptions,
InstrumentationOptions,
BrowserSdkLoaderOptions,
A365Options,
} from "./distro/index.js";

// ── Re-exports from A365 configuration ──────────────────────────────────────
export { A365Configuration } from "./_a365/index.js";
export type {
ClusterCategory,
A365BaggageOptions,
A365HostingOptions,
} from "./_a365/index.js";

// ── Re-exports from types ───────────────────────────────────────────────────
export type {
OpenAIAgentsInstrumentationConfig,
LangChainInstrumentationConfig,
} from "./types.js";

// ── Azure Monitor backward-compatible API ───────────────────────────────────

/**
Expand Down
7 changes: 7 additions & 0 deletions src/shared/jsonConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
BrowserSdkLoaderOptions,
InstrumentationOptions,
} from "../types.js";
import type { A365Options } from "../_a365/index.js";
import type { AzureMonitorExporterOptions } from "@azure/monitor-opentelemetry-exporter";
import type { MicrosoftOpenTelemetryOptions } from "../distro/types.js";
import { Logger } from "./logging/index.js";
Expand Down Expand Up @@ -58,6 +59,8 @@ export class JsonConfig implements MicrosoftOpenTelemetryOptions {
public instrumentationOptions?: InstrumentationOptions;
/** Azure Monitor scoped options */
public azureMonitor?: AzureMonitorOpenTelemetryOptions;
/** A365 scoped options */
public a365?: A365Options;

private static _instance: JsonConfig;

Expand Down Expand Up @@ -115,6 +118,10 @@ export class JsonConfig implements MicrosoftOpenTelemetryOptions {
enableStandardMetrics: jsonConfig.enableStandardMetrics as boolean | undefined,
enableTraceBasedSamplingForLogs: jsonConfig.enableTraceBasedSamplingForLogs as boolean | undefined,
};
// A365-scoped options
if (jsonConfig.a365 && typeof jsonConfig.a365 === "object") {
this.a365 = jsonConfig.a365 as A365Options;
}
} catch (err) {
Logger.getInstance().info("Missing or invalid JSON config file: ", err);
}
Expand Down
Loading
Loading