Skip to content

Commit 56b1b4f

Browse files
authored
[AgentObservability] Add BaggageSpanProcessor to populate baggage attributes into OTel spans, and AwsBatchUnsampledSpanProcessor (#205)
*Issue #, if available:* JS Equivalent of: 1. aws-observability/aws-otel-python-instrumentation#390 2. aws-observability/aws-otel-python-instrumentation#391 *Description of changes:* By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 17a5892 commit 56b1b4f

File tree

4 files changed

+213
-1
lines changed

4 files changed

+213
-1
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"@opentelemetry/auto-configuration-propagators": "0.3.2",
102102
"@opentelemetry/auto-instrumentations-node": "0.56.0",
103103
"@opentelemetry/api-events": "0.57.1",
104+
"@opentelemetry/baggage-span-processor": "0.3.1",
104105
"@opentelemetry/sdk-events": "0.57.1",
105106
"@opentelemetry/sdk-logs": "0.57.1",
106107
"@opentelemetry/core": "1.30.1",

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ import { OTLPUdpSpanExporter } from './otlp-udp-exporter';
6161
import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler';
6262
// This file is generated via `npm run compile`
6363
import { LIB_VERSION } from './version';
64+
import { isAgentObservabilityEnabled } from './utils';
65+
import { BaggageSpanProcessor } from '@opentelemetry/baggage-span-processor';
66+
import { logs } from '@opentelemetry/api-logs';
6467

6568
const XRAY_OTLP_ENDPOINT_PATTERN = '^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$';
6669

@@ -223,7 +226,48 @@ export class AwsOpentelemetryConfigurator {
223226
return isApplicationSignalsEnabled.toLowerCase() === 'true';
224227
}
225228

229+
static exportUnsampledSpanForAgentObservability(spanProcessors: SpanProcessor[], resource: Resource): void {
230+
if (!isAgentObservabilityEnabled()) {
231+
return;
232+
}
233+
234+
// Get the traces endpoint from environment
235+
const tracesEndpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
236+
237+
if (!tracesEndpoint) {
238+
// No traces endpoint configured, skip unsampled span export
239+
diag.warn('No traces endpoint configured for agent observability unsampled spans');
240+
return;
241+
}
242+
243+
let spanExporter: SpanExporter;
244+
// Create the appropriate span exporter based on the endpoint
245+
if (isXrayOtlpEndpoint(tracesEndpoint)) {
246+
spanExporter = new OTLPAwsSpanExporter(tracesEndpoint, undefined, logs.getLoggerProvider());
247+
} else {
248+
spanExporter = new OTLPAwsSpanExporter(tracesEndpoint);
249+
}
250+
251+
// Add the unsampled span processor
252+
spanProcessors.push(new AwsBatchUnsampledSpanProcessor(spanExporter));
253+
}
254+
226255
static customizeSpanProcessors(spanProcessors: SpanProcessor[], resource: Resource): void {
256+
if (isAgentObservabilityEnabled()) {
257+
// We always send 100% spans to Genesis platform for agent observability because
258+
// AI applications typically have low throughput traffic patterns and require
259+
// comprehensive monitoring to catch subtle failure modes like hallucinations
260+
// and quality degradation that sampling could miss.
261+
this.exportUnsampledSpanForAgentObservability(spanProcessors, resource);
262+
263+
// Add session.id baggage attribute to span attributes to support AI Agent use cases
264+
// enabling session ID tracking in spans.
265+
const sessionIdPredicate = (baggageKey: string) => {
266+
return baggageKey === 'session.id';
267+
};
268+
spanProcessors.push(new BaggageSpanProcessor(sessionIdPredicate));
269+
}
270+
227271
if (!AwsOpentelemetryConfigurator.isApplicationSignalsEnabled()) {
228272
return;
229273
}

aws-distro-opentelemetry-node-autoinstrumentation/test/aws-opentelemetry-configurator.test.ts

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import { setAwsDefaultEnvironmentVariables } from '../src/register';
4242
import { AwsXRayRemoteSampler } from '../src/sampler/aws-xray-remote-sampler';
4343
import { AwsXraySamplingClient } from '../src/sampler/aws-xray-sampling-client';
4444
import { GetSamplingRulesResponse } from '../src/sampler/remote-sampler.types';
45+
import { OTLPAwsSpanExporter } from '../src/otlp-aws-span-exporter';
46+
import { BaggageSpanProcessor } from '@opentelemetry/baggage-span-processor';
4547

4648
// Tests AwsOpenTelemetryConfigurator after running Environment Variable setup in register.ts
4749
describe('AwsOpenTelemetryConfiguratorTest', () => {
@@ -276,7 +278,10 @@ describe('AwsOpenTelemetryConfiguratorTest', () => {
276278

277279
it('CustomizeSpanProcessorsTest', () => {
278280
delete process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED;
279-
const spanProcessors: SpanProcessor[] = [];
281+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
282+
283+
// Test application signals only
284+
let spanProcessors: SpanProcessor[] = [];
280285
AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessors, Resource.empty());
281286
expect(spanProcessors.length).toEqual(0);
282287

@@ -310,6 +315,81 @@ describe('AwsOpenTelemetryConfiguratorTest', () => {
310315
spanProcessors.forEach(spanProcessor => {
311316
spanProcessor.shutdown();
312317
});
318+
319+
// Reset spanProcessors list for next set of tests
320+
spanProcessors = [];
321+
322+
process.env.AGENT_OBSERVABILITY_ENABLED = 'true';
323+
process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED = 'True';
324+
AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessors, Resource.empty());
325+
expect(spanProcessors.length).toEqual(3);
326+
327+
// Verify processors are added in the expected order
328+
expect(spanProcessors[0]).toBeInstanceOf(BaggageSpanProcessor);
329+
expect(spanProcessors[1]).toBeInstanceOf(AttributePropagatingSpanProcessor);
330+
expect(spanProcessors[2]).toBeInstanceOf(AwsSpanMetricsProcessor);
331+
332+
// shut down exporters for test cleanup
333+
spanProcessors.forEach(spanProcessor => {
334+
spanProcessor.shutdown();
335+
});
336+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
337+
delete process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED;
338+
});
339+
340+
it('CustomizeSpanProcessorsWithAgentObservabilityTest', () => {
341+
const spanProcessorsToTest: SpanProcessor[] = [];
342+
343+
// Test that BaggageSpanProcessor is not added when agent observability is disabled
344+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
345+
AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty());
346+
expect(spanProcessorsToTest).toEqual([]);
347+
348+
// Test that BaggageSpanProcessor is added when agent observability is enabled
349+
process.env.AGENT_OBSERVABILITY_ENABLED = 'true';
350+
AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty());
351+
expect(spanProcessorsToTest.length).toEqual(1);
352+
353+
// Verify the added processor is BaggageSpanProcessor
354+
const addedProcessor = spanProcessorsToTest[0];
355+
expect(addedProcessor).toBeInstanceOf(BaggageSpanProcessor);
356+
357+
// Clean up
358+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
359+
});
360+
361+
it('BaggageSpanProcessorSessionIdFilteringTest', () => {
362+
// Set up agent observability
363+
process.env.AGENT_OBSERVABILITY_ENABLED = 'true';
364+
365+
// Create a SpanProcessor list for this test
366+
const spanProcessorsToTest: SpanProcessor[] = [];
367+
368+
// Add our span processors
369+
AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty());
370+
371+
// Verify that the BaggageSpanProcessor was added
372+
const baggageProcessors = spanProcessorsToTest.filter(
373+
processor => processor.constructor.name === 'BaggageSpanProcessor'
374+
);
375+
expect(baggageProcessors.length).toBe(1);
376+
377+
// Verify the predicate function only accepts session.id
378+
const baggageProcessor = baggageProcessors[0];
379+
expect(baggageProcessor).toBeInstanceOf(BaggageSpanProcessor);
380+
const predicate = (baggageProcessor as BaggageSpanProcessor)['_keyPredicate'].bind(baggageProcessor);
381+
382+
// Test the predicate function directly
383+
expect(predicate('session.id')).toBeTruthy();
384+
expect(predicate('user.id')).toBeFalsy();
385+
expect(predicate('request.id')).toBeFalsy();
386+
expect(predicate('other.key')).toBeFalsy();
387+
expect(predicate('')).toBeFalsy();
388+
expect(predicate('session')).toBeFalsy();
389+
expect(predicate('id')).toBeFalsy();
390+
391+
// Clean up
392+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
313393
});
314394

315395
it('ApplicationSignalsExporterProviderTest', () => {
@@ -667,4 +747,75 @@ describe('AwsOpenTelemetryConfiguratorTest', () => {
667747
// Cleanup
668748
delete process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL;
669749
});
750+
751+
it('ExportUnsampledSpanForAgentObservabilityTest', () => {
752+
const spanProcessorsToTest: SpanProcessor[] = [];
753+
754+
// Test with agent observability disabled
755+
AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty());
756+
expect(spanProcessorsToTest).toEqual([]);
757+
758+
// Test with agent observability enabled
759+
process.env.AGENT_OBSERVABILITY_ENABLED = 'true';
760+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'https://xray.us-east-1.amazonaws.com/v1/traces';
761+
762+
AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty());
763+
expect(spanProcessorsToTest.length).toEqual(1);
764+
765+
const processor = spanProcessorsToTest[0];
766+
expect(processor).toBeInstanceOf(AwsBatchUnsampledSpanProcessor);
767+
768+
// Cleanup
769+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
770+
delete process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
771+
});
772+
773+
it('ExportUnsampledSpanForAgentObservabilityUsesOtlpAwsSpanExporterTest', () => {
774+
const spanProcessorsToTest: SpanProcessor[] = [];
775+
776+
process.env.AGENT_OBSERVABILITY_ENABLED = 'true';
777+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'https://xray.us-east-1.amazonaws.com/v1/traces';
778+
779+
AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty());
780+
781+
// Verify AwsBatchUnsampledSpanProcessor was created with the AWS exporter
782+
expect(spanProcessorsToTest[0]).toBeInstanceOf(AwsBatchUnsampledSpanProcessor);
783+
const otlpAwsSpanExporter = (spanProcessorsToTest[0] as AwsBatchUnsampledSpanProcessor)['_exporter'];
784+
785+
// Verify OTLPAwsSpanExporter was created with correct parameters
786+
expect(otlpAwsSpanExporter).toBeInstanceOf(OTLPAwsSpanExporter);
787+
expect(otlpAwsSpanExporter['endpoint']).toEqual('https://xray.us-east-1.amazonaws.com/v1/traces');
788+
expect(otlpAwsSpanExporter['loggerProvider']).toBeDefined();
789+
790+
// Cleanup environment variables
791+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
792+
delete process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
793+
});
794+
795+
it('CustomizeSpanProcessorsCallsExportUnsampledSpanTest', () => {
796+
const spanProcessorsToTest: SpanProcessor[] = [];
797+
798+
// Create spy for exportUnsampledSpanForAgentObservability
799+
const exportUnsampledSpanSpy = sinon.spy(AwsOpentelemetryConfigurator, 'exportUnsampledSpanForAgentObservability');
800+
801+
try {
802+
// Test that function is NOT called when agent observability is disabled
803+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
804+
AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty());
805+
expect(exportUnsampledSpanSpy.called).toBeFalsy();
806+
807+
// Test that function is called when agent observability is enabled
808+
exportUnsampledSpanSpy.resetHistory();
809+
process.env.AGENT_OBSERVABILITY_ENABLED = 'true';
810+
AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty());
811+
expect(exportUnsampledSpanSpy.calledOnce).toBeTruthy();
812+
expect(exportUnsampledSpanSpy.calledWith(spanProcessorsToTest, Resource.empty())).toBeTruthy();
813+
} finally {
814+
// Restore original implementation
815+
exportUnsampledSpanSpy.restore();
816+
817+
// Cleanup
818+
delete process.env.AGENT_OBSERVABILITY_ENABLED;
819+
}
820+
});
670821
});

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)