Skip to content

Commit

Permalink
AppSignals Functionality - add ADOT Span Processors and Exporter (#11)
Browse files Browse the repository at this point in the history
*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
jj22ee authored Aug 9, 2024
1 parent 486474d commit 2d59af7
Show file tree
Hide file tree
Showing 15 changed files with 2,197 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export class AlwaysRecordSampler implements Sampler {
}

private constructor(rootSampler: Sampler) {
if (rootSampler === null) {
throw new Error('rootSampler is null. It must be provided');
if (rootSampler == null) {
throw new Error('rootSampler is null/undefined. It must be provided');
}
this.rootSampler = rootSampler;
}
Expand Down
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
);
}
}
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();
}
}
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);
}
}
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;
}
}
Loading

0 comments on commit 2d59af7

Please sign in to comment.