Skip to content

Commit aaf202e

Browse files
authored
Merge branch 'main' into upload-layer-zip
2 parents 229569a + 746c648 commit aaf202e

File tree

22 files changed

+1492
-25
lines changed

22 files changed

+1492
-25
lines changed

.github/workflows/daily-scan.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ jobs:
5656
if: always()
5757
run: |
5858
gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 259A55407DD6C00299E6607EFFDE55BE73A2D1ED
59-
VERSION=$(curl -s https://jeremylong.github.io/DependencyCheck/current.txt)
60-
curl -Ls "https://github.com/jeremylong/DependencyCheck/releases/download/v$VERSION/dependency-check-$VERSION-release.zip" --output dependency-check.zip
61-
curl -Ls "https://github.com/jeremylong/DependencyCheck/releases/download/v$VERSION/dependency-check-$VERSION-release.zip.asc" --output dependency-check.zip.asc
59+
VERSION=$(curl -s https://jeremylong.github.io/DependencyCheck/current.txt | head -n1 | cut -d' ' -f1)
60+
curl -Ls "https://github.com/dependency-check/DependencyCheck/releases/download/v$VERSION/dependency-check-$VERSION-release.zip" --output dependency-check.zip
61+
curl -Ls "https://github.com/dependency-check/DependencyCheck/releases/download/v$VERSION/dependency-check-$VERSION-release.zip.asc" --output dependency-check.zip.asc
6262
gpg --verify dependency-check.zip.asc
6363
unzip dependency-check.zip
6464
./dependency-check/bin/dependency-check.sh --nvdApiKey ${{ env.NVD_API_KEY_NVD_API_KEY }} -s "aws-distro-opentelemetry-node-autoinstrumentation/"

.github/workflows/release-build.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,11 @@ jobs:
113113
layer_artifact/layer.zip \
114114
layer_artifact/layer.zip.sha256
115115
116-
# Publish to npm
117-
- name: Publish to npm
116+
# Publish '@aws/aws-distro-opentelemetry-node-autoinstrumentation' to npm
117+
- name: Publish autoinstrumentation to npm
118+
working-directory: aws-distro-opentelemetry-node-autoinstrumentation
118119
env:
119120
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
120121
NPM_CONFIG_PROVENANCE: true
121-
run: npx lerna publish from-package --no-push --no-private --no-git-tag-version --no-verify-access --yes
122+
run: npm publish
122123

.github/workflows/release-lambda.yml

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -174,24 +174,22 @@ jobs:
174174
- name: generate tf layer
175175
working-directory: ${{ env.LAYER_NAME }}
176176
run: |
177-
echo "locals {" >> ../layer.tf
178-
echo " sdk_layer_arns = {" >> ../layer.tf
177+
echo "locals {" >> ../layer_arns.tf
178+
echo " sdk_layer_arns = {" >> ../layer_arns.tf
179179
for file in *
180180
do
181181
read arn < $file
182-
echo " \""$file"\" = \""$arn"\"" >> ../layer.tf
182+
echo " \""$file"\" = \""$arn"\"" >> ../layer_arns.tf
183183
done
184184
cd ..
185-
echo " }" >> layer.tf
186-
echo "}" >> layer.tf
187-
terraform fmt layer.tf
188-
cat layer.tf
189-
- name: upload layer tf file
190-
uses: actions/upload-artifact@v4
185+
echo " }" >> layer_arns.tf
186+
echo "}" >> layer_arns.tf
187+
terraform fmt layer_arns.tf
188+
cat layer_arns.tf
189+
- name: download layer.zip
190+
uses: actions/download-artifact@v4
191191
with:
192-
name: layer.tf
193-
path: layer.tf
194-
192+
name: layer.zip
195193
- name: Get commit hash
196194
id: commit
197195
run: |
@@ -216,7 +214,7 @@ jobs:
216214
--notes-file release_notes.md \
217215
--draft \
218216
"lambda-v${{ github.event.inputs.version }}-${{ steps.commit.outputs.sha_short }}" \
219-
lambda-layer/terraform/lambda/layer.tf
217+
layer_arns.tf layer.zip
220218
echo Removing release_notes.md ...
221219
rm -f release_notes.md
222220
- name: Upload layer.zip and SHA-256 checksum to SDK Release Notes (tagged with latest)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Release ADOT OTLP UDP Exporter
2+
on:
3+
workflow_dispatch:
4+
inputs:
5+
version:
6+
description: The version to tag the release with, e.g., 1.2.0
7+
required: true
8+
9+
jobs:
10+
build:
11+
environment: Release
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout Contrib Repo @ SHA - ${{ github.sha }}
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Node and run Unit Tests
18+
uses: ./.github/actions/set_up
19+
with:
20+
node_version: "20"
21+
package_name: "@aws/aws-otel-otlp-udp-exporter"
22+
os: ubuntu-latest
23+
run_unit_tests: true
24+
25+
# Project dependencies and compilation are already done in the previous step
26+
- name: Install Dependencies, Compile, and Build Tarball
27+
id: staging_tarball_build
28+
shell: bash
29+
run: |
30+
cd exporters/aws-otel-otlp-udp-exporter
31+
npm pack
32+
33+
- name: Download and run X-Ray Daemon
34+
run: |
35+
mkdir xray-daemon
36+
cd xray-daemon
37+
wget https://s3.us-west-2.amazonaws.com/aws-xray-assets.us-west-2/xray-daemon/aws-xray-daemon-linux-3.x.zip
38+
unzip aws-xray-daemon-linux-3.x.zip
39+
./xray -o -n us-west-2 -f ./daemon-logs.log --log-level debug &
40+
41+
- name: Setup Sample App
42+
run: |
43+
cd sample-applications/integ-test-http-server
44+
npm install
45+
npm install ../../exporters/aws-otel-otlp-udp-exporter/aws-aws-otel-otlp-udp-exporter-*.tgz
46+
47+
- name: Run Sample App in Background
48+
run: |
49+
cd sample-applications/integ-test-http-server
50+
node udp-exporter-test-server.js &
51+
# Wait for test server to initialize
52+
sleep 5
53+
54+
- name: Call Sample App Endpoint
55+
id: call-endpoint
56+
run: |
57+
echo "traceId=$(curl localhost:8080/test)" >> $GITHUB_OUTPUT
58+
59+
- name: Verify X-Ray daemon received traces
60+
run: |
61+
sleep 10
62+
echo "X-Ray daemon logs:"
63+
cat xray-daemon/daemon-logs.log
64+
# Check if the daemon received and processed some data
65+
if grep -q "sending.*batch" xray-daemon/daemon-logs.log; then
66+
echo "✅ X-Ray daemon processed trace data (AWS upload errors are expected)"
67+
exit 0
68+
elif grep -q "processor:.*segment" xray-daemon/daemon-logs.log; then
69+
echo "✅ X-Ray daemon processed segment data (AWS upload errors are expected)"
70+
exit 0
71+
else
72+
echo "❌ No evidence of traces being received by X-Ray daemon"
73+
exit 1
74+
fi
75+
76+
# TODO: Uncomment when we make the first release
77+
# # Publish OTLP UDP Exporter to npm
78+
# - name: Publish to npm
79+
# working-directory: exporters/aws-otel-otlp-udp-exporter
80+
# env:
81+
# NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
82+
# NPM_CONFIG_PROVENANCE: true
83+
# run: npx publish

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: 21 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,20 @@ 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+
diag.debug('Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter');
452+
return new OTLPAwsSpanExporter(otlp_exporter_traces_endpoint);
453+
}
441454
return new OTLPProtoTraceExporter();
442455
case 'udp':
443456
diag.debug('Detected AWS Lambda environment and enabling UDPSpanExporter');
444457
return new OTLPUdpSpanExporter(getXrayDaemonEndpoint(), FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX);
445458
default:
446459
diag.warn(`Unsupported OTLP traces protocol: ${protocol}. Using http/protobuf.`);
460+
if (otlp_exporter_traces_endpoint && isXrayOtlpEndpoint(otlp_exporter_traces_endpoint)) {
461+
diag.debug('Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter');
462+
return new OTLPAwsSpanExporter(otlp_exporter_traces_endpoint);
463+
}
447464
return new OTLPProtoTraceExporter();
448465
}
449466
}
@@ -652,4 +669,8 @@ function getXrayDaemonEndpoint() {
652669
return process.env[AWS_XRAY_DAEMON_ADDRESS_CONFIG];
653670
}
654671

672+
function isXrayOtlpEndpoint(otlpEndpoint: string | undefined) {
673+
return otlpEndpoint && new RegExp(XRAY_OTLP_ENDPOINT_PATTERN).test(otlpEndpoint.toLowerCase());
674+
}
675+
655676
// END The OpenTelemetry Authors code
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
...this.removeSigV4Headers(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+
// Removes Sigv4 headers from old headers to avoid accidentally copying them to the new headers
97+
private removeSigV4Headers(headers: Record<string, string>) {
98+
const newHeaders: Record<string, string> = {};
99+
const sigV4Headers = ['x-amz-date', 'authorization', 'x-amz-content-sha256', 'x-amz-security-token'];
100+
101+
for (const key in headers) {
102+
if (!sigV4Headers.includes(key.toLowerCase())) {
103+
newHeaders[key] = headers[key];
104+
}
105+
}
106+
return newHeaders;
107+
}
108+
109+
private initDependencies(): any {
110+
if (getNodeVersion() < 16) {
111+
diag.error('SigV4 signing requires atleast Node major version 16');
112+
return;
113+
}
114+
115+
try {
116+
const awsSdkModule = require('@aws-sdk/credential-provider-node');
117+
const awsCryptoModule = require('@aws-crypto/sha256-js');
118+
const signatureModule = require('@smithy/signature-v4');
119+
const httpModule = require('@smithy/protocol-http');
120+
121+
(this.defaultProvider = awsSdkModule.defaultProvider),
122+
(this.sha256 = awsCryptoModule.Sha256),
123+
(this.signatureV4 = signatureModule.SignatureV4),
124+
(this.httpRequest = httpModule.HttpRequest);
125+
this.hasRequiredDependencies = true;
126+
} catch (error) {
127+
diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`);
128+
}
129+
}
130+
131+
private static changeUrlConfig(endpoint: string, config?: OTLPExporterNodeConfigBase) {
132+
const newConfig =
133+
config === undefined
134+
? { url: endpoint }
135+
: {
136+
...config,
137+
url: endpoint,
138+
};
139+
140+
return newConfig;
141+
}
142+
}

0 commit comments

Comments
 (0)