Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"@aws-sdk/client-sfn": "^3.632.0",
"@aws-sdk/client-sns": "^3.632.0",
"@opentelemetry/contrib-test-utils": "0.41.0",
"@smithy/protocol-http": "^5.0.1",
"@smithy/signature-v4": "^5.0.1",
"@types/mocha": "7.0.2",
"@types/node": "18.6.5",
"@types/sinon": "10.0.18",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@
import { AwsMetricAttributesSpanExporterBuilder } from './aws-metric-attributes-span-exporter-builder';
import { AwsSpanMetricsProcessorBuilder } from './aws-span-metrics-processor-builder';
import { OTLPUdpSpanExporter } from './otlp-udp-exporter';
import { OTLPAwsSpanExporter } from './otlp-aws-span-exporter';
import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler';
// This file is generated via `npm run compile`
import { LIB_VERSION } from './version';

const XRAY_OTLP_ENDPOINT_PATTERN = '^https://xray.([a-z0-9-]+).amazonaws.com/v1/traces$';

const APPLICATION_SIGNALS_ENABLED_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_ENABLED';
const APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT';
const METRIC_EXPORT_INTERVAL_CONFIG: string = 'OTEL_METRIC_EXPORT_INTERVAL';
Expand Down Expand Up @@ -422,6 +425,7 @@
private resource: Resource;

static configureOtlp(): SpanExporter {
const otlp_exporter_traces_endpoint = process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'];
// eslint-disable-next-line @typescript-eslint/typedef
let protocol = this.getOtlpProtocol();

Expand All @@ -438,12 +442,18 @@
case 'http/json':
return new OTLPHttpTraceExporter();
case 'http/protobuf':
if (otlp_exporter_traces_endpoint && isXrayOtlpEndpoint(otlp_exporter_traces_endpoint)) {
return new OTLPAwsSpanExporter(otlp_exporter_traces_endpoint);
}
return new OTLPProtoTraceExporter();
case 'udp':
diag.debug('Detected AWS Lambda environment and enabling UDPSpanExporter');
return new OTLPUdpSpanExporter(getXrayDaemonEndpoint(), FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX);
default:
diag.warn(`Unsupported OTLP traces protocol: ${protocol}. Using http/protobuf.`);
if (otlp_exporter_traces_endpoint && isXrayOtlpEndpoint(otlp_exporter_traces_endpoint)) {
return new OTLPAwsSpanExporter(otlp_exporter_traces_endpoint);
}
return new OTLPProtoTraceExporter();
}
}
Expand Down Expand Up @@ -652,4 +662,8 @@
return process.env[AWS_XRAY_DAEMON_ADDRESS_CONFIG];
}

function isXrayOtlpEndpoint(otlpEndpoint: string | undefined) {
return otlpEndpoint && new RegExp(XRAY_OTLP_ENDPOINT_PATTERN).test(otlpEndpoint.toLowerCase());
}

// END The OpenTelemetry Authors code
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { diag } from '@opentelemetry/api';
import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base';
import { ExportResult } from '@opentelemetry/core';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
import { Sha256 } from '@aws-crypto/sha256-js';
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import { SignatureV4 } from '@smithy/signature-v4';
import { ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer';
import { HttpRequest } from '@smithy/protocol-http';

const SERVICE_NAME = 'xray';

/**
* This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported
* to the XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. Utilizes the aws-sdk
* library to sign and directly inject SigV4 Authentication to the exported request's headers. <a
* href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html">...</a>
*/
export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter {
private endpoint: string;
private region: string;

constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) {
super(config);
this.region = endpoint.split('.')[1];
this.endpoint = endpoint;
}

/**
* Overrides the upstream implementation of export. All behaviors are the same except if the
* endpoint is an XRay OTLP endpoint, we will sign the request with SigV4 in headers before
* sending it to the endpoint. Otherwise, we will skip signing.
*/
public override async export(items: ReadableSpan[], resultCallback: (result: ExportResult) => void): Promise<void> {
const url = new URL(this.endpoint);
const serializedSpans: Uint8Array | undefined = ProtobufTraceSerializer.serializeRequest(items);

if (serializedSpans === undefined) {
return;
}

/*
This is bad practice but there is no other way to access and inject SigV4 headers
into the request headers before the traces get exported.
*/
const oldHeaders = (this as any)._transport._transport._parameters.headers;

const request = new HttpRequest({
method: 'POST',
protocol: 'https',
hostname: url.hostname,
path: url.pathname,
body: serializedSpans,
headers: {
...oldHeaders,
host: url.hostname,
},
});

try {
const signer = new SignatureV4({
credentials: defaultProvider(),
region: this.region,
service: SERVICE_NAME,
sha256: Sha256,
});

const signedRequest = await signer.sign(request);

(this as any)._transport._transport._parameters.headers = signedRequest.headers;
} catch (exception) {
diag.debug(
`Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}`
);
}

await super.export(items, resultCallback);
}
}
192 changes: 189 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading