Skip to content

Commit 2d59af7

Browse files
authored
AppSignals Functionality - add ADOT Span Processors and Exporter (#11)
*Issue #, if available:* *Description of changes:* Implement the following files in TypeScript ``` attribute-propagating-span-processor-builder.ts attribute-propagating-span-processor.ts aws-metric-attributes-span-exporter-builder.ts aws-metric-attributes-span-exporter.ts aws-span-metrics-processor-builder.ts aws-span-metrics-processor.ts ``` - [Java Comparison](https://github.com/aws-observability/aws-otel-java-instrumentation/tree/main/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers) - [Python Comparison](https://github.com/aws-observability/aws-otel-python-instrumentation/tree/main/aws-opentelemetry-distro/src/amazon/opentelemetry/distro) 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 486474d commit 2d59af7

15 files changed

+2197
-8
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/always-record-sampler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export class AlwaysRecordSampler implements Sampler {
2424
}
2525

2626
private constructor(rootSampler: Sampler) {
27-
if (rootSampler === null) {
28-
throw new Error('rootSampler is null. It must be provided');
27+
if (rootSampler == null) {
28+
throw new Error('rootSampler is null/undefined. It must be provided');
2929
}
3030
this.rootSampler = rootSampler;
3131
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
5+
import { AttributePropagatingSpanProcessor } from './attribute-propagating-span-processor';
6+
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
7+
import { AwsSpanProcessingUtil } from './aws-span-processing-util';
8+
9+
/**
10+
* AttributePropagatingSpanProcessorBuilder is used to construct a {@link AttributePropagatingSpanProcessor}.
11+
* If {@link setPropagationDataExtractor}, {@link setPropagationDataKey} or {@link setAttributesKeysToPropagate}
12+
* are not invoked, the builder defaults to using specific propagation targets.
13+
*/
14+
export class AttributePropagatingSpanProcessorBuilder {
15+
private propagationDataExtractor: (span: ReadableSpan) => string = AwsSpanProcessingUtil.getIngressOperation;
16+
private propagationDataKey: string = AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION;
17+
private attributesKeysToPropagate: string[] = [
18+
AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE,
19+
AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION,
20+
];
21+
22+
public static create(): AttributePropagatingSpanProcessorBuilder {
23+
return new AttributePropagatingSpanProcessorBuilder();
24+
}
25+
26+
private constructor() {}
27+
28+
public setPropagationDataExtractor(
29+
propagationDataExtractor: (span: ReadableSpan) => string
30+
): AttributePropagatingSpanProcessorBuilder {
31+
if (propagationDataExtractor == null) {
32+
throw new Error('propagationDataExtractor must not be null');
33+
}
34+
this.propagationDataExtractor = propagationDataExtractor;
35+
return this;
36+
}
37+
38+
public setPropagationDataKey(propagationDataKey: string): AttributePropagatingSpanProcessorBuilder {
39+
if (propagationDataKey == null) {
40+
throw new Error('propagationDataKey must not be null');
41+
}
42+
this.propagationDataKey = propagationDataKey;
43+
return this;
44+
}
45+
46+
public setAttributesKeysToPropagate(attributesKeysToPropagate: string[]): AttributePropagatingSpanProcessorBuilder {
47+
if (attributesKeysToPropagate == null) {
48+
throw new Error('attributesKeysToPropagate must not be null');
49+
}
50+
this.attributesKeysToPropagate = [...attributesKeysToPropagate];
51+
return this;
52+
}
53+
54+
public build(): AttributePropagatingSpanProcessor {
55+
return AttributePropagatingSpanProcessor.create(
56+
this.propagationDataExtractor,
57+
this.propagationDataKey,
58+
this.attributesKeysToPropagate
59+
);
60+
}
61+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Span as APISpan, AttributeValue, Context, SpanKind, trace } from '@opentelemetry/api';
5+
import { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';
6+
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
7+
import { AwsSpanProcessingUtil } from './aws-span-processing-util';
8+
9+
/**
10+
* AttributePropagatingSpanProcessor handles the propagation of attributes from parent spans to
11+
* child spans, specified in {@link attributesKeysToPropagate}. AttributePropagatingSpanProcessor
12+
* also propagates configurable data from parent spans to child spans, as a new attribute specified
13+
* by {@link propagationDataKey}. Propagated data can be configured via the {@link propagationDataExtractor}.
14+
* Span data propagation only starts from local root server/consumer spans, but from there will
15+
* be propagated to any descendant spans. If the span is a CONSUMER PROCESS with the parent also
16+
* a CONSUMER, it will set attribute AWS_CONSUMER_PARENT_SPAN_KIND as CONSUMER to indicate that
17+
* dependency metrics should not be generated for this span.
18+
*/
19+
export class AttributePropagatingSpanProcessor implements SpanProcessor {
20+
private propagationDataExtractor: (span: ReadableSpan) => string;
21+
22+
private propagationDataKey: string;
23+
private attributesKeysToPropagate: string[];
24+
25+
public static create(
26+
propagationDataExtractor: (span: ReadableSpan) => string,
27+
propagationDataKey: string,
28+
attributesKeysToPropagate: string[]
29+
): AttributePropagatingSpanProcessor {
30+
return new AttributePropagatingSpanProcessor(
31+
propagationDataExtractor,
32+
propagationDataKey,
33+
attributesKeysToPropagate
34+
);
35+
}
36+
37+
private constructor(
38+
propagationDataExtractor: (span: ReadableSpan) => string,
39+
propagationDataKey: string,
40+
attributesKeysToPropagate: string[]
41+
) {
42+
this.propagationDataExtractor = propagationDataExtractor;
43+
this.propagationDataKey = propagationDataKey;
44+
this.attributesKeysToPropagate = attributesKeysToPropagate;
45+
}
46+
47+
public onStart(span: Span, parentContext: Context): void {
48+
// Divergence from Java/Python
49+
// Workaround implemented in TypeScript. Calculation of isLocalRoot is not possible
50+
// in `AwsSpanProcessingUtil.isLocalRoot` because the parent context is not accessible
51+
// from a span. Therefore we pre-calculate its value here as an attribute.
52+
AwsSpanProcessingUtil.setIsLocalRootInformation(span, parentContext);
53+
54+
const parentSpan: APISpan | undefined = trace.getSpan(parentContext);
55+
let parentReadableSpan: ReadableSpan | undefined = undefined;
56+
57+
// `if check` is different than Python and Java. Here we just check if parentSpan is not undefined
58+
// Whereas in Python and Java, the check is if parentSpan is and instance of ReadableSpan, which is
59+
// not possible in TypeScript because the check is not allowed for interfaces (such as ReadableSpan).
60+
if (parentSpan !== undefined) {
61+
parentReadableSpan = parentSpan as Span;
62+
63+
// Add the AWS_SDK_DESCENDANT attribute to the immediate child spans of AWS SDK span.
64+
// This attribute helps the backend differentiate between SDK spans and their immediate
65+
// children.
66+
// It's assumed that the HTTP spans are immediate children of the AWS SDK span
67+
// TODO: we should have a contract test to check the immediate children are HTTP span
68+
if (AwsSpanProcessingUtil.isAwsSDKSpan(parentReadableSpan)) {
69+
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_SDK_DESCENDANT, 'true');
70+
}
71+
72+
if (SpanKind.INTERNAL === parentReadableSpan.kind) {
73+
for (const keyToPropagate of this.attributesKeysToPropagate) {
74+
const valueToPropagate: AttributeValue | undefined = parentReadableSpan.attributes[keyToPropagate];
75+
if (valueToPropagate !== undefined) {
76+
span.setAttribute(keyToPropagate, valueToPropagate);
77+
}
78+
}
79+
}
80+
81+
// We cannot guarantee that messaging.operation is set onStart, it could be set after the fact.
82+
// To work around this, add the AWS_CONSUMER_PARENT_SPAN_KIND attribute if parent and child are
83+
// both CONSUMER then check later if a metric should be generated.
84+
if (this.isConsumerKind(span) && this.isConsumerKind(parentReadableSpan)) {
85+
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_CONSUMER_PARENT_SPAN_KIND, SpanKind[parentReadableSpan.kind]);
86+
}
87+
}
88+
89+
let propagationData: AttributeValue | undefined = undefined;
90+
if (AwsSpanProcessingUtil.isLocalRoot(span)) {
91+
if (!this.isServerKind(span)) {
92+
propagationData = this.propagationDataExtractor(span);
93+
}
94+
} else if (parentReadableSpan !== undefined && this.isServerKind(parentReadableSpan)) {
95+
// In TypeScript, perform `parentReadableSpan !== undefined` check
96+
// This should be done in Python and Java as well, but is not as of now
97+
// If parentReadableSpan is not defined, the first `if statement` should occur,
98+
// so that is why it is not a problem for Java/Python...
99+
propagationData = this.propagationDataExtractor(parentReadableSpan);
100+
} else {
101+
// In TypeScript, perform `parentReadableSpan?` check (returns undefined if undefined)
102+
// This should be done in Python and Java as well, but is not as of now
103+
propagationData = parentReadableSpan?.attributes[this.propagationDataKey];
104+
}
105+
106+
if (propagationData !== undefined) {
107+
span.setAttribute(this.propagationDataKey, propagationData);
108+
}
109+
}
110+
111+
private isConsumerKind(span: ReadableSpan): boolean {
112+
return SpanKind.CONSUMER === span.kind;
113+
}
114+
115+
private isServerKind(span: ReadableSpan): boolean {
116+
return SpanKind.SERVER === span.kind;
117+
}
118+
119+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
120+
public onEnd(span: ReadableSpan): void {}
121+
122+
public shutdown(): Promise<void> {
123+
return this.forceFlush();
124+
}
125+
126+
public forceFlush(): Promise<void> {
127+
return Promise.resolve();
128+
}
129+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Resource } from '@opentelemetry/resources';
5+
import { SpanExporter } from '@opentelemetry/sdk-trace-base';
6+
import { AwsMetricAttributeGenerator } from './aws-metric-attribute-generator';
7+
import { AwsMetricAttributesSpanExporter } from './aws-metric-attributes-span-exporter';
8+
import { MetricAttributeGenerator } from './metric-attribute-generator';
9+
10+
export class AwsMetricAttributesSpanExporterBuilder {
11+
// Defaults
12+
private static DEFAULT_GENERATOR: MetricAttributeGenerator = new AwsMetricAttributeGenerator();
13+
14+
// Required builder elements
15+
private delegate: SpanExporter;
16+
private resource: Resource;
17+
18+
// Optional builder elements
19+
private generator: MetricAttributeGenerator = AwsMetricAttributesSpanExporterBuilder.DEFAULT_GENERATOR;
20+
21+
public static create(delegate: SpanExporter, resource: Resource): AwsMetricAttributesSpanExporterBuilder {
22+
return new AwsMetricAttributesSpanExporterBuilder(delegate, resource);
23+
}
24+
25+
private constructor(delegate: SpanExporter, resource: Resource) {
26+
this.delegate = delegate;
27+
this.resource = resource;
28+
}
29+
30+
/**
31+
* Sets the generator used to generate attributes used spancs exported by the exporter. If unset,
32+
* defaults to {@link DEFAULT_GENERATOR}. Must not be null.
33+
*/
34+
public setGenerator(generator: MetricAttributeGenerator): AwsMetricAttributesSpanExporterBuilder {
35+
if (generator == null) {
36+
throw new Error('generator must not be null/undefined');
37+
}
38+
this.generator = generator;
39+
return this;
40+
}
41+
42+
public build(): AwsMetricAttributesSpanExporter {
43+
return AwsMetricAttributesSpanExporter.create(this.delegate, this.generator, this.resource);
44+
}
45+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Attributes } from '@opentelemetry/api';
5+
import { ExportResult } from '@opentelemetry/core';
6+
import { Resource } from '@opentelemetry/resources';
7+
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
8+
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
9+
import { AwsSpanProcessingUtil } from './aws-span-processing-util';
10+
import {
11+
AttributeMap,
12+
DEPENDENCY_METRIC,
13+
MetricAttributeGenerator,
14+
SERVICE_METRIC,
15+
} from './metric-attribute-generator';
16+
17+
/**
18+
* This exporter will update a span with metric attributes before exporting. It depends on a
19+
* {@link SpanExporter} being provided on instantiation, which the AwsMetricAttributesSpanExporter will
20+
* delegate export to. Also, a {@link MetricAttributeGenerator} must be provided, which will provide a
21+
* means to determine attributes which should be applied to the span. Finally, a {@link Resource} must
22+
* be provided, which is used to generate metric attributes.
23+
*
24+
* <p>This exporter should be coupled with the {@link AwsSpanMetricsProcessor} using the same
25+
* {@link MetricAttributeGenerator}. This will result in metrics and spans being produced with
26+
* common attributes.
27+
*/
28+
export class AwsMetricAttributesSpanExporter implements SpanExporter {
29+
private delegate: SpanExporter;
30+
private generator: MetricAttributeGenerator;
31+
private resource: Resource;
32+
33+
/** Use {@link AwsMetricAttributesSpanExporterBuilder} to construct this exporter. */
34+
static create(
35+
delegate: SpanExporter,
36+
generator: MetricAttributeGenerator,
37+
resource: Resource
38+
): AwsMetricAttributesSpanExporter {
39+
return new AwsMetricAttributesSpanExporter(delegate, generator, resource);
40+
}
41+
42+
private constructor(delegate: SpanExporter, generator: MetricAttributeGenerator, resource: Resource) {
43+
this.delegate = delegate;
44+
this.generator = generator;
45+
this.resource = resource;
46+
}
47+
48+
public export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
49+
const modifiedSpans: ReadableSpan[] = this.addMetricAttributes(spans);
50+
this.delegate.export(modifiedSpans, resultCallback);
51+
}
52+
53+
public shutdown(): Promise<void> {
54+
return this.delegate.shutdown();
55+
}
56+
57+
public forceFlush(): Promise<void> {
58+
if (this.delegate.forceFlush !== undefined) {
59+
return this.delegate.forceFlush();
60+
}
61+
return Promise.resolve();
62+
}
63+
64+
private addMetricAttributes(spans: ReadableSpan[]): ReadableSpan[] {
65+
const modifiedSpans: ReadableSpan[] = [];
66+
67+
spans.forEach((span: ReadableSpan) => {
68+
// If the map has no items, no modifications are required. If there is one item, it means the
69+
// span either produces Service or Dependency metric attributes, and in either case we want to
70+
// modify the span with them. If there are two items, the span produces both Service and
71+
// Dependency metric attributes indicating the span is a local dependency root. The Service
72+
// Attributes must be a subset of the Dependency, with the exception of AWS_SPAN_KIND. The
73+
// knowledge that the span is a local root is more important that knowing that it is a
74+
// Dependency metric, so we take all the Dependency metrics but replace AWS_SPAN_KIND with
75+
// LOCAL_ROOT.
76+
77+
const attributeMap: AttributeMap = this.generator.generateMetricAttributeMapFromSpan(span, this.resource);
78+
let attributes: Attributes | undefined = {};
79+
80+
const generatesServiceMetrics: boolean = AwsSpanProcessingUtil.shouldGenerateServiceMetricAttributes(span);
81+
const generatesDependencyMetrics: boolean = AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(span);
82+
83+
if (generatesServiceMetrics && generatesDependencyMetrics) {
84+
attributes = this.copyAttributesWithLocalRoot(attributeMap[DEPENDENCY_METRIC]);
85+
} else if (generatesServiceMetrics) {
86+
attributes = attributeMap[SERVICE_METRIC];
87+
} else if (generatesDependencyMetrics) {
88+
attributes = attributeMap[DEPENDENCY_METRIC];
89+
}
90+
91+
if (attributes !== undefined && Object.keys(attributes).length > 0) {
92+
span = AwsMetricAttributesSpanExporter.wrapSpanWithAttributes(span, attributes);
93+
}
94+
modifiedSpans.push(span);
95+
});
96+
97+
return modifiedSpans;
98+
}
99+
100+
private copyAttributesWithLocalRoot(attributes: Attributes): Attributes {
101+
const updatedAttributes: Attributes = { ...attributes };
102+
delete updatedAttributes[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND];
103+
updatedAttributes[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND] = AwsSpanProcessingUtil.LOCAL_ROOT;
104+
return updatedAttributes;
105+
}
106+
107+
/**
108+
* {@link export} works with a {@link ReadableSpan}, which does not permit modification. However, we
109+
* need to add derived metric attributes to the span. However, we are still able to modify the
110+
* attributes in the span (the attributes itself is readonly, so it cannot be outright replaced).
111+
* This may be risky.
112+
*
113+
* <p>See https://github.com/open-telemetry/opentelemetry-specification/issues/1089 for more
114+
* context on this approach.
115+
*/
116+
private static wrapSpanWithAttributes(span: ReadableSpan, attributes: Attributes): ReadableSpan {
117+
const originalAttributes: Attributes = span.attributes;
118+
const updateAttributes: Attributes = {};
119+
120+
for (const key in originalAttributes) {
121+
updateAttributes[key] = originalAttributes[key];
122+
}
123+
for (const key in attributes) {
124+
updateAttributes[key] = attributes[key];
125+
}
126+
127+
// Bypass `readonly` restriction of ReadableSpan's attributes.
128+
// Workaround provided from official TypeScript docs:
129+
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#improved-control-over-mapped-type-modifiers
130+
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
131+
const mutableSpan: Mutable<ReadableSpan> = span;
132+
mutableSpan.attributes = updateAttributes;
133+
134+
return span;
135+
}
136+
}

0 commit comments

Comments
 (0)