-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
15 changed files
with
2,197 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
...pentelemetry-node-autoinstrumentation/src/attribute-propagating-span-processor-builder.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; | ||
import { AttributePropagatingSpanProcessor } from './attribute-propagating-span-processor'; | ||
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys'; | ||
import { AwsSpanProcessingUtil } from './aws-span-processing-util'; | ||
|
||
/** | ||
* AttributePropagatingSpanProcessorBuilder is used to construct a {@link AttributePropagatingSpanProcessor}. | ||
* If {@link setPropagationDataExtractor}, {@link setPropagationDataKey} or {@link setAttributesKeysToPropagate} | ||
* are not invoked, the builder defaults to using specific propagation targets. | ||
*/ | ||
export class AttributePropagatingSpanProcessorBuilder { | ||
private propagationDataExtractor: (span: ReadableSpan) => string = AwsSpanProcessingUtil.getIngressOperation; | ||
private propagationDataKey: string = AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION; | ||
private attributesKeysToPropagate: string[] = [ | ||
AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, | ||
AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, | ||
]; | ||
|
||
public static create(): AttributePropagatingSpanProcessorBuilder { | ||
return new AttributePropagatingSpanProcessorBuilder(); | ||
} | ||
|
||
private constructor() {} | ||
|
||
public setPropagationDataExtractor( | ||
propagationDataExtractor: (span: ReadableSpan) => string | ||
): AttributePropagatingSpanProcessorBuilder { | ||
if (propagationDataExtractor == null) { | ||
throw new Error('propagationDataExtractor must not be null'); | ||
} | ||
this.propagationDataExtractor = propagationDataExtractor; | ||
return this; | ||
} | ||
|
||
public setPropagationDataKey(propagationDataKey: string): AttributePropagatingSpanProcessorBuilder { | ||
if (propagationDataKey == null) { | ||
throw new Error('propagationDataKey must not be null'); | ||
} | ||
this.propagationDataKey = propagationDataKey; | ||
return this; | ||
} | ||
|
||
public setAttributesKeysToPropagate(attributesKeysToPropagate: string[]): AttributePropagatingSpanProcessorBuilder { | ||
if (attributesKeysToPropagate == null) { | ||
throw new Error('attributesKeysToPropagate must not be null'); | ||
} | ||
this.attributesKeysToPropagate = [...attributesKeysToPropagate]; | ||
return this; | ||
} | ||
|
||
public build(): AttributePropagatingSpanProcessor { | ||
return AttributePropagatingSpanProcessor.create( | ||
this.propagationDataExtractor, | ||
this.propagationDataKey, | ||
this.attributesKeysToPropagate | ||
); | ||
} | ||
} |
129 changes: 129 additions & 0 deletions
129
...distro-opentelemetry-node-autoinstrumentation/src/attribute-propagating-span-processor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { Span as APISpan, AttributeValue, Context, SpanKind, trace } from '@opentelemetry/api'; | ||
import { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base'; | ||
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys'; | ||
import { AwsSpanProcessingUtil } from './aws-span-processing-util'; | ||
|
||
/** | ||
* AttributePropagatingSpanProcessor handles the propagation of attributes from parent spans to | ||
* child spans, specified in {@link attributesKeysToPropagate}. AttributePropagatingSpanProcessor | ||
* also propagates configurable data from parent spans to child spans, as a new attribute specified | ||
* by {@link propagationDataKey}. Propagated data can be configured via the {@link propagationDataExtractor}. | ||
* Span data propagation only starts from local root server/consumer spans, but from there will | ||
* be propagated to any descendant spans. If the span is a CONSUMER PROCESS with the parent also | ||
* a CONSUMER, it will set attribute AWS_CONSUMER_PARENT_SPAN_KIND as CONSUMER to indicate that | ||
* dependency metrics should not be generated for this span. | ||
*/ | ||
export class AttributePropagatingSpanProcessor implements SpanProcessor { | ||
private propagationDataExtractor: (span: ReadableSpan) => string; | ||
|
||
private propagationDataKey: string; | ||
private attributesKeysToPropagate: string[]; | ||
|
||
public static create( | ||
propagationDataExtractor: (span: ReadableSpan) => string, | ||
propagationDataKey: string, | ||
attributesKeysToPropagate: string[] | ||
): AttributePropagatingSpanProcessor { | ||
return new AttributePropagatingSpanProcessor( | ||
propagationDataExtractor, | ||
propagationDataKey, | ||
attributesKeysToPropagate | ||
); | ||
} | ||
|
||
private constructor( | ||
propagationDataExtractor: (span: ReadableSpan) => string, | ||
propagationDataKey: string, | ||
attributesKeysToPropagate: string[] | ||
) { | ||
this.propagationDataExtractor = propagationDataExtractor; | ||
this.propagationDataKey = propagationDataKey; | ||
this.attributesKeysToPropagate = attributesKeysToPropagate; | ||
} | ||
|
||
public onStart(span: Span, parentContext: Context): void { | ||
// Divergence from Java/Python | ||
// Workaround implemented in TypeScript. Calculation of isLocalRoot is not possible | ||
// in `AwsSpanProcessingUtil.isLocalRoot` because the parent context is not accessible | ||
// from a span. Therefore we pre-calculate its value here as an attribute. | ||
AwsSpanProcessingUtil.setIsLocalRootInformation(span, parentContext); | ||
|
||
const parentSpan: APISpan | undefined = trace.getSpan(parentContext); | ||
let parentReadableSpan: ReadableSpan | undefined = undefined; | ||
|
||
// `if check` is different than Python and Java. Here we just check if parentSpan is not undefined | ||
// Whereas in Python and Java, the check is if parentSpan is and instance of ReadableSpan, which is | ||
// not possible in TypeScript because the check is not allowed for interfaces (such as ReadableSpan). | ||
if (parentSpan !== undefined) { | ||
parentReadableSpan = parentSpan as Span; | ||
|
||
// Add the AWS_SDK_DESCENDANT attribute to the immediate child spans of AWS SDK span. | ||
// This attribute helps the backend differentiate between SDK spans and their immediate | ||
// children. | ||
// It's assumed that the HTTP spans are immediate children of the AWS SDK span | ||
// TODO: we should have a contract test to check the immediate children are HTTP span | ||
if (AwsSpanProcessingUtil.isAwsSDKSpan(parentReadableSpan)) { | ||
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_SDK_DESCENDANT, 'true'); | ||
} | ||
|
||
if (SpanKind.INTERNAL === parentReadableSpan.kind) { | ||
for (const keyToPropagate of this.attributesKeysToPropagate) { | ||
const valueToPropagate: AttributeValue | undefined = parentReadableSpan.attributes[keyToPropagate]; | ||
if (valueToPropagate !== undefined) { | ||
span.setAttribute(keyToPropagate, valueToPropagate); | ||
} | ||
} | ||
} | ||
|
||
// We cannot guarantee that messaging.operation is set onStart, it could be set after the fact. | ||
// To work around this, add the AWS_CONSUMER_PARENT_SPAN_KIND attribute if parent and child are | ||
// both CONSUMER then check later if a metric should be generated. | ||
if (this.isConsumerKind(span) && this.isConsumerKind(parentReadableSpan)) { | ||
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_CONSUMER_PARENT_SPAN_KIND, SpanKind[parentReadableSpan.kind]); | ||
} | ||
} | ||
|
||
let propagationData: AttributeValue | undefined = undefined; | ||
if (AwsSpanProcessingUtil.isLocalRoot(span)) { | ||
if (!this.isServerKind(span)) { | ||
propagationData = this.propagationDataExtractor(span); | ||
} | ||
} else if (parentReadableSpan !== undefined && this.isServerKind(parentReadableSpan)) { | ||
// In TypeScript, perform `parentReadableSpan !== undefined` check | ||
// This should be done in Python and Java as well, but is not as of now | ||
// If parentReadableSpan is not defined, the first `if statement` should occur, | ||
// so that is why it is not a problem for Java/Python... | ||
propagationData = this.propagationDataExtractor(parentReadableSpan); | ||
} else { | ||
// In TypeScript, perform `parentReadableSpan?` check (returns undefined if undefined) | ||
// This should be done in Python and Java as well, but is not as of now | ||
propagationData = parentReadableSpan?.attributes[this.propagationDataKey]; | ||
} | ||
|
||
if (propagationData !== undefined) { | ||
span.setAttribute(this.propagationDataKey, propagationData); | ||
} | ||
} | ||
|
||
private isConsumerKind(span: ReadableSpan): boolean { | ||
return SpanKind.CONSUMER === span.kind; | ||
} | ||
|
||
private isServerKind(span: ReadableSpan): boolean { | ||
return SpanKind.SERVER === span.kind; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
public onEnd(span: ReadableSpan): void {} | ||
|
||
public shutdown(): Promise<void> { | ||
return this.forceFlush(); | ||
} | ||
|
||
public forceFlush(): Promise<void> { | ||
return Promise.resolve(); | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
...opentelemetry-node-autoinstrumentation/src/aws-metric-attributes-span-exporter-builder.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { Resource } from '@opentelemetry/resources'; | ||
import { SpanExporter } from '@opentelemetry/sdk-trace-base'; | ||
import { AwsMetricAttributeGenerator } from './aws-metric-attribute-generator'; | ||
import { AwsMetricAttributesSpanExporter } from './aws-metric-attributes-span-exporter'; | ||
import { MetricAttributeGenerator } from './metric-attribute-generator'; | ||
|
||
export class AwsMetricAttributesSpanExporterBuilder { | ||
// Defaults | ||
private static DEFAULT_GENERATOR: MetricAttributeGenerator = new AwsMetricAttributeGenerator(); | ||
|
||
// Required builder elements | ||
private delegate: SpanExporter; | ||
private resource: Resource; | ||
|
||
// Optional builder elements | ||
private generator: MetricAttributeGenerator = AwsMetricAttributesSpanExporterBuilder.DEFAULT_GENERATOR; | ||
|
||
public static create(delegate: SpanExporter, resource: Resource): AwsMetricAttributesSpanExporterBuilder { | ||
return new AwsMetricAttributesSpanExporterBuilder(delegate, resource); | ||
} | ||
|
||
private constructor(delegate: SpanExporter, resource: Resource) { | ||
this.delegate = delegate; | ||
this.resource = resource; | ||
} | ||
|
||
/** | ||
* Sets the generator used to generate attributes used spancs exported by the exporter. If unset, | ||
* defaults to {@link DEFAULT_GENERATOR}. Must not be null. | ||
*/ | ||
public setGenerator(generator: MetricAttributeGenerator): AwsMetricAttributesSpanExporterBuilder { | ||
if (generator == null) { | ||
throw new Error('generator must not be null/undefined'); | ||
} | ||
this.generator = generator; | ||
return this; | ||
} | ||
|
||
public build(): AwsMetricAttributesSpanExporter { | ||
return AwsMetricAttributesSpanExporter.create(this.delegate, this.generator, this.resource); | ||
} | ||
} |
136 changes: 136 additions & 0 deletions
136
aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attributes-span-exporter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { Attributes } from '@opentelemetry/api'; | ||
import { ExportResult } from '@opentelemetry/core'; | ||
import { Resource } from '@opentelemetry/resources'; | ||
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; | ||
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys'; | ||
import { AwsSpanProcessingUtil } from './aws-span-processing-util'; | ||
import { | ||
AttributeMap, | ||
DEPENDENCY_METRIC, | ||
MetricAttributeGenerator, | ||
SERVICE_METRIC, | ||
} from './metric-attribute-generator'; | ||
|
||
/** | ||
* This exporter will update a span with metric attributes before exporting. It depends on a | ||
* {@link SpanExporter} being provided on instantiation, which the AwsMetricAttributesSpanExporter will | ||
* delegate export to. Also, a {@link MetricAttributeGenerator} must be provided, which will provide a | ||
* means to determine attributes which should be applied to the span. Finally, a {@link Resource} must | ||
* be provided, which is used to generate metric attributes. | ||
* | ||
* <p>This exporter should be coupled with the {@link AwsSpanMetricsProcessor} using the same | ||
* {@link MetricAttributeGenerator}. This will result in metrics and spans being produced with | ||
* common attributes. | ||
*/ | ||
export class AwsMetricAttributesSpanExporter implements SpanExporter { | ||
private delegate: SpanExporter; | ||
private generator: MetricAttributeGenerator; | ||
private resource: Resource; | ||
|
||
/** Use {@link AwsMetricAttributesSpanExporterBuilder} to construct this exporter. */ | ||
static create( | ||
delegate: SpanExporter, | ||
generator: MetricAttributeGenerator, | ||
resource: Resource | ||
): AwsMetricAttributesSpanExporter { | ||
return new AwsMetricAttributesSpanExporter(delegate, generator, resource); | ||
} | ||
|
||
private constructor(delegate: SpanExporter, generator: MetricAttributeGenerator, resource: Resource) { | ||
this.delegate = delegate; | ||
this.generator = generator; | ||
this.resource = resource; | ||
} | ||
|
||
public export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void { | ||
const modifiedSpans: ReadableSpan[] = this.addMetricAttributes(spans); | ||
this.delegate.export(modifiedSpans, resultCallback); | ||
} | ||
|
||
public shutdown(): Promise<void> { | ||
return this.delegate.shutdown(); | ||
} | ||
|
||
public forceFlush(): Promise<void> { | ||
if (this.delegate.forceFlush !== undefined) { | ||
return this.delegate.forceFlush(); | ||
} | ||
return Promise.resolve(); | ||
} | ||
|
||
private addMetricAttributes(spans: ReadableSpan[]): ReadableSpan[] { | ||
const modifiedSpans: ReadableSpan[] = []; | ||
|
||
spans.forEach((span: ReadableSpan) => { | ||
// If the map has no items, no modifications are required. If there is one item, it means the | ||
// span either produces Service or Dependency metric attributes, and in either case we want to | ||
// modify the span with them. If there are two items, the span produces both Service and | ||
// Dependency metric attributes indicating the span is a local dependency root. The Service | ||
// Attributes must be a subset of the Dependency, with the exception of AWS_SPAN_KIND. The | ||
// knowledge that the span is a local root is more important that knowing that it is a | ||
// Dependency metric, so we take all the Dependency metrics but replace AWS_SPAN_KIND with | ||
// LOCAL_ROOT. | ||
|
||
const attributeMap: AttributeMap = this.generator.generateMetricAttributeMapFromSpan(span, this.resource); | ||
let attributes: Attributes | undefined = {}; | ||
|
||
const generatesServiceMetrics: boolean = AwsSpanProcessingUtil.shouldGenerateServiceMetricAttributes(span); | ||
const generatesDependencyMetrics: boolean = AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(span); | ||
|
||
if (generatesServiceMetrics && generatesDependencyMetrics) { | ||
attributes = this.copyAttributesWithLocalRoot(attributeMap[DEPENDENCY_METRIC]); | ||
} else if (generatesServiceMetrics) { | ||
attributes = attributeMap[SERVICE_METRIC]; | ||
} else if (generatesDependencyMetrics) { | ||
attributes = attributeMap[DEPENDENCY_METRIC]; | ||
} | ||
|
||
if (attributes !== undefined && Object.keys(attributes).length > 0) { | ||
span = AwsMetricAttributesSpanExporter.wrapSpanWithAttributes(span, attributes); | ||
} | ||
modifiedSpans.push(span); | ||
}); | ||
|
||
return modifiedSpans; | ||
} | ||
|
||
private copyAttributesWithLocalRoot(attributes: Attributes): Attributes { | ||
const updatedAttributes: Attributes = { ...attributes }; | ||
delete updatedAttributes[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]; | ||
updatedAttributes[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND] = AwsSpanProcessingUtil.LOCAL_ROOT; | ||
return updatedAttributes; | ||
} | ||
|
||
/** | ||
* {@link export} works with a {@link ReadableSpan}, which does not permit modification. However, we | ||
* need to add derived metric attributes to the span. However, we are still able to modify the | ||
* attributes in the span (the attributes itself is readonly, so it cannot be outright replaced). | ||
* This may be risky. | ||
* | ||
* <p>See https://github.com/open-telemetry/opentelemetry-specification/issues/1089 for more | ||
* context on this approach. | ||
*/ | ||
private static wrapSpanWithAttributes(span: ReadableSpan, attributes: Attributes): ReadableSpan { | ||
const originalAttributes: Attributes = span.attributes; | ||
const updateAttributes: Attributes = {}; | ||
|
||
for (const key in originalAttributes) { | ||
updateAttributes[key] = originalAttributes[key]; | ||
} | ||
for (const key in attributes) { | ||
updateAttributes[key] = attributes[key]; | ||
} | ||
|
||
// Bypass `readonly` restriction of ReadableSpan's attributes. | ||
// Workaround provided from official TypeScript docs: | ||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#improved-control-over-mapped-type-modifiers | ||
type Mutable<T> = { -readonly [P in keyof T]: T[P] }; | ||
const mutableSpan: Mutable<ReadableSpan> = span; | ||
mutableSpan.attributes = updateAttributes; | ||
|
||
return span; | ||
} | ||
} |
Oops, something went wrong.