Skip to content

AppSignals Functionality - add ADOT Span Processors and Exporter #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 9, 2024
Merged
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
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