Skip to content

Commit d84b956

Browse files
committed
added unit testing, made dependencies optional
1 parent f9cca5b commit d84b956

File tree

4 files changed

+251
-46
lines changed

4 files changed

+251
-46
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,13 @@
8383
"@smithy/signature-v4": "^5.0.1",
8484
"@types/mocha": "7.0.2",
8585
"@types/node": "18.6.5",
86+
"@types/proxyquire": "^1.3.31",
8687
"@types/sinon": "10.0.18",
8788
"expect": "29.2.0",
8889
"mocha": "7.2.0",
8990
"nock": "13.2.1",
9091
"nyc": "15.1.0",
92+
"proxyquire": "^2.1.3",
9193
"rimraf": "5.0.5",
9294
"sinon": "15.2.0",
9395
"ts-mocha": "10.0.0",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler';
6262
// This file is generated via `npm run compile`
6363
import { LIB_VERSION } from './version';
6464

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

6767
const APPLICATION_SIGNALS_ENABLED_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_ENABLED';
6868
const APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT';

aws-distro-opentelemetry-node-autoinstrumentation/src/otlp-aws-span-exporter.ts

Lines changed: 84 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,9 @@
33
import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
44
import { diag } from '@opentelemetry/api';
55
import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base';
6-
import { ExportResult } from '@opentelemetry/core';
7-
import { defaultProvider } from '@aws-sdk/credential-provider-node';
8-
import { Sha256 } from '@aws-crypto/sha256-js';
9-
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
10-
import { SignatureV4 } from '@smithy/signature-v4';
116
import { ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer';
12-
import { HttpRequest } from '@smithy/protocol-http';
13-
14-
const SERVICE_NAME = 'xray';
7+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
8+
import { ExportResult } from '@opentelemetry/core';
159

1610
/**
1711
* This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported
@@ -20,11 +14,22 @@ const SERVICE_NAME = 'xray';
2014
* href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html">...</a>
2115
*/
2216
export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter {
17+
private static readonly SERVICE_NAME: string = 'xray';
2318
private endpoint: string;
2419
private region: string;
2520

21+
// Holds the dependencies needed to sign the SigV4 headers
22+
private defaultProvider: any;
23+
private sha256: any;
24+
private signatureV4: any;
25+
private httpRequest: any;
26+
27+
// If there are required dependencies then we enable SigV4 signing. Otherwise skip it
28+
private hasRequiredDependencies: boolean = false;
29+
2630
constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) {
27-
super(config);
31+
super(OTLPAwsSpanExporter.changeUrlConfig(endpoint, config));
32+
this.initDependencies();
2833
this.region = endpoint.split('.')[1];
2934
this.endpoint = endpoint;
3035
}
@@ -35,48 +40,82 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter {
3540
* sending it to the endpoint. Otherwise, we will skip signing.
3641
*/
3742
public override async export(items: ReadableSpan[], resultCallback: (result: ExportResult) => void): Promise<void> {
38-
const url = new URL(this.endpoint);
39-
const serializedSpans: Uint8Array | undefined = ProtobufTraceSerializer.serializeRequest(items);
43+
// Only do SigV4 Signing if the required dependencies are installed. Otherwise default to the regular http/protobuf exporter.
44+
if (this.hasRequiredDependencies) {
45+
const url = new URL(this.endpoint);
46+
const serializedSpans: Uint8Array | undefined = ProtobufTraceSerializer.serializeRequest(items);
47+
48+
if (serializedSpans === undefined) {
49+
return;
50+
}
51+
52+
/*
53+
This is bad practice but there is no other way to access and inject SigV4 headers
54+
into the request headers before the traces get exported.
55+
*/
56+
const oldHeaders = (this as any)._transport?._transport?._parameters?.headers;
4057

41-
if (serializedSpans === undefined) {
42-
return;
58+
if (oldHeaders) {
59+
const request = new this.httpRequest({
60+
method: 'POST',
61+
protocol: 'https',
62+
hostname: url.hostname,
63+
path: url.pathname,
64+
body: serializedSpans,
65+
headers: {
66+
...oldHeaders,
67+
host: url.hostname,
68+
},
69+
});
70+
71+
try {
72+
const signer = new this.signatureV4({
73+
credentials: this.defaultProvider(),
74+
region: this.region,
75+
service: OTLPAwsSpanExporter.SERVICE_NAME,
76+
sha256: this.sha256,
77+
});
78+
79+
const signedRequest = await signer.sign(request);
80+
81+
(this as any)._transport._transport._parameters.headers = signedRequest.headers;
82+
} catch (exception) {
83+
diag.debug(
84+
`Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}`
85+
);
86+
}
87+
}
4388
}
4489

45-
/*
46-
This is bad practice but there is no other way to access and inject SigV4 headers
47-
into the request headers before the traces get exported.
48-
*/
49-
const oldHeaders = (this as any)._transport._transport._parameters.headers;
50-
51-
const request = new HttpRequest({
52-
method: 'POST',
53-
protocol: 'https',
54-
hostname: url.hostname,
55-
path: url.pathname,
56-
body: serializedSpans,
57-
headers: {
58-
...oldHeaders,
59-
host: url.hostname,
60-
},
61-
});
90+
await super.export(items, resultCallback);
91+
}
6292

93+
private initDependencies(): any {
6394
try {
64-
const signer = new SignatureV4({
65-
credentials: defaultProvider(),
66-
region: this.region,
67-
service: SERVICE_NAME,
68-
sha256: Sha256,
69-
});
70-
71-
const signedRequest = await signer.sign(request);
72-
73-
(this as any)._transport._transport._parameters.headers = signedRequest.headers;
74-
} catch (exception) {
75-
diag.debug(
76-
`Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}`
77-
);
95+
const awsSdkModule = require('@aws-sdk/credential-provider-node');
96+
const awsCryptoModule = require('@aws-crypto/sha256-js');
97+
const signatureModule = require('@smithy/signature-v4');
98+
const httpModule = require('@smithy/protocol-http');
99+
100+
(this.defaultProvider = awsSdkModule.defaultProvider),
101+
(this.sha256 = awsCryptoModule.Sha256),
102+
(this.signatureV4 = signatureModule.SignatureV4),
103+
(this.httpRequest = httpModule.HttpRequest);
104+
this.hasRequiredDependencies = true;
105+
} catch (error) {
106+
diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`);
78107
}
108+
}
79109

80-
await super.export(items, resultCallback);
110+
private static changeUrlConfig(endpoint: string, config?: OTLPExporterNodeConfigBase) {
111+
const newConfig =
112+
config === undefined
113+
? { url: endpoint }
114+
: {
115+
...config,
116+
url: endpoint,
117+
};
118+
119+
return newConfig;
81120
}
82121
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import expect from 'expect';
4+
import { OTLPAwsSpanExporter } from '../src/otlp-aws-span-exporter';
5+
import * as sinon from 'sinon';
6+
import * as proxyquire from 'proxyquire';
7+
import nock = require('nock');
8+
9+
const XRAY_OTLP_ENDPOINT = 'https://xray.us-east-1.amazonaws.com';
10+
const AUTHORIZATION_HEADER = 'Authorization';
11+
const X_AMZ_DATE_HEADER = 'X-Amz-Date';
12+
const X_AMZ_SECURITY_TOKEN_HEADER = 'X-Amz-Security-Token';
13+
14+
const EXPECTED_AUTH_HEADER = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request';
15+
const EXPECTED_AUTH_X_AMZ_DATE = 'some_date';
16+
const EXPECTED_AUTH_SECURITY_TOKEN = 'test_token';
17+
18+
describe('OTLPAwsSpanExporter', () => {
19+
let sandbox: sinon.SinonSandbox;
20+
let scope: nock.Scope;
21+
let mockModule: any;
22+
23+
beforeEach(() => {
24+
scope = nock(XRAY_OTLP_ENDPOINT)
25+
.post('/v1/traces')
26+
.reply((uri: any, requestBody: any) => {
27+
return [200, ''];
28+
});
29+
30+
sandbox = sinon.createSandbox();
31+
mockModule = proxyquire('../src/otlp-aws-span-exporter', {
32+
'@smithy/signature-v4': {
33+
SignatureV4: class MockSignatureV4 {
34+
sign(req: any) {
35+
req.headers = {
36+
...req.headers,
37+
[AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER,
38+
[X_AMZ_DATE_HEADER]: EXPECTED_AUTH_X_AMZ_DATE,
39+
[X_AMZ_SECURITY_TOKEN_HEADER]: EXPECTED_AUTH_SECURITY_TOKEN,
40+
};
41+
42+
return req;
43+
}
44+
},
45+
},
46+
'@aws-sdk/credential-provider-node': {
47+
defaultProvider: () => async () => {
48+
return {
49+
accessKeyId: 'test_access_key',
50+
secretAccessKey: 'test_secret_key',
51+
};
52+
},
53+
},
54+
});
55+
});
56+
57+
afterEach(() => {
58+
sandbox.restore();
59+
});
60+
61+
it('Should inject SigV4 Headers successfully', () => {
62+
const exporter = new mockModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + '/v1/traces');
63+
64+
scope.on('request', (req, interceptor, body) => {
65+
const headers = req.headers;
66+
expect(headers).toHaveProperty(AUTHORIZATION_HEADER.toLowerCase());
67+
expect(headers).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase());
68+
expect(headers).toHaveProperty(X_AMZ_DATE_HEADER.toLowerCase());
69+
70+
expect(headers[AUTHORIZATION_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_HEADER);
71+
expect(headers[X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_SECURITY_TOKEN);
72+
expect(headers[X_AMZ_DATE_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_X_AMZ_DATE);
73+
74+
expect(headers['content-type']).toBe('application/x-protobuf');
75+
expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/);
76+
});
77+
78+
exporter.export([], () => {});
79+
});
80+
81+
describe('Should not inject SigV4 headers if dependencies are missing', () => {
82+
const dependencies = [
83+
'@aws-sdk/credential-provider-node',
84+
'@aws-crypto/sha256-js',
85+
'@smithy/signature-v4',
86+
'@smithy/protocol-http',
87+
];
88+
89+
dependencies.forEach(dependency => {
90+
it(`should not sign headers if missing dependency: ${dependency}`, () => {
91+
const exporter = new OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + '/v1/traces');
92+
93+
scope.on('request', (req, interceptor, body) => {
94+
const headers = req.headers;
95+
expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER);
96+
expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER);
97+
expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER);
98+
99+
expect(headers['content-type']).toBe('application/x-protobuf');
100+
expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/);
101+
});
102+
103+
Object.keys(require.cache).forEach(key => {
104+
delete require.cache[key];
105+
});
106+
const requireStub = sandbox.stub(require('module'), '_load');
107+
requireStub.withArgs(dependency).throws(new Error(`Cannot find module '${dependency}'`));
108+
requireStub.callThrough();
109+
110+
exporter.export([], () => {});
111+
});
112+
});
113+
});
114+
115+
it('should not inject SigV4 headers if failure to sign headers', async () => {
116+
scope.on('request', (req, interceptor, body) => {
117+
const headers = req.headers;
118+
expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER);
119+
expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER);
120+
expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER);
121+
122+
expect(headers['content-type']).toBe('application/x-protobuf');
123+
expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/);
124+
});
125+
126+
const stubbedModule = proxyquire('../src/otlp-aws-span-exporter', {
127+
'@smithy/signature-v4': {
128+
SignatureV4: class MockSignatureV4 {
129+
sign() {
130+
throw new Error('signing error');
131+
}
132+
},
133+
},
134+
});
135+
136+
const exporter = new stubbedModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT);
137+
138+
exporter.export([], () => {});
139+
});
140+
141+
it('should not inject SigV4 headers if failure to retrieve credentials', async () => {
142+
scope.on('request', (req, interceptor, body) => {
143+
const headers = req.headers;
144+
expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER);
145+
expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER);
146+
expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER);
147+
148+
expect(headers['content-type']).toBe('application/x-protobuf');
149+
expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/);
150+
});
151+
152+
const stubbedModule = proxyquire('../src/otlp-aws-span-exporter', {
153+
'@aws-sdk/credential-provider-node': {
154+
defaultProvider: () => async () => {
155+
throw new Error('credentials error');
156+
},
157+
},
158+
});
159+
160+
const exporter = new stubbedModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT);
161+
162+
exporter.export([], () => {});
163+
});
164+
});

0 commit comments

Comments
 (0)