Skip to content

Commit 3e82554

Browse files
fpfp100jsl517
andauthored
Add option to use custom domain for A365 service (#92)
* Use custom domain for A365 service * comments * comments * test update * comment * comment * comment * build failure --------- Co-authored-by: jsl517 <pefan@microsoft.com>
1 parent 22ca368 commit 3e82554

3 files changed

Lines changed: 136 additions & 36 deletions

File tree

packages/agents-a365-observability/src/tracing/exporter/Agent365Exporter.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { PowerPlatformApiDiscovery, ClusterCategory } from '@microsoft/agents-a3
1010
import { partitionByIdentity, parseIdentityKey, hexTraceId, hexSpanId, kindName, statusName } from './utils';
1111
import logger, { formatError } from '../../utils/logging';
1212
import { Agent365ExporterOptions } from './Agent365ExporterOptions';
13+
import { useCustomDomainForObservability, resolveAgent365Endpoint } from '../util';
14+
1315
const DEFAULT_HTTP_TIMEOUT_SECONDS = 30000; // 30 seconds in ms
1416
const DEFAULT_MAX_RETRIES = 3;
1517

@@ -143,11 +145,19 @@ export class Agent365Exporter implements SpanExporter {
143145
const payload = this.buildExportRequest(spans);
144146
const body = JSON.stringify(payload);
145147

146-
// Resolve endpoint + token
147-
const discovery = new PowerPlatformApiDiscovery(this.options.clusterCategory as ClusterCategory);
148-
const endpoint = discovery.getTenantIslandClusterEndpoint(tenantId);
149-
const url = `https://${endpoint}/maven/agent365/agents/${agentId}/traces?api-version=1`;
150-
logger.info(`[Agent365Exporter] Resolved endpoint: ${endpoint}`);
148+
const usingCustomServiceEndpoint = useCustomDomainForObservability();
149+
150+
let url: string;
151+
if (usingCustomServiceEndpoint) {
152+
url = resolveAgent365Endpoint(this.options.clusterCategory as ClusterCategory);
153+
logger.info(`[Agent365Exporter] Using custom domain endpoint: ${url}`);
154+
} else {
155+
// Default behavior: discover PPAPI gateway endpoint per-tenant
156+
const discovery = new PowerPlatformApiDiscovery(this.options.clusterCategory as ClusterCategory);
157+
const endpoint = discovery.getTenantIslandClusterEndpoint(tenantId);
158+
url = `https://${endpoint}/maven/agent365/agents/${agentId}/traces?api-version=1`;
159+
logger.info(`[Agent365Exporter] Resolved endpoint: ${endpoint}`);
160+
}
151161

152162
const headers: Record<string, string> = {
153163
'content-type': 'application/json'
@@ -166,6 +176,10 @@ export class Agent365Exporter implements SpanExporter {
166176
logger.error('[Agent365Exporter] No token resolved');
167177
}
168178

179+
// Add tenant id to headers when using custom domain
180+
if (usingCustomServiceEndpoint) {
181+
headers['x-ms-tenant-id'] = tenantId;
182+
}
169183

170184
// Basic retry loop
171185
const ok = await this.postWithRetries(url, body, headers);
Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,61 @@
1-
import { OpenTelemetryConstants } from './constants';
2-
3-
4-
/**
5-
* Check if exporter is enabled via environment variables
6-
*/
7-
export const isAgent365ExporterEnabled: () => boolean = (): boolean => {
8-
const enableA365Exporter = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]?.toLowerCase();
9-
10-
return (
11-
enableA365Exporter === 'true' ||
12-
enableA365Exporter === '1' ||
13-
enableA365Exporter === 'yes' ||
14-
enableA365Exporter === 'on'
15-
);
16-
};
17-
18-
/**
19-
* Gets the enable telemetry configuration value
20-
*/
21-
export const isAgent365TelemetryEnabled: () => boolean = (): boolean => {
22-
const enableObservability = process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY]?.toLowerCase();
23-
const enableA365 = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY]?.toLowerCase();
24-
25-
return (
26-
enableObservability === 'true' ||
27-
enableObservability === '1' ||
28-
enableA365 === 'true' ||
29-
enableA365 === '1'
30-
);
31-
};
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation.
3+
// Licensed under the MIT License.
4+
// ------------------------------------------------------------------------------
5+
6+
import { OpenTelemetryConstants } from './constants';
7+
import { ClusterCategory } from '@microsoft/agents-a365-runtime';
8+
/**
9+
* Check if exporter is enabled via environment variables
10+
*/
11+
export const isAgent365ExporterEnabled: () => boolean = (): boolean => {
12+
const enableA365Exporter = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]?.toLowerCase();
13+
14+
return (
15+
enableA365Exporter === 'true' ||
16+
enableA365Exporter === '1' ||
17+
enableA365Exporter === 'yes' ||
18+
enableA365Exporter === 'on'
19+
);
20+
};
21+
22+
/**
23+
* Gets the enable telemetry configuration value
24+
*/
25+
export const isAgent365TelemetryEnabled: () => boolean = (): boolean => {
26+
const enableObservability = process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY]?.toLowerCase();
27+
const enableA365 = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY]?.toLowerCase();
28+
29+
return (
30+
enableObservability === 'true' ||
31+
enableObservability === '1' ||
32+
enableA365 === 'true' ||
33+
enableA365 === '1'
34+
);
35+
};
36+
37+
/**
38+
* Single toggle to use custom domain for observability export.
39+
* When true exporter will send traces to custom Agent365 service endpoint
40+
* and include x-ms-tenant-id in headers.
41+
*/
42+
export const useCustomDomainForObservability = (): boolean => {
43+
const value = process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN?.toLowerCase();
44+
return (
45+
value === 'true' ||
46+
value === '1' ||
47+
value === 'yes' ||
48+
value === 'on'
49+
);
50+
};
51+
52+
/**
53+
* Resolve the Agent365 service endpoint base URI for a given cluster category.
54+
*/
55+
export function resolveAgent365Endpoint(clusterCategory: ClusterCategory): string {
56+
switch (clusterCategory) {
57+
case 'prod':
58+
default:
59+
return 'https://agent365.svc.cloud.microsoft';
60+
}
61+
}

tests/observability/core/agent365-exporter.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ describe('Agent365Exporter', () => {
5454
jest.clearAllTimers();
5555
jest.useRealTimers();
5656
global.fetch = originalFetch;
57+
delete process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN;
5758
});
5859

5960
it('returns success immediately with no spans', async () => {
@@ -102,6 +103,61 @@ describe('Agent365Exporter', () => {
102103
expect(exportedSpan.attributes[OpenTelemetryConstants.GEN_AI_CALLER_AGENT_CLIENT_IP_KEY]).toBe('1.0.0.5');
103104
});
104105

106+
it.each([
107+
{ cluster: 'prod', expectedUrl: 'https://agent365.svc.cloud.microsoft', token: 'tok-prod' },
108+
{ cluster: 'preprod', expectedUrl: 'https://agent365.svc.cloud.microsoft', token: 'tok-preprod' }
109+
])('exports to custom domain when enabled (cluster=%s)', async ({ cluster, expectedUrl, token }) => {
110+
mockFetchSequence([200]);
111+
process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN = 'true';
112+
const opts = new Agent365ExporterOptions();
113+
opts.clusterCategory = cluster;
114+
opts.tokenResolver = () => token;
115+
const exporter = new Agent365Exporter(opts);
116+
const spans = [
117+
makeSpan({
118+
[OpenTelemetryConstants.TENANT_ID_KEY]: tenantId,
119+
[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: agentId
120+
})
121+
];
122+
const callback = jest.fn();
123+
await exporter.export(spans, callback);
124+
expect(callback).toHaveBeenCalledWith({ code: ExportResultCode.SUCCESS });
125+
const fetchCalls = (global.fetch as unknown as { mock: { calls: any[] } }).mock.calls;
126+
expect(fetchCalls.length).toBe(1);
127+
const urlArg = fetchCalls[0][0];
128+
const headersArg = fetchCalls[0][1].headers;
129+
expect(urlArg).toBe(expectedUrl);
130+
expect(headersArg['x-ms-tenant-id']).toBe(tenantId);
131+
expect(headersArg['authorization']).toBe(`Bearer ${token}`);
132+
});
133+
134+
it('exports to discovery endpoint when custom domain disabled', async () => {
135+
mockFetchSequence([200]);
136+
delete process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN;
137+
const token = 'tok-prod-disabled';
138+
const opts = new Agent365ExporterOptions();
139+
opts.clusterCategory = 'prod';
140+
opts.tokenResolver = () => token;
141+
const exporter = new Agent365Exporter(opts);
142+
const spans = [
143+
makeSpan({
144+
[OpenTelemetryConstants.TENANT_ID_KEY]: tenantId,
145+
[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: agentId
146+
})
147+
];
148+
const callback = jest.fn();
149+
await exporter.export(spans, callback);
150+
expect(callback).toHaveBeenCalledWith({ code: ExportResultCode.SUCCESS });
151+
const fetchCalls = (global.fetch as unknown as { mock: { calls: any[] } }).mock.calls;
152+
expect(fetchCalls.length).toBe(1);
153+
const urlArg = fetchCalls[0][0];
154+
const headersArg = fetchCalls[0][1].headers;
155+
const discoveryRegex = new RegExp(`^https://[\\w.-]+/maven/agent365/agents/${agentId}/traces\\?api-version=1$`, 'i');
156+
expect(urlArg).toMatch(discoveryRegex);
157+
expect(headersArg['x-ms-tenant-id']).toBeUndefined();
158+
expect(headersArg['authorization']).toBe(`Bearer ${token}`);
159+
});
160+
105161
it('requires a tokenResolver and fails export when missing', async () => {
106162
const opts = new Agent365ExporterOptions();
107163
opts.clusterCategory = 'local';

0 commit comments

Comments
 (0)