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
12 changes: 12 additions & 0 deletions packages/agents-a365-observability/src/tracing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,20 @@ export const useCustomDomainForObservability = (): boolean => {

/**
* Resolve the Agent365 service endpoint base URI for a given cluster category.
*
* By default this returns the production Agent365 endpoint. Internal development
* and test clusters can override this by setting the
* `A365_OBSERVABILITY_DOMAIN_OVERRIDE` environment variable. When set to a
* non-empty value, that value is used as the base URI regardless of cluster category.
*/
export function resolveAgent365Endpoint(clusterCategory: ClusterCategory): string {
const override = process.env.A365_OBSERVABILITY_DOMAIN_OVERRIDE;

if (override && override.trim().length > 0) {
// Normalize to avoid double slashes when concatenating paths
return override.trim().replace(/\/+$/, '');
}

switch (clusterCategory) {
case 'prod':
default:
Expand Down
16 changes: 14 additions & 2 deletions packages/agents-a365-runtime/src/environment-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,22 @@ export const DEVELOPMENT_ENVIRONMENT_NAME = 'Development';
/**
* Returns the scope for authenticating to the observability service
*
* @returns The authentication scope for the current environment.
* The default is the production observability scope, but this can be overridden
* for internal development and testing scenarios using the
* `A365_OBSERVABILITY_SCOPES_OVERRIDE` environment variable.
*
* When the override is set to a non-empty string, it is split on whitespace
* into individual scopes.
*
* @returns The authentication scopes for the current environment.
*/
export function getObservabilityAuthenticationScope(): string[] {
// Always return production scope
const override = process.env.A365_OBSERVABILITY_SCOPES_OVERRIDE;

if (override && override.trim().length > 0) {
return override.trim().split(/\s+/);
}

return [PROD_OBSERVABILITY_SCOPE];
}

Expand Down
2 changes: 2 additions & 0 deletions tests-agent/basic-agent-sdk-sample/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ ENABLE_A365_OBSERVABILITY_EXPORTER=true
CLUSTER_CATEGORY=prod # optional - defaults to 'prod' if not set
A365_OBSERVABILITY_LOG_LEVEL= # optional - set to enable observability logs, value can be 'info', 'warn', or 'error', default to 'none' if not set
Use_Custom_Resolver= # optional - set to 'true' to use custom token resolver, defaults to 'false' if not set
A365_OBSERVABILITY_DOMAIN_OVERRIDE= # optional - set to override the default observability domain
A365_OBSERVABILITY_SCOPES_OVERRIDE= # optional - set to override the default observability scopes
57 changes: 57 additions & 0 deletions tests/observability/core/agent365-exporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('Agent365Exporter', () => {
jest.useRealTimers();
global.fetch = originalFetch;
delete process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN;
delete process.env.A365_OBSERVABILITY_DOMAIN_OVERRIDE;
});

it('returns success immediately with no spans', async () => {
Expand Down Expand Up @@ -131,6 +132,62 @@ describe('Agent365Exporter', () => {
expect(headersArg['authorization']).toBe(`Bearer ${token}`);
});

it.each([
{
description: 'set to non-empty value',
override: 'https://custom-observability.internal',
expectedBaseUrl: 'https://custom-observability.internal'
},
{
description: 'set to empty string',
override: '',
expectedBaseUrl: 'https://agent365.svc.cloud.microsoft'
},
{
description: 'set to whitespace only',
override: ' ',
expectedBaseUrl: 'https://agent365.svc.cloud.microsoft'
},
{
description: 'unset (undefined)',
override: undefined,
expectedBaseUrl: 'https://agent365.svc.cloud.microsoft'
}
])('uses correct domain when A365_OBSERVABILITY_DOMAIN_OVERRIDE is $description', async ({ override, expectedBaseUrl }) => {
mockFetchSequence([200]);
process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN = 'true';

if (override !== undefined) {
process.env.A365_OBSERVABILITY_DOMAIN_OVERRIDE = override as string;
}

const token = 'tok-override-domain';
const opts = new Agent365ExporterOptions();
opts.clusterCategory = 'prod';
opts.tokenResolver = () => token;

const exporter = new Agent365Exporter(opts);
const spans = [
makeSpan({
[OpenTelemetryConstants.TENANT_ID_KEY]: tenantId,
[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: agentId
})
];

const callback = jest.fn();
await exporter.export(spans, callback);

expect(callback).toHaveBeenCalledWith({ code: ExportResultCode.SUCCESS });
const fetchCalls = (global.fetch as unknown as { mock: { calls: any[] } }).mock.calls;
expect(fetchCalls.length).toBe(1);
const urlArg = fetchCalls[0][0] as string;
const headersArg = fetchCalls[0][1].headers as Record<string, string>;

expect(urlArg).toBe(`${expectedBaseUrl}/maven/agent365/agents/${agentId}/traces?api-version=1`);
expect(headersArg['x-ms-tenant-id']).toBe(tenantId);
expect(headersArg['authorization']).toBe(`Bearer ${token}`);
});

it('exports to discovery endpoint when custom domain disabled', async () => {
mockFetchSequence([200]);
delete process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN;
Expand Down
28 changes: 27 additions & 1 deletion tests/runtime/environment-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,38 @@ describe('environment-utils', () => {
});

describe('getObservabilityAuthenticationScope', () => {
it('should return production observability scope', () => {
it('should return production observability scope when override is not set', () => {
delete process.env.A365_OBSERVABILITY_SCOPES_OVERRIDE;

const scopes = getObservabilityAuthenticationScope();

expect(scopes).toEqual([PROD_OBSERVABILITY_SCOPE]);
expect(scopes[0]).toEqual('https://api.powerplatform.com/.default');
});

it('should return overridden observability scope when A365_OBSERVABILITY_SCOPES_OVERRIDE is set', () => {
process.env.A365_OBSERVABILITY_SCOPES_OVERRIDE = 'https://override.example.com/.default';

const scopes = getObservabilityAuthenticationScope();

expect(scopes).toEqual(['https://override.example.com/.default']);
});

it('should support multiple scopes separated by whitespace', () => {
process.env.A365_OBSERVABILITY_SCOPES_OVERRIDE = 'scope-one/.default scope-two/.default';

const scopes = getObservabilityAuthenticationScope();

expect(scopes).toEqual(['scope-one/.default', 'scope-two/.default']);
});

it('should fall back to production scope when override is empty or whitespace', () => {
process.env.A365_OBSERVABILITY_SCOPES_OVERRIDE = ' ';

const scopes = getObservabilityAuthenticationScope();

expect(scopes).toEqual([PROD_OBSERVABILITY_SCOPE]);
});
Comment thread
fpfp100 marked this conversation as resolved.
});

describe('getClusterCategory', () => {
Expand Down