Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
SEMATTRS_AWS_DYNAMODB_TABLE_NAMES,
SEMATTRS_MESSAGING_DESTINATION,
SEMATTRS_MESSAGING_URL,
} from '@opentelemetry/semantic-conventions';

// Utility class holding attribute keys with special meaning to AWS components
export const AWS_ATTRIBUTE_KEYS: { [key: string]: string } = {
AWS_SPAN_KIND: 'aws.span.kind',
Expand All @@ -20,13 +26,11 @@ export const AWS_ATTRIBUTE_KEYS: { [key: string]: string } = {
AWS_IS_LOCAL_ROOT: 'aws.is.local.root',

// Divergence from Java/Python
// TODO: Audit this: These will most definitely be different in JavaScript.
// For example:
// - `messaging.url` for AWS_QUEUE_URL
// - `aws.dynamodb.table_names` for AWS_TABLE_NAME
AWS_BUCKET_NAME: 'aws.bucket.name',
AWS_QUEUE_URL: 'aws.queue.url',
AWS_QUEUE_NAME: 'aws.queue.name',
AWS_STREAM_NAME: 'aws.stream.name',
AWS_TABLE_NAME: 'aws.table.name',
// For consistency between ADOT SDK languages, the attribute Key name is named similarly to Java/Python,
// while the value is different to accommodate the actual attribute set from OTel JS instrumentations
AWS_BUCKET_NAME: 'aws.s3.bucket',
AWS_QUEUE_URL: SEMATTRS_MESSAGING_URL,
AWS_QUEUE_NAME: SEMATTRS_MESSAGING_DESTINATION,
AWS_STREAM_NAME: 'aws.kinesis.stream.name',
AWS_TABLE_NAMES: SEMATTRS_AWS_DYNAMODB_TABLE_NAMES,
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { Attributes, AttributeValue, diag, SpanKind } from '@opentelemetry/api';
import { Resource, defaultServiceName } from '@opentelemetry/resources';
import { defaultServiceName, Resource } from '@opentelemetry/resources';
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import {
SEMATTRS_DB_CONNECTION_STRING,
Expand Down Expand Up @@ -340,10 +340,13 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
let remoteResourceIdentifier: AttributeValue | undefined;

if (AwsSpanProcessingUtil.isAwsSDKSpan(span)) {
if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME)) {
if (
AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES) &&
(span.attributes[AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES] as string[]).length === 1
) {
remoteResourceType = NORMALIZED_DYNAMO_DB_SERVICE_NAME + '::Table';
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME]
(span.attributes[AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES] as string[])[0]
);
} else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_STREAM_NAME)) {
remoteResourceType = NORMALIZED_KINESIS_SERVICE_NAME + '::Stream';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

import { diag } from '@opentelemetry/api';
import {
getNodeAutoInstrumentations,
getResourceDetectors as getResourceDetectorsFromEnv,
} from '@opentelemetry/auto-instrumentations-node';
import { getResourceDetectors as getResourceDetectorsFromEnv } from '@opentelemetry/auto-instrumentations-node';
import { Instrumentation } from '@opentelemetry/instrumentation';
import { awsEc2Detector, awsEcsDetector, awsEksDetector } from '@opentelemetry/resource-detector-aws';
import {
Detector,
Expand Down Expand Up @@ -37,8 +35,9 @@ const APPLICATION_SIGNALS_ENABLED_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS
*/
export class AwsOpentelemetryConfigurator {
private resource: Resource;
private instrumentations: Instrumentation[];

constructor() {
constructor(instrumentations: Instrumentation[]) {
/*
* Set and Detect Resources via Resource Detectors
*
Expand Down Expand Up @@ -88,6 +87,8 @@ export class AwsOpentelemetryConfigurator {
autoResource = autoResource.merge(detectResourcesSync(internalConfig));

this.resource = autoResource;

this.instrumentations = instrumentations;
}

private customizeVersions(autoResource: Resource): Resource {
Expand All @@ -106,14 +107,14 @@ export class AwsOpentelemetryConfigurator {
if (this.isApplicationSignalsEnabled()) {
// TODO: This is a placeholder config. This will be replaced with an ADOT config in a future commit.
config = {
instrumentations: getNodeAutoInstrumentations(),
instrumentations: this.instrumentations,
resource: this.resource,
autoDetectResources: false,
};
} else {
// Default experience config
config = {
instrumentations: getNodeAutoInstrumentations(),
instrumentations: this.instrumentations,
resource: this.resource,
autoDetectResources: false,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { DiagLogger, Span, SpanAttributes, SpanKind, Tracer } from '@opentelemetry/api';
import {
AwsSdkInstrumentationConfig,
NormalizedRequest,
NormalizedResponse,
} from '@opentelemetry/instrumentation-aws-sdk';

// The OpenTelemetry Authors code
// We need to copy these interfaces that are not exported by Opentelemetry
export interface RequestMetadata {
// isIncoming - if true, then the operation callback / promise should be bind with the operation's span
isIncoming: boolean;
spanAttributes?: SpanAttributes;
spanKind?: SpanKind;
spanName?: string;
}

export interface ServiceExtension {
// called before request is sent, and before span is started
requestPreSpanHook: (
request: NormalizedRequest,
config: AwsSdkInstrumentationConfig,
diag: DiagLogger
) => RequestMetadata;

// called before request is sent, and after span is started
requestPostSpanHook?: (request: NormalizedRequest) => void;

responseHook?: (
response: NormalizedResponse,
span: Span,
tracer: Tracer,
config: AwsSdkInstrumentationConfig
) => void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export { KinesisServiceExtension } from './kinesis';
export { S3ServiceExtension } from './s3';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Attributes, Span, SpanKind, Tracer } from '@opentelemetry/api';
import {
AwsSdkInstrumentationConfig,
NormalizedRequest,
NormalizedResponse,
} from '@opentelemetry/instrumentation-aws-sdk';
import { RequestMetadata, ServiceExtension } from './ServiceExtension';

// The OpenTelemetry Authors code
// This file's contents are being contributed to upstream
// - https://github.com/open-telemetry/opentelemetry-js-contrib/pull/2361

const _AWS_KINESIS_STREAM_NAME = 'aws.kinesis.stream.name';

export class KinesisServiceExtension implements ServiceExtension {
requestPreSpanHook(request: NormalizedRequest, _config: AwsSdkInstrumentationConfig): RequestMetadata {
const streamName = request.commandInput.StreamName;

const spanKind: SpanKind = SpanKind.CLIENT;
let spanName: string | undefined;

const spanAttributes: Attributes = {};

if (streamName) {
spanAttributes[_AWS_KINESIS_STREAM_NAME] = streamName;
}

const isIncoming = false;

return {
isIncoming,
spanAttributes,
spanKind,
spanName,
};
}

requestPostSpanHook = (request: NormalizedRequest) => {};

responseHook = (response: NormalizedResponse, span: Span, tracer: Tracer, config: AwsSdkInstrumentationConfig) => {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Attributes, Span, SpanKind, Tracer } from '@opentelemetry/api';
import {
AwsSdkInstrumentationConfig,
NormalizedRequest,
NormalizedResponse,
} from '@opentelemetry/instrumentation-aws-sdk';
import { RequestMetadata, ServiceExtension } from './ServiceExtension';

// The OpenTelemetry Authors code
// This file's contents are being contributed to upstream
// - https://github.com/open-telemetry/opentelemetry-js-contrib/pull/2361

const _AWS_S3_BUCKET = 'aws.s3.bucket';

export class S3ServiceExtension implements ServiceExtension {
requestPreSpanHook(request: NormalizedRequest, _config: AwsSdkInstrumentationConfig): RequestMetadata {
const bucketName = request.commandInput.Bucket;

const spanKind: SpanKind = SpanKind.CLIENT;
let spanName: string | undefined;

const spanAttributes: Attributes = {};

if (bucketName) {
spanAttributes[_AWS_S3_BUCKET] = bucketName;
}

const isIncoming = false;

return {
isIncoming,
spanAttributes,
spanKind,
spanName,
};
}

requestPostSpanHook = (request: NormalizedRequest) => {};

responseHook = (response: NormalizedResponse, span: Span, tracer: Tracer, config: AwsSdkInstrumentationConfig) => {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Instrumentation } from '@opentelemetry/instrumentation';
import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';
import { KinesisServiceExtension, S3ServiceExtension } from './aws/services';

export function applyInstrumentationPatches(instrumentations: Instrumentation[]): void {
/*
Apply patches to upstream instrumentation libraries.

This method is invoked to apply changes to upstream instrumentation libraries, typically when changes to upstream
are required on a timeline that cannot wait for upstream release. Generally speaking, patches should be short-term
local solutions that are comparable to long-term upstream solutions.

Where possible, automated testing should be run to catch upstream changes resulting in broken patches
*/
instrumentations.forEach(instrumentation => {
if (instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-sdk') {
// Access private property servicesExtensions of AwsInstrumentation
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const services = (instrumentation as AwsInstrumentation).servicesExtensions?.services;
if (services) {
services.set('S3', new S3ServiceExtension());
services.set('Kinesis', new KinesisServiceExtension());
}
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
// SPDX-License-Identifier: Apache-2.0

import { DiagConsoleLogger, diag } from '@opentelemetry/api';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Instrumentation } from '@opentelemetry/instrumentation';
import * as opentelemetry from '@opentelemetry/sdk-node';
import { AwsOpentelemetryConfigurator } from './aws-opentelemetry-configurator';
import { applyInstrumentationPatches } from './patches/instrumentation-patch';

diag.setLogger(new DiagConsoleLogger(), opentelemetry.core.getEnv().OTEL_LOG_LEVEL);

Expand Down Expand Up @@ -31,7 +34,14 @@ if (!process.env.OTEL_PROPAGATORS) {
process.env.OTEL_PROPAGATORS = 'xray,tracecontext,b3,b3multi';
}

const configurator: AwsOpentelemetryConfigurator = new AwsOpentelemetryConfigurator();
const instrumentations: Instrumentation[] = getNodeAutoInstrumentations();

// Apply instrumentation patches by default
if (process.env.AWS_APPLY_PATCHES === undefined || process.env.AWS_APPLY_PATCHES?.toLowerCase() === 'true') {
applyInstrumentationPatches(instrumentations);
}

const configurator: AwsOpentelemetryConfigurator = new AwsOpentelemetryConfigurator(instrumentations);
const configuration: Partial<opentelemetry.NodeSDKConfiguration> = configurator.configure();

const sdk: opentelemetry.NodeSDK = new opentelemetry.NodeSDK(configuration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -760,20 +760,30 @@ describe('AwsMetricAttributeGeneratorTest', () => {
validateRemoteResourceAttributes('AWS::Kinesis::Stream', 'aws_stream_name');
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STREAM_NAME, undefined);

// Validate behaviour of AWS_TABLE_NAME attribute, then remove it.
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, 'aws_table_name');
// Validate behaviour of AWS_TABLE_NAMES attribute with one table name, then remove it.
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, ['aws_table_name']);
validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table_name');
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, undefined);
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, undefined);

// Validate behaviour of AWS_TABLE_NAME attribute with special chars(|), then remove it.
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, 'aws_table|name');
// Validate behaviour of AWS_TABLE_NAMES attribute with no table name, then remove it.
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, []);
validateRemoteResourceAttributes(undefined, undefined);
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, undefined);

// Validate behaviour of AWS_TABLE_NAMES attribute with two table names, then remove it.
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, ['aws_table_name1', 'aws_table_name2']);
validateRemoteResourceAttributes(undefined, undefined);
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, undefined);

// Validate behaviour of AWS_TABLE_NAMES attribute with special chars(|), then remove it.
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, ['aws_table|name']);
validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table^|name');
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, undefined);
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, undefined);

// Validate behaviour of AWS_TABLE_NAME attribute with special chars(^), then remove it.
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, 'aws_table^name');
// Validate behaviour of AWS_TABLE_NAMES attribute with special chars(^), then remove it.
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, ['aws_table^name']);
validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table^^name');
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, undefined);
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAMES, undefined);
});

it('testDBClientSpanWithRemoteResourceAttributes', () => {
Expand Down Expand Up @@ -1010,7 +1020,7 @@ describe('AwsMetricAttributeGeneratorTest', () => {
mockAttribute(SEMATTRS_PEER_SERVICE, undefined);
}

function validateRemoteResourceAttributes(type: string, identifier: string): void {
function validateRemoteResourceAttributes(type: string | undefined, identifier: string | undefined): void {
// Client, Producer and Consumer spans should generate the expected remote resource attributes
(spanDataMock as any).kind = SpanKind.CLIENT;
let actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Instrumentation } from '@opentelemetry/instrumentation';
import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';
import { expect } from 'expect';
import { applyInstrumentationPatches } from './../../src/patches/instrumentation-patch';

describe('InstrumentationPatchTest', () => {
it('PatchAwsSdkInstrumentation', () => {
const instrumentations: Instrumentation[] = getNodeAutoInstrumentations();
applyInstrumentationPatches(instrumentations);

const filteredInstrumentations: Instrumentation[] = instrumentations.filter(
instrumentation => instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-sdk'
);
expect(filteredInstrumentations.length).toEqual(1);

const awsSdkInstrumentation = filteredInstrumentations[0];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const services: Map<string, ServiceExtension> = (awsSdkInstrumentation as AwsInstrumentation).servicesExtensions
?.services;
// Not from patching
expect(services.has('SQS')).toBeTruthy();
expect(services.has('SNS')).toBeTruthy();
expect(services.has('DynamoDB')).toBeTruthy();
expect(services.has('Lambda')).toBeTruthy();
// From patching
expect(services.has('S3')).toBeTruthy();
expect(services.has('Kinesis')).toBeTruthy();
// Sanity check
expect(services.has('InvalidService')).toBeFalsy();
});
});