Skip to content
Draft
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
43 changes: 26 additions & 17 deletions packages/agents-a365-observability/src/ObservabilityBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,33 @@ export class ObservabilityBuilder {
}

private createBatchProcessor(): BatchSpanProcessor {
if (!isAgent365ExporterEnabled()) {
return new BatchSpanProcessor(new ConsoleSpanExporter());
// To send telemetry to Agent365 service, BOTH conditions must be met:
// 1. ENABLE_A365_OBSERVABILITY_EXPORTER=true must be explicitly set
// 2. A tokenResolver must be provided
const isExporterEnabled = isAgent365ExporterEnabled();
const hasTokenResolver = this.options.tokenResolver || this.options.exporterOptions?.tokenResolver;

// Use Agent365Exporter only if both exporter is enabled AND token resolver is available
if (isExporterEnabled && hasTokenResolver) {
const opts = new Agent365ExporterOptions();
if (this.options.exporterOptions) {
Object.assign(opts, this.options.exporterOptions);
}
opts.clusterCategory = this.options.clusterCategory || opts.clusterCategory || 'prod';
if (this.options.tokenResolver) {
opts.tokenResolver = this.options.tokenResolver;
}

return new BatchSpanProcessor(new Agent365Exporter(opts), {
maxQueueSize: opts.maxQueueSize,
scheduledDelayMillis: opts.scheduledDelayMilliseconds,
exportTimeoutMillis: opts.exporterTimeoutMilliseconds,
maxExportBatchSize: opts.maxExportBatchSize
});
}

const opts = new Agent365ExporterOptions();
if (this.options.exporterOptions) {
Object.assign(opts, this.options.exporterOptions);
}
opts.clusterCategory = this.options.clusterCategory || opts.clusterCategory || 'prod';
if (this.options.tokenResolver) {
opts.tokenResolver = this.options.tokenResolver;
}
return new BatchSpanProcessor(new Agent365Exporter(opts), {
maxQueueSize: opts.maxQueueSize,
scheduledDelayMillis: opts.scheduledDelayMilliseconds,
exportTimeoutMillis: opts.exporterTimeoutMilliseconds,
maxExportBatchSize: opts.maxExportBatchSize
});

// Default: use console exporter (for local development and when service export is not configured)
return new BatchSpanProcessor(new ConsoleSpanExporter());
}

private createResource() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export class Agent365ExporterOptions {
/** Environment / cluster category (e.g. "preprod", "prod"). */
public clusterCategory: ClusterCategory | string = 'prod';

/** Optional delegate to resolve auth token used by exporter */
public tokenResolver?: TokenResolver; // Optional if ENABLE_A365_OBSERVABILITY_EXPORTER is false
/** Optional delegate to resolve auth token used by exporter. Required to send telemetry to Agent365 service; if not provided, telemetry is logged to console. */
public tokenResolver?: TokenResolver;

/** Maximum span queue size before new spans are dropped. */
public maxQueueSize: number = 2048;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import { SpanKind, SpanStatusCode } from '@opentelemetry/api';
import { OpenTelemetryConstants } from '../constants';
import logger from '../../utils/logging';
import { isAgent365ExporterEnabled as isExporterEnabled } from '../util';

/**
* Convert trace ID to hex string format
Expand Down Expand Up @@ -110,12 +111,16 @@ export function partitionByIdentity(

/**
* Check if Agent 365 exporter is enabled via environment variable
* Requires explicit enabling by setting to 'true', '1', 'yes', or 'on'
* This wrapper adds logging for debugging purposes
*/
export function isAgent365ExporterEnabled(): boolean {
const a365Env = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]?.toLowerCase() || '';
const validValues = ['true', '1', 'yes', 'on'];
const enabled: boolean = validValues.includes(a365Env);
logger.info(`[Agent365Exporter] Agent 365 exporter enabled: ${enabled}`);
const enabled = isExporterEnabled();
const envVar = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER];
const message = envVar
? `[Agent365Exporter] Agent 365 exporter enabled: ${enabled} (env var: ${envVar})`
: '[Agent365Exporter] Agent 365 exporter enabled: false (not set, requires explicit enabling)';
logger.info(message);
return enabled;
}

Expand Down
39 changes: 31 additions & 8 deletions packages/agents-a365-observability/src/tracing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,24 @@

import { OpenTelemetryConstants } from './constants';
import { ClusterCategory } from '@microsoft/agents-a365-runtime';

/**
* Helper function to check if a value is explicitly disabled
*/
const isExplicitlyDisabled = (value: string | undefined): boolean => {
if (!value) return false;
const lowerValue = value.toLowerCase();
return (
lowerValue === 'false' ||
lowerValue === '0' ||
lowerValue === 'no' ||
lowerValue === 'off'
);
};

/**
* Check if exporter is enabled via environment variables
* Requires explicit enabling by setting to 'true', '1', 'yes', or 'on'
*/
export const isAgent365ExporterEnabled: () => boolean = (): boolean => {
const enableA365Exporter = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]?.toLowerCase();
Expand All @@ -21,17 +37,24 @@ export const isAgent365ExporterEnabled: () => boolean = (): boolean => {

/**
* Gets the enable telemetry configuration value
* Enabled by default, can be disabled by setting to 'false', '0', 'no', or 'off'
*/
export const isAgent365TelemetryEnabled: () => boolean = (): boolean => {
const enableObservability = process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY]?.toLowerCase();
const enableA365 = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY]?.toLowerCase();
const enableObservability = process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY];
const enableA365 = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY];

return (
enableObservability === 'true' ||
enableObservability === '1' ||
enableA365 === 'true' ||
enableA365 === '1'
);
// If neither is set, default to enabled (true)
if (!enableObservability && !enableA365) {
return true;
}

// If both are set, both must not be disabled
// If only one is set, it must not be disabled
return enableObservability && enableA365
? !isExplicitlyDisabled(enableObservability) && !isExplicitlyDisabled(enableA365)
: enableObservability
? !isExplicitlyDisabled(enableObservability)
: !isExplicitlyDisabled(enableA365);
};

/**
Expand Down
40 changes: 36 additions & 4 deletions tests/observability/core/observabilityBuilder-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@ jest.mock('@microsoft/agents-a365-observability/src/tracing/exporter/Agent365Exp

describe('ObservabilityBuilder exporterOptions merging', () => {
beforeEach(() => {
// Ensure exporter is enabled so BatchSpanProcessor is created with Agent365Exporter
process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = 'true';
// Clean up any captured options from previous tests
delete (global as any).__capturedExporterOptions;
delete (global as any).__capturedExporterOptionsCallCount;
});

afterEach(() => {
// Clean up environment variable after each test
delete process.env.ENABLE_A365_OBSERVABILITY_EXPORTER;
});

it('applies provided exporterOptions and allows builder overrides to take precedence', () => {
// Enable Agent365 exporter to test the exporter options
process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = 'true';

const builder = new ObservabilityBuilder()
.withExporterOptions({
maxQueueSize: 10,
Expand Down Expand Up @@ -65,13 +68,42 @@ describe('ObservabilityBuilder exporterOptions merging', () => {
});

it('defaults to prod clusterCategory when none provided', () => {
// Enable Agent365 exporter to test the exporter options
process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = 'true';

const builder = new ObservabilityBuilder()
.withExporterOptions({ maxQueueSize: 15 }); // no cluster category passed
.withExporterOptions({ maxQueueSize: 15 }) // no cluster category passed
.withTokenResolver(() => 'test-token'); // Add token resolver so Agent365Exporter is used

builder.build();
const captured: any = (global as any).__capturedExporterOptions;
expect(captured.clusterCategory).toBe('prod');
expect(captured.maxQueueSize).toBe(15);
expect(captured.scheduledDelayMilliseconds).toBe(5000); // default value
});
});

it('uses ConsoleSpanExporter when no tokenResolver is provided', () => {
const builder = new ObservabilityBuilder()
.withExporterOptions({ maxQueueSize: 15 }); // no token resolver

const built = builder.build();
expect(built).toBe(true);

// Since no tokenResolver was provided, Agent365Exporter should NOT be created
const captured: any = (global as any).__capturedExporterOptions;
expect(captured).toBeUndefined();
});

it('uses ConsoleSpanExporter when ENABLE_A365_OBSERVABILITY_EXPORTER is not set', () => {
// Even with tokenResolver, if env var is not set, should use ConsoleSpanExporter
const builder = new ObservabilityBuilder()
.withTokenResolver(() => 'test-token');

const built = builder.build();
expect(built).toBe(true);

// Since ENABLE_A365_OBSERVABILITY_EXPORTER is not set, Agent365Exporter should NOT be created
const captured: any = (global as any).__capturedExporterOptions;
expect(captured).toBeUndefined();
});
});
128 changes: 128 additions & 0 deletions tests/observability/core/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------------------------

import { isAgent365TelemetryEnabled, isAgent365ExporterEnabled } from '@microsoft/agents-a365-observability/src/tracing/util';
import { OpenTelemetryConstants } from '@microsoft/agents-a365-observability/src/tracing/constants';

describe('Observability Utility Functions', () => {
describe('isAgent365TelemetryEnabled', () => {
beforeEach(() => {
// Clear all relevant environment variables before each test
delete process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY];
delete process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY];
});

it('should return true by default when no environment variables are set', () => {
expect(isAgent365TelemetryEnabled()).toBe(true);
});

it('should return false when ENABLE_OBSERVABILITY is explicitly set to false', () => {
process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'false';
expect(isAgent365TelemetryEnabled()).toBe(false);
});

it('should return false when ENABLE_OBSERVABILITY is set to 0', () => {
process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = '0';
expect(isAgent365TelemetryEnabled()).toBe(false);
});

it('should return false when ENABLE_OBSERVABILITY is set to no', () => {
process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'no';
expect(isAgent365TelemetryEnabled()).toBe(false);
});

it('should return false when ENABLE_OBSERVABILITY is set to off', () => {
process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'off';
expect(isAgent365TelemetryEnabled()).toBe(false);
});

it('should return true when ENABLE_OBSERVABILITY is set to true', () => {
process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'true';
expect(isAgent365TelemetryEnabled()).toBe(true);
});

it('should return true when ENABLE_OBSERVABILITY is set to 1', () => {
process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = '1';
expect(isAgent365TelemetryEnabled()).toBe(true);
});

it('should return false when ENABLE_A365_OBSERVABILITY is explicitly set to false', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY] = 'false';
expect(isAgent365TelemetryEnabled()).toBe(false);
});

it('should return true when ENABLE_A365_OBSERVABILITY is set to true', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY] = 'true';
expect(isAgent365TelemetryEnabled()).toBe(true);
});

it('should return false when both are explicitly disabled', () => {
process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'false';
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY] = 'false';
expect(isAgent365TelemetryEnabled()).toBe(false);
});

it('should return true when ENABLE_OBSERVABILITY is enabled and ENABLE_A365_OBSERVABILITY is not set', () => {
process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'true';
expect(isAgent365TelemetryEnabled()).toBe(true);
});

it('should return true when ENABLE_A365_OBSERVABILITY is enabled and ENABLE_OBSERVABILITY is not set', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY] = 'true';
expect(isAgent365TelemetryEnabled()).toBe(true);
});
});

describe('isAgent365ExporterEnabled', () => {
beforeEach(() => {
// Clear environment variable before each test
delete process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER];
});

it('should return false by default when environment variable is not set', () => {
expect(isAgent365ExporterEnabled()).toBe(false);
});

it('should return false when explicitly set to false', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'false';
expect(isAgent365ExporterEnabled()).toBe(false);
});

it('should return false when set to 0', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = '0';
expect(isAgent365ExporterEnabled()).toBe(false);
});

it('should return false when set to no', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'no';
expect(isAgent365ExporterEnabled()).toBe(false);
});

it('should return false when set to off', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'off';
expect(isAgent365ExporterEnabled()).toBe(false);
});

it('should return true when set to true', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'true';
expect(isAgent365ExporterEnabled()).toBe(true);
});

it('should return true when set to 1', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = '1';
expect(isAgent365ExporterEnabled()).toBe(true);
});

it('should return true when set to yes', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'yes';
expect(isAgent365ExporterEnabled()).toBe(true);
});

it('should return true when set to on', () => {
process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'on';
expect(isAgent365ExporterEnabled()).toBe(true);
});
});
});