Skip to content

Commit fa9a211

Browse files
authored
SigV4 Authentication support for http/protobuf exporter (#156)
*Issue #, if available:* Adding SigV4 Authentication extension for Exporting traces to OTLP XRay endpoint without needing to explictily install the collector. *Description of changes:* Added a new class that extends upstream's OTLP http/protobuf span exporter. Overrides the export method so that if the endpoint is CW, we add an extra step of injecting SigV4 authentication to the headers. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 7017193 commit fa9a211

File tree

6 files changed

+597
-4
lines changed

6 files changed

+597
-4
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"src/**/*.ts"
4141
],
4242
"exclude": [
43-
"src/third-party/**/*.ts"
43+
"src/third-party/**/*.ts",
44+
"src/otlp-aws-span-exporter.ts"
4445
]
4546
},
4647
"bugs": {
@@ -79,13 +80,17 @@
7980
"@aws-sdk/client-sfn": "^3.632.0",
8081
"@aws-sdk/client-sns": "^3.632.0",
8182
"@opentelemetry/contrib-test-utils": "0.41.0",
83+
"@smithy/protocol-http": "^5.0.1",
84+
"@smithy/signature-v4": "^5.0.1",
8285
"@types/mocha": "7.0.2",
8386
"@types/node": "18.6.5",
87+
"@types/proxyquire": "^1.3.31",
8488
"@types/sinon": "10.0.18",
8589
"expect": "29.2.0",
8690
"mocha": "7.2.0",
8791
"nock": "13.2.1",
8892
"nyc": "15.1.0",
93+
"proxyquire": "^2.1.3",
8994
"rimraf": "5.0.5",
9095
"sinon": "15.2.0",
9196
"ts-mocha": "10.0.0",

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,14 @@ import { AttributePropagatingSpanProcessorBuilder } from './attribute-propagatin
5656
import { AwsBatchUnsampledSpanProcessor } from './aws-batch-unsampled-span-processor';
5757
import { AwsMetricAttributesSpanExporterBuilder } from './aws-metric-attributes-span-exporter-builder';
5858
import { AwsSpanMetricsProcessorBuilder } from './aws-span-metrics-processor-builder';
59+
import { OTLPAwsSpanExporter } from './otlp-aws-span-exporter';
5960
import { OTLPUdpSpanExporter } from './otlp-udp-exporter';
6061
import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler';
6162
// This file is generated via `npm run compile`
6263
import { LIB_VERSION } from './version';
6364

65+
const XRAY_OTLP_ENDPOINT_PATTERN = '^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$';
66+
6467
const APPLICATION_SIGNALS_ENABLED_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_ENABLED';
6568
const APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT';
6669
const METRIC_EXPORT_INTERVAL_CONFIG: string = 'OTEL_METRIC_EXPORT_INTERVAL';
@@ -235,6 +238,11 @@ export class AwsOpentelemetryConfigurator {
235238
}
236239

237240
spanProcessors.push(AttributePropagatingSpanProcessorBuilder.create().build());
241+
242+
if (isXrayOtlpEndpoint(process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'])) {
243+
return;
244+
}
245+
238246
const applicationSignalsMetricExporter: PushMetricExporter =
239247
ApplicationSignalsExporterProvider.Instance.createExporter();
240248
const periodicExportingMetricReader: PeriodicExportingMetricReader = new PeriodicExportingMetricReader({
@@ -422,6 +430,7 @@ export class AwsSpanProcessorProvider {
422430
private resource: Resource;
423431

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

@@ -438,12 +447,18 @@ export class AwsSpanProcessorProvider {
438447
case 'http/json':
439448
return new OTLPHttpTraceExporter();
440449
case 'http/protobuf':
450+
if (otlp_exporter_traces_endpoint && isXrayOtlpEndpoint(otlp_exporter_traces_endpoint)) {
451+
return new OTLPAwsSpanExporter(otlp_exporter_traces_endpoint);
452+
}
441453
return new OTLPProtoTraceExporter();
442454
case 'udp':
443455
diag.debug('Detected AWS Lambda environment and enabling UDPSpanExporter');
444456
return new OTLPUdpSpanExporter(getXrayDaemonEndpoint(), FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX);
445457
default:
446458
diag.warn(`Unsupported OTLP traces protocol: ${protocol}. Using http/protobuf.`);
459+
if (otlp_exporter_traces_endpoint && isXrayOtlpEndpoint(otlp_exporter_traces_endpoint)) {
460+
return new OTLPAwsSpanExporter(otlp_exporter_traces_endpoint);
461+
}
447462
return new OTLPProtoTraceExporter();
448463
}
449464
}
@@ -652,4 +667,8 @@ function getXrayDaemonEndpoint() {
652667
return process.env[AWS_XRAY_DAEMON_ADDRESS_CONFIG];
653668
}
654669

670+
function isXrayOtlpEndpoint(otlpEndpoint: string | undefined) {
671+
return otlpEndpoint && new RegExp(XRAY_OTLP_ENDPOINT_PATTERN).test(otlpEndpoint.toLowerCase());
672+
}
673+
655674
// END The OpenTelemetry Authors code
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
4+
import { diag } from '@opentelemetry/api';
5+
import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base';
6+
import { ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer';
7+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
8+
import { ExportResult } from '@opentelemetry/core';
9+
import { getNodeVersion } from './utils';
10+
11+
/**
12+
* This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported
13+
* to the XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. Utilizes the aws-sdk
14+
* library to sign and directly inject SigV4 Authentication to the exported request's headers. <a
15+
* href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html">...</a>
16+
*
17+
* This only works with version >=16 Node.js environments.
18+
*/
19+
export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter {
20+
private static readonly SERVICE_NAME: string = 'xray';
21+
private endpoint: string;
22+
private region: string;
23+
24+
// Holds the dependencies needed to sign the SigV4 headers
25+
private defaultProvider: any;
26+
private sha256: any;
27+
private signatureV4: any;
28+
private httpRequest: any;
29+
30+
// If the required dependencies are installed then we enable SigV4 signing. Otherwise skip it
31+
private hasRequiredDependencies: boolean = false;
32+
33+
constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) {
34+
super(OTLPAwsSpanExporter.changeUrlConfig(endpoint, config));
35+
this.initDependencies();
36+
this.region = endpoint.split('.')[1];
37+
this.endpoint = endpoint;
38+
}
39+
40+
/**
41+
* Overrides the upstream implementation of export. All behaviors are the same except if the
42+
* endpoint is an XRay OTLP endpoint, we will sign the request with SigV4 in headers before
43+
* sending it to the endpoint. Otherwise, we will skip signing.
44+
*/
45+
public override async export(items: ReadableSpan[], resultCallback: (result: ExportResult) => void): Promise<void> {
46+
// Only do SigV4 Signing if the required dependencies are installed. Otherwise default to the regular http/protobuf exporter.
47+
if (this.hasRequiredDependencies) {
48+
const url = new URL(this.endpoint);
49+
const serializedSpans: Uint8Array | undefined = ProtobufTraceSerializer.serializeRequest(items);
50+
51+
if (serializedSpans === undefined) {
52+
return;
53+
}
54+
55+
/*
56+
This is bad practice but there is no other way to access and inject SigV4 headers
57+
into the request headers before the traces get exported.
58+
*/
59+
const oldHeaders = (this as any)._transport?._transport?._parameters?.headers;
60+
61+
if (oldHeaders) {
62+
const request = new this.httpRequest({
63+
method: 'POST',
64+
protocol: 'https',
65+
hostname: url.hostname,
66+
path: url.pathname,
67+
body: serializedSpans,
68+
headers: {
69+
...oldHeaders,
70+
host: url.hostname,
71+
},
72+
});
73+
74+
try {
75+
const signer = new this.signatureV4({
76+
credentials: this.defaultProvider(),
77+
region: this.region,
78+
service: OTLPAwsSpanExporter.SERVICE_NAME,
79+
sha256: this.sha256,
80+
});
81+
82+
const signedRequest = await signer.sign(request);
83+
84+
(this as any)._transport._transport._parameters.headers = signedRequest.headers;
85+
} catch (exception) {
86+
diag.debug(
87+
`Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}`
88+
);
89+
}
90+
}
91+
}
92+
93+
await super.export(items, resultCallback);
94+
}
95+
96+
private initDependencies(): any {
97+
if (getNodeVersion() < 16) {
98+
diag.error('SigV4 signing requires atleast Node major version 16');
99+
return;
100+
}
101+
102+
try {
103+
const awsSdkModule = require('@aws-sdk/credential-provider-node');
104+
const awsCryptoModule = require('@aws-crypto/sha256-js');
105+
const signatureModule = require('@smithy/signature-v4');
106+
const httpModule = require('@smithy/protocol-http');
107+
108+
(this.defaultProvider = awsSdkModule.defaultProvider),
109+
(this.sha256 = awsCryptoModule.Sha256),
110+
(this.signatureV4 = signatureModule.SignatureV4),
111+
(this.httpRequest = httpModule.HttpRequest);
112+
this.hasRequiredDependencies = true;
113+
} catch (error) {
114+
diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`);
115+
}
116+
}
117+
118+
private static changeUrlConfig(endpoint: string, config?: OTLPExporterNodeConfigBase) {
119+
const newConfig =
120+
config === undefined
121+
? { url: endpoint }
122+
: {
123+
...config,
124+
url: endpoint,
125+
};
126+
127+
return newConfig;
128+
}
129+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export const getNodeVersion = () => {
5+
const nodeVersion = process.versions.node;
6+
const versionParts = nodeVersion.split('.');
7+
8+
if (versionParts.length === 0) {
9+
return -1;
10+
}
11+
12+
const majorVersion = parseInt(versionParts[0], 10);
13+
14+
if (isNaN(majorVersion)) {
15+
return -1;
16+
}
17+
18+
return majorVersion;
19+
};

0 commit comments

Comments
 (0)