From 6d12166ab5e8377665b38d3ec70ab0823707ce6a Mon Sep 17 00:00:00 2001 From: liustve Date: Mon, 12 May 2025 20:16:27 +0000 Subject: [PATCH 01/11] initial commit --- .../package.json | 1 + .../src/aws-opentelemetry-configurator.ts | 2 +- .../otlp/aws/common/aws-authenticator.ts | 80 ++++ .../otlp/aws/logs/otlp-aws-log-exporter.ts | 0 .../aws/traces}/otlp-aws-span-exporter.ts | 88 +++-- .../test/otlp-aws-span-exporter.test.ts | 370 +++++++++--------- package-lock.json | 1 + 7 files changed, 324 insertions(+), 218 deletions(-) create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts rename aws-distro-opentelemetry-node-autoinstrumentation/src/{ => exporter/otlp/aws/traces}/otlp-aws-span-exporter.ts (68%) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/package.json b/aws-distro-opentelemetry-node-autoinstrumentation/package.json index 28d63edf..bd29706a 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/package.json +++ b/aws-distro-opentelemetry-node-autoinstrumentation/package.json @@ -104,6 +104,7 @@ "@opentelemetry/exporter-metrics-otlp-grpc": "0.57.1", "@opentelemetry/exporter-metrics-otlp-http": "0.57.1", "@opentelemetry/exporter-trace-otlp-proto": "0.57.1", + "@opentelemetry/exporter-logs-otlp-proto": "0.57.1", "@opentelemetry/exporter-zipkin": "1.30.1", "@opentelemetry/id-generator-aws-xray": "1.2.3", "@opentelemetry/instrumentation": "0.57.1", diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts index 4d4938fe..ebfda8b7 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts @@ -56,7 +56,7 @@ import { AttributePropagatingSpanProcessorBuilder } from './attribute-propagatin import { AwsBatchUnsampledSpanProcessor } from './aws-batch-unsampled-span-processor'; import { AwsMetricAttributesSpanExporterBuilder } from './aws-metric-attributes-span-exporter-builder'; import { AwsSpanMetricsProcessorBuilder } from './aws-span-metrics-processor-builder'; -import { OTLPAwsSpanExporter } from './otlp-aws-span-exporter'; +import { OTLPAwsSpanExporter } from './exporter/otlp/aws/traces/otlp-aws-span-exporter'; import { OTLPUdpSpanExporter } from './otlp-udp-exporter'; import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler'; // This file is generated via `npm run compile` diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts new file mode 100644 index 00000000..c4e96eed --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts @@ -0,0 +1,80 @@ +// import { diag } from '@opentelemetry/api'; +// import { getNodeVersion } from '../../../../utils'; + +// let SignatureV4: any; +// let HttpRequest: any; +// let defaultProvider: any; +// let Sha256: any; + +// const nodeVersionSupported = getNodeVersion() >= 16; + +// if (nodeVersionSupported) { +// try { +// const { defaultProvider: awsDefaultProvider } = require('@aws-sdk/credential-provider-node'); +// const { Sha256: awsSha256 } = require('@aws-crypto/sha256-js'); +// const { SignatureV4: awsSignatureV4 } = require('@smithy/signature-v4'); +// const { HttpRequest: awsHttpRequest } = require('@smithy/protocol-http'); + +// // Assign to module-level variables +// defaultProvider = awsDefaultProvider; +// Sha256 = awsSha256; +// SignatureV4 = awsSignatureV4; +// HttpRequest = awsHttpRequest; +// } catch (error) { +// diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`); +// } +// } + +// export class AwsAuthenticator { + +// private static readonly SERVICE_NAME: string = 'xray'; +// private endpoint: string; +// private region: string; +// private service: string; + +// // Holds the dependencies needed to sign the SigV4 headers +// private defaultProvider: any; +// private sha256: any; +// private signatureV4: any; +// private httpRequest: any; + +// constructor(endpoint: string, region: string, service: string) { +// this.endpoint = endpoint; +// this.region = region; +// this.service = service; + +// } + +// // if (oldHeaders) { +// // const request = new this.httpRequest({ +// // method: 'POST', +// // protocol: 'https', +// // hostname: url.hostname, +// // path: url.pathname, +// // body: serializedSpans, +// // headers: { +// // ...this.removeSigV4Headers(oldHeaders), +// // host: url.hostname, +// // }, +// // }); + +// // try { +// // const signer = new this.signatureV4({ +// // credentials: this.defaultProvider(), +// // region: this.region, +// // service: OTLPAwsSpanExporter.SERVICE_NAME, +// // sha256: this.sha256, +// // }); + +// // const signedRequest = await signer.sign(request); + +// // // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 +// // const newHeaders: () => Record = () => signedRequest.headers; +// // this['_delegate']._transport._transport._parameters.headers = newHeaders; +// // } catch (exception) { +// // diag.debug( +// // `Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}` +// // ); +// // } +// // } +// } \ No newline at end of file diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts new file mode 100644 index 00000000..e69de29b diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/otlp-aws-span-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts similarity index 68% rename from aws-distro-opentelemetry-node-autoinstrumentation/src/otlp-aws-span-exporter.ts rename to aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts index 406d0bea..17a5b20d 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/otlp-aws-span-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -6,7 +6,31 @@ import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; import { ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { ExportResult } from '@opentelemetry/core'; -import { getNodeVersion } from './utils'; +import { getNodeVersion } from '../../../../utils'; +let SignatureV4: any; +let HttpRequest: any; +let defaultProvider: any; +let Sha256: any; + +const nodeVersionSupported = getNodeVersion() >= 16; + +if (nodeVersionSupported) { + try { + const { defaultProvider: awsDefaultProvider } = require('@aws-sdk/credential-provider-node'); + const { Sha256: awsSha256 } = require('@aws-crypto/sha256-js'); + const { SignatureV4: awsSignatureV4 } = require('@smithy/signature-v4'); + const { HttpRequest: awsHttpRequest } = require('@smithy/protocol-http'); + + // Assign to module-level variables + defaultProvider = awsDefaultProvider; + Sha256 = awsSha256; + SignatureV4 = awsSignatureV4; + HttpRequest = awsHttpRequest; + } catch (error) { + diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`); + } +} + /** * This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported @@ -21,18 +45,18 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { private endpoint: string; private region: string; - // Holds the dependencies needed to sign the SigV4 headers - private defaultProvider: any; - private sha256: any; - private signatureV4: any; - private httpRequest: any; + // // Holds the dependencies needed to sign the SigV4 headers + // private defaultProvider: any; + // private sha256: any; + // private signatureV4: any; + // private httpRequest: any; // If the required dependencies are installed then we enable SigV4 signing. Otherwise skip it private hasRequiredDependencies: boolean = false; constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { super(OTLPAwsSpanExporter.changeUrlConfig(endpoint, config)); - this.initDependencies(); + // this.initDependencies(); this.region = endpoint.split('.')[1]; this.endpoint = endpoint; } @@ -59,7 +83,7 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { const oldHeaders = this['_delegate']._transport?._transport?._parameters?.headers(); if (oldHeaders) { - const request = new this.httpRequest({ + const request = new HttpRequest.httpRequest({ method: 'POST', protocol: 'https', hostname: url.hostname, @@ -72,11 +96,11 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { }); try { - const signer = new this.signatureV4({ - credentials: this.defaultProvider(), + const signer = new SignatureV4({ + credentials: defaultProvider(), region: this.region, service: OTLPAwsSpanExporter.SERVICE_NAME, - sha256: this.sha256, + sha256: Sha256, }); const signedRequest = await signer.sign(request); @@ -108,27 +132,27 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { return newHeaders; } - private initDependencies(): any { - if (getNodeVersion() < 16) { - diag.error('SigV4 signing requires atleast Node major version 16'); - return; - } - - try { - const awsSdkModule = require('@aws-sdk/credential-provider-node'); - const awsCryptoModule = require('@aws-crypto/sha256-js'); - const signatureModule = require('@smithy/signature-v4'); - const httpModule = require('@smithy/protocol-http'); - - (this.defaultProvider = awsSdkModule.defaultProvider), - (this.sha256 = awsCryptoModule.Sha256), - (this.signatureV4 = signatureModule.SignatureV4), - (this.httpRequest = httpModule.HttpRequest); - this.hasRequiredDependencies = true; - } catch (error) { - diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`); - } - } + // private initDependencies(): any { + // if (getNodeVersion() < 16) { + // diag.error('SigV4 signing requires atleast Node major version 16'); + // return; + // } + + // try { + // const awsSdkModule = require('@aws-sdk/credential-provider-node'); + // const awsCryptoModule = require('@aws-crypto/sha256-js'); + // const signatureModule = require('@smithy/signature-v4'); + // const httpModule = require('@smithy/protocol-http'); + + // (this.defaultProvider = awsSdkModule.defaultProvider), + // (this.sha256 = awsCryptoModule.Sha256), + // (this.signatureV4 = signatureModule.SignatureV4), + // (this.httpRequest = httpModule.HttpRequest); + // this.hasRequiredDependencies = true; + // } catch (error) { + // diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`); + // } + // } private static changeUrlConfig(endpoint: string, config?: OTLPExporterNodeConfigBase): OTLPExporterNodeConfigBase { const newConfig = diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/otlp-aws-span-exporter.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/otlp-aws-span-exporter.test.ts index 4002293c..e71fdfd8 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/otlp-aws-span-exporter.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/otlp-aws-span-exporter.test.ts @@ -1,185 +1,185 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import expect from 'expect'; -import { OTLPAwsSpanExporter } from '../src/otlp-aws-span-exporter'; -import * as sinon from 'sinon'; -import * as proxyquire from 'proxyquire'; -import * as nock from 'nock'; -import { getNodeVersion } from '../src/utils'; - -const XRAY_OTLP_ENDPOINT = 'https://xray.us-east-1.amazonaws.com'; -const XRAY_OTLP_ENDPOINT_PATH = '/v1/traces'; -const AUTHORIZATION_HEADER = 'Authorization'; -const X_AMZ_DATE_HEADER = 'X-Amz-Date'; -const X_AMZ_SECURITY_TOKEN_HEADER = 'X-Amz-Security-Token'; - -const EXPECTED_AUTH_HEADER = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request'; -const EXPECTED_AUTH_X_AMZ_DATE = 'some_date'; -const EXPECTED_AUTH_SECURITY_TOKEN = 'test_token'; - -const nodeVersion = getNodeVersion(); - -// SigV4 exporter requires packages that require Node environments >= 16 -/* istanbul ignore next */ -if (nodeVersion >= 16) { - describe('OTLPAwsSpanExporter', () => { - let sandbox: sinon.SinonSandbox; - let scope: nock.Scope; - let mockModule: any; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - - scope = nock(XRAY_OTLP_ENDPOINT) - .post(XRAY_OTLP_ENDPOINT_PATH) - .reply((uri: any, requestBody: any) => { - return [200, '']; - }); - - mockModule = proxyquire('../src/otlp-aws-span-exporter', { - '@smithy/signature-v4': { - SignatureV4: class MockSignatureV4 { - sign(req: any) { - req.headers = { - ...req.headers, - [AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER, - [X_AMZ_DATE_HEADER]: EXPECTED_AUTH_X_AMZ_DATE, - [X_AMZ_SECURITY_TOKEN_HEADER]: EXPECTED_AUTH_SECURITY_TOKEN, - }; - - return req; - } - }, - }, - '@aws-sdk/credential-provider-node': { - defaultProvider: () => async () => { - return { - accessKeyId: 'test_access_key', - secretAccessKey: 'test_secret_key', - }; - }, - }, - }); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('Should inject SigV4 Headers successfully', done => { - const exporter = new mockModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); - - exporter - .export([], () => {}) - .then(() => { - scope.on('request', (req, interceptor, body) => { - const headers = req.headers; - expect(headers).toHaveProperty(AUTHORIZATION_HEADER.toLowerCase()); - expect(headers).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()); - expect(headers).toHaveProperty(X_AMZ_DATE_HEADER.toLowerCase()); - - expect(headers[AUTHORIZATION_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_HEADER); - expect(headers[X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_SECURITY_TOKEN); - expect(headers[X_AMZ_DATE_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_X_AMZ_DATE); - - expect(headers['content-type']).toBe('application/x-protobuf'); - expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); - done(); - }); - }); - }); - - describe('Should not inject SigV4 headers if dependencies are missing', () => { - const dependencies = [ - '@aws-sdk/credential-provider-node', - '@aws-crypto/sha256-js', - '@smithy/signature-v4', - '@smithy/protocol-http', - ]; - - dependencies.forEach(dependency => { - it(`should not sign headers if missing dependency: ${dependency}`, done => { - const exporter = new OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); - - Object.keys(require.cache).forEach(key => { - delete require.cache[key]; - }); - const requireStub = sandbox.stub(require('module'), '_load'); - requireStub.withArgs(dependency).throws(new Error(`Cannot find module '${dependency}'`)); - requireStub.callThrough(); - - exporter - .export([], () => {}) - .then(() => { - scope.on('request', (req, interceptor, body) => { - const headers = req.headers; - expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); - expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); - expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); - - expect(headers['content-type']).toBe('application/x-protobuf'); - expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); - done(); - }); - }); - }); - }); - }); - - it('should not inject SigV4 headers if failure to sign headers', done => { - const stubbedModule = proxyquire('../src/otlp-aws-span-exporter', { - '@smithy/signature-v4': { - SignatureV4: class MockSignatureV4 { - sign() { - throw new Error('signing error'); - } - }, - }, - }); - - const exporter = new stubbedModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); - - exporter - .export([], () => {}) - .then(() => { - scope.on('request', (req, interceptor, body) => { - const headers = req.headers; - expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); - expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); - expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); - - expect(headers['content-type']).toBe('application/x-protobuf'); - expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); - done(); - }); - }); - }); - - it('should not inject SigV4 headers if failure to retrieve credentials', done => { - const stubbedModule = proxyquire('../src/otlp-aws-span-exporter', { - '@aws-sdk/credential-provider-node': { - defaultProvider: () => async () => { - throw new Error('credentials error'); - }, - }, - }); - - const exporter = new stubbedModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); - - exporter - .export([], () => {}) - .then(() => { - scope.on('request', (req, interceptor, body) => { - const headers = req.headers; - expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); - expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); - expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); - - expect(headers['content-type']).toBe('application/x-protobuf'); - expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); - done(); - }); - }); - }); - }); -} +// // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // SPDX-License-Identifier: Apache-2.0 +// import expect from 'expect'; +// import { OTLPAwsSpanExporter } from '../src/exporter/otlp/aws/traces/otlp-aws-span-exporter'; +// import * as sinon from 'sinon'; +// import * as proxyquire from 'proxyquire'; +// import * as nock from 'nock'; +// import { getNodeVersion } from '../src/utils'; + +// const XRAY_OTLP_ENDPOINT = 'https://xray.us-east-1.amazonaws.com'; +// const XRAY_OTLP_ENDPOINT_PATH = '/v1/traces'; +// const AUTHORIZATION_HEADER = 'Authorization'; +// const X_AMZ_DATE_HEADER = 'X-Amz-Date'; +// const X_AMZ_SECURITY_TOKEN_HEADER = 'X-Amz-Security-Token'; + +// const EXPECTED_AUTH_HEADER = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request'; +// const EXPECTED_AUTH_X_AMZ_DATE = 'some_date'; +// const EXPECTED_AUTH_SECURITY_TOKEN = 'test_token'; + +// const nodeVersion = getNodeVersion(); + +// // SigV4 exporter requires packages that require Node environments >= 16 +// /* istanbul ignore next */ +// if (nodeVersion >= 16) { +// describe('OTLPAwsSpanExporter', () => { +// let sandbox: sinon.SinonSandbox; +// let scope: nock.Scope; +// let mockModule: any; + +// beforeEach(() => { +// sandbox = sinon.createSandbox(); + +// scope = nock(XRAY_OTLP_ENDPOINT) +// .post(XRAY_OTLP_ENDPOINT_PATH) +// .reply((uri: any, requestBody: any) => { +// return [200, '']; +// }); + +// mockModule = proxyquire('../src/otlp-aws-span-exporter', { +// '@smithy/signature-v4': { +// SignatureV4: class MockSignatureV4 { +// sign(req: any) { +// req.headers = { +// ...req.headers, +// [AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER, +// [X_AMZ_DATE_HEADER]: EXPECTED_AUTH_X_AMZ_DATE, +// [X_AMZ_SECURITY_TOKEN_HEADER]: EXPECTED_AUTH_SECURITY_TOKEN, +// }; + +// return req; +// } +// }, +// }, +// '@aws-sdk/credential-provider-node': { +// defaultProvider: () => async () => { +// return { +// accessKeyId: 'test_access_key', +// secretAccessKey: 'test_secret_key', +// }; +// }, +// }, +// }); +// }); + +// afterEach(() => { +// sandbox.restore(); +// }); + +// it('Should inject SigV4 Headers successfully', done => { +// const exporter = new mockModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); + +// exporter +// .export([], () => {}) +// .then(() => { +// scope.on('request', (req, interceptor, body) => { +// const headers = req.headers; +// expect(headers).toHaveProperty(AUTHORIZATION_HEADER.toLowerCase()); +// expect(headers).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()); +// expect(headers).toHaveProperty(X_AMZ_DATE_HEADER.toLowerCase()); + +// expect(headers[AUTHORIZATION_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_HEADER); +// expect(headers[X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_SECURITY_TOKEN); +// expect(headers[X_AMZ_DATE_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_X_AMZ_DATE); + +// expect(headers['content-type']).toBe('application/x-protobuf'); +// expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); +// done(); +// }); +// }); +// }); + +// describe('Should not inject SigV4 headers if dependencies are missing', () => { +// const dependencies = [ +// '@aws-sdk/credential-provider-node', +// '@aws-crypto/sha256-js', +// '@smithy/signature-v4', +// '@smithy/protocol-http', +// ]; + +// dependencies.forEach(dependency => { +// it(`should not sign headers if missing dependency: ${dependency}`, done => { +// const exporter = new OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); + +// Object.keys(require.cache).forEach(key => { +// delete require.cache[key]; +// }); +// const requireStub = sandbox.stub(require('module'), '_load'); +// requireStub.withArgs(dependency).throws(new Error(`Cannot find module '${dependency}'`)); +// requireStub.callThrough(); + +// exporter +// .export([], () => {}) +// .then(() => { +// scope.on('request', (req, interceptor, body) => { +// const headers = req.headers; +// expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); +// expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); +// expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); + +// expect(headers['content-type']).toBe('application/x-protobuf'); +// expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); +// done(); +// }); +// }); +// }); +// }); +// }); + +// it('should not inject SigV4 headers if failure to sign headers', done => { +// const stubbedModule = proxyquire('../src/otlp-aws-span-exporter', { +// '@smithy/signature-v4': { +// SignatureV4: class MockSignatureV4 { +// sign() { +// throw new Error('signing error'); +// } +// }, +// }, +// }); + +// const exporter = new stubbedModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); + +// exporter +// .export([], () => {}) +// .then(() => { +// scope.on('request', (req, interceptor, body) => { +// const headers = req.headers; +// expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); +// expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); +// expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); + +// expect(headers['content-type']).toBe('application/x-protobuf'); +// expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); +// done(); +// }); +// }); +// }); + +// it('should not inject SigV4 headers if failure to retrieve credentials', done => { +// const stubbedModule = proxyquire('../src/otlp-aws-span-exporter', { +// '@aws-sdk/credential-provider-node': { +// defaultProvider: () => async () => { +// throw new Error('credentials error'); +// }, +// }, +// }); + +// const exporter = new stubbedModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); + +// exporter +// .export([], () => {}) +// .then(() => { +// scope.on('request', (req, interceptor, body) => { +// const headers = req.headers; +// expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); +// expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); +// expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); + +// expect(headers['content-type']).toBe('application/x-protobuf'); +// expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); +// done(); +// }); +// }); +// }); +// }); +// } diff --git a/package-lock.json b/package-lock.json index 237f2090..03c19fb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@opentelemetry/auto-configuration-propagators": "0.3.2", "@opentelemetry/auto-instrumentations-node": "0.56.0", "@opentelemetry/core": "1.30.1", + "@opentelemetry/exporter-logs-otlp-proto": "0.57.1", "@opentelemetry/exporter-metrics-otlp-grpc": "0.57.1", "@opentelemetry/exporter-metrics-otlp-http": "0.57.1", "@opentelemetry/exporter-trace-otlp-proto": "0.57.1", From 6b3ceb146b97591cd9b9fe94045fb811f361294b Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 27 May 2025 05:22:00 +0000 Subject: [PATCH 02/11] sigv4 authentication support for otlp logs exporter --- .../package.json | 5 +- .../src/aws-opentelemetry-configurator.ts | 215 ++++++++++++- .../otlp/aws/common/aws-authenticator.ts | 156 ++++----- .../src/exporter/otlp/aws/common/utils.ts | 15 + .../otlp/aws/logs/otlp-aws-log-exporter.ts | 53 +++ .../otlp/aws/traces/otlp-aws-span-exporter.ts | 142 +------- .../src/register.ts | 4 + .../aws-opentelemetry-configurator.test.ts | 303 ++++++++++++++++-- .../otlp/aws/common/aws-authenticator.test.ts | 171 ++++++++++ .../otlp/aws/common/test-utils.test.ts | 20 ++ .../aws/logs/otlp-aws-log-exporter.test.ts | 78 +++++ .../aws/traces/otlp-aws-span-exporter.test.ts | 77 +++++ .../test/otlp-aws-span-exporter.test.ts | 185 ----------- package-lock.json | 3 + 14 files changed, 1007 insertions(+), 420 deletions(-) create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/utils.ts create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/test-utils.test.ts create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts delete mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/test/otlp-aws-span-exporter.test.ts diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/package.json b/aws-distro-opentelemetry-node-autoinstrumentation/package.json index bd29706a..42c246f7 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/package.json +++ b/aws-distro-opentelemetry-node-autoinstrumentation/package.json @@ -41,7 +41,7 @@ ], "exclude": [ "src/third-party/**/*.ts", - "src/otlp-aws-span-exporter.ts" + "src/exporter/otlp/aws/common/aws-authenticator.ts" ] }, "bugs": { @@ -104,6 +104,8 @@ "@opentelemetry/exporter-metrics-otlp-grpc": "0.57.1", "@opentelemetry/exporter-metrics-otlp-http": "0.57.1", "@opentelemetry/exporter-trace-otlp-proto": "0.57.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.57.1", + "@opentelemetry/exporter-logs-otlp-http": "0.57.1", "@opentelemetry/exporter-logs-otlp-proto": "0.57.1", "@opentelemetry/exporter-zipkin": "1.30.1", "@opentelemetry/id-generator-aws-xray": "1.2.3", @@ -116,6 +118,7 @@ "@opentelemetry/sdk-metrics": "1.30.1", "@opentelemetry/sdk-node": "0.57.1", "@opentelemetry/sdk-trace-base": "1.30.1", + "@opentelemetry/sdk-logs": "0.57.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "files": [ diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts index ebfda8b7..6a3f5653 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts @@ -14,6 +14,9 @@ import { import { OTLPTraceExporter as OTLPGrpcTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; import { OTLPTraceExporter as OTLPHttpTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; +import { OTLPLogExporter as OTLPGrpcLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc'; +import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; +import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; import { AWSXRayIdGenerator } from '@opentelemetry/id-generator-aws-xray'; import { Instrumentation } from '@opentelemetry/instrumentation'; @@ -50,6 +53,14 @@ import { SpanProcessor, TraceIdRatioBasedSampler, } from '@opentelemetry/sdk-trace-base'; + +import { + BatchLogRecordProcessor, + ConsoleLogRecordExporter, + LogRecordExporter, + LogRecordProcessor, + SimpleLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; import { SEMRESATTRS_TELEMETRY_AUTO_VERSION } from '@opentelemetry/semantic-conventions'; import { AlwaysRecordSampler } from './always-record-sampler'; import { AttributePropagatingSpanProcessorBuilder } from './attribute-propagating-span-processor-builder'; @@ -61,14 +72,20 @@ import { OTLPUdpSpanExporter } from './otlp-udp-exporter'; import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler'; // This file is generated via `npm run compile` import { LIB_VERSION } from './version'; +import { OTLPAwsLogExporter } from './exporter/otlp/aws/logs/otlp-aws-log-exporter'; -const XRAY_OTLP_ENDPOINT_PATTERN = '^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$'; +const AWS_TRACES_OTLP_ENDPOINT_PATTERN = '^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$'; +const AWS_LOGS_OTLP_ENDPOINT_PATTERN = '^https://logs\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/logs$'; + +const AWS_OTLP_LOGS_GROUP_HEADER = 'x-aws-log-group'; +const AWS_OTLP_LOGS_STREAM_HEADER = 'x-aws-log-stream'; 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'; const DEFAULT_METRIC_EXPORT_INTERVAL_MILLIS: number = 60000; export const AWS_LAMBDA_FUNCTION_NAME_CONFIG: string = 'AWS_LAMBDA_FUNCTION_NAME'; +export const AGENT_OBSERVABILITY_ENABLED = 'AGENT_OBSERVABILITY_ENABLED'; const AWS_XRAY_DAEMON_ADDRESS_CONFIG: string = 'AWS_XRAY_DAEMON_ADDRESS'; const FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = 'T1S'; const FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = 'T1U'; @@ -95,6 +112,7 @@ export class AwsOpentelemetryConfigurator { private idGenerator: IdGenerator; private sampler: Sampler; private spanProcessors: SpanProcessor[]; + private logRecordProcessors: LogRecordProcessor[]; private propagator: TextMapPropagator; /** @@ -178,6 +196,7 @@ export class AwsOpentelemetryConfigurator { // default SpanProcessors with Span Exporters wrapped inside AwsMetricAttributesSpanExporter const awsSpanProcessorProvider: AwsSpanProcessorProvider = new AwsSpanProcessorProvider(this.resource); this.spanProcessors = awsSpanProcessorProvider.getSpanProcessors(); + this.logRecordProcessors = AwsLoggerProcessorProvider.getlogRecordProcessors(); AwsOpentelemetryConfigurator.customizeSpanProcessors(this.spanProcessors, this.resource); } @@ -206,6 +225,7 @@ export class AwsOpentelemetryConfigurator { // span processors are specified // https://github.com/open-telemetry/opentelemetry-js/issues/3449 spanProcessors: this.spanProcessors, + logRecordProcessors: this.logRecordProcessors, autoDetectResources: false, textMapPropagator: this.propagator, }; @@ -384,6 +404,142 @@ export class ApplicationSignalsExporterProvider { }; } +// The OpenTelemetry Authors code +// AWS Distro for OpenTelemetry JavaScript needs to copy and adapt code from the upstream OpenTelemetry project because the original implementation doesn't expose certain critical components +// needed for AWS-specific customizations. Specifically, the private configureLoggerProviderFromEnv() from the OpenTelemetry SDK, is a key function that allows us to configure logs exporters based on environment variables, +// By implementing our own version of these methods, we can extend the functionality to detect AWS service endpoints and automatically switch to AWS-specific, OTLPAwsLogExporter. +// Long term, we want to contribute these changes to upstream. +// +// https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-sdk-node/src/sdk.ts#L443 +// +// The upstream OpenTelemetry SDK has changed its API by deprecating `getEnv()` and +// `getEnvWithoutDefaults()` in favor of specific methods like `getStringListFromEnv` +// and `getStringFromEnv`. Since these newer methods aren't available in our current +// supported version, we've also needed to copy them down here. +// +// https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-core/src/platform/node/environment.ts#L52 +// https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-core/src/platform/node/environment.ts#L100 +// +// TODO: Remove getStringListFromEnv and getStringFromEnv implementations +// once we upgrade to @opentelemetry/core 2.0.0 or higher, which provides these methods natively. +// +export class AwsLoggerProcessorProvider { + public static getlogRecordProcessors(): LogRecordProcessor[] { + const exporters = AwsLoggerProcessorProvider.configureLogExportersFromEnv(); + + return exporters.map(exporter => { + if (exporter instanceof ConsoleLogRecordExporter) { + return new SimpleLogRecordProcessor(exporter); + } else { + return new BatchLogRecordProcessor(exporter); + } + }); + } + + static configureLogExportersFromEnv(): LogRecordExporter[] { + const otlpExporterLogsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; + const enabledExporters = AwsLoggerProcessorProvider.getStringListFromEnv('OTEL_LOGS_EXPORTER') ?? []; + + if (enabledExporters.length === 0) { + diag.debug('OTEL_LOGS_EXPORTER is empty. Using default otlp exporter.'); + enabledExporters.push('otlp'); + } + + if (enabledExporters.includes('none')) { + diag.info('OTEL_LOGS_EXPORTER contains "none". Logger provider will not be initialized.'); + return []; + } + + const exporters: LogRecordExporter[] = []; + + enabledExporters.forEach(exporter => { + if (exporter === 'otlp') { + const protocol = ( + AwsLoggerProcessorProvider.getStringFromEnv('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL') ?? + AwsLoggerProcessorProvider.getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') + )?.trim(); + + switch (protocol) { + case 'grpc': + exporters.push(new OTLPGrpcLogExporter()); + break; + case 'http/json': + exporters.push(new OTLPHttpLogExporter()); + break; + case 'http/protobuf': + if ( + otlpExporterLogsEndpoint && + isAwsOtlpEndpoint(otlpExporterLogsEndpoint, 'logs') && + validateLogsHeaders() + ) { + diag.debug('Detected CloudWatch Logs OTLP endpoint. Switching exporter to OTLPAwsLogExporter'); + exporters.push(new OTLPAwsLogExporter(otlpExporterLogsEndpoint)); + } else { + exporters.push(new OTLPProtoLogExporter()); + } + break; + case undefined: + case '': + exporters.push(new OTLPProtoLogExporter()); + break; + default: + diag.warn(`Unsupported OTLP logs protocol: "${protocol}". Using http/protobuf.`); + if ( + otlpExporterLogsEndpoint && + isAwsOtlpEndpoint(otlpExporterLogsEndpoint, 'logs') && + validateLogsHeaders() + ) { + diag.debug('Detected CloudWatch Logs OTLP endpoint. Switching exporter to OTLPAwsLogExporter'); + exporters.push(new OTLPAwsLogExporter(otlpExporterLogsEndpoint)); + } else { + exporters.push(new OTLPProtoLogExporter()); + } + } + } else if (exporter === 'console') { + exporters.push(new ConsoleLogRecordExporter()); + } else { + diag.warn(`Unsupported OTEL_LOGS_EXPORTER value: "${exporter}". Supported values are: otlp, console, none.`); + } + }); + + return exporters; + } + + /** + * Retrieves a list of strings from an environment variable. + * - Uses ',' as the delimiter. + * - Trims leading and trailing whitespace from each entry. + * - Excludes empty entries. + * - Returns `undefined` if the environment variable is empty or contains only whitespace. + * - Returns an empty array if all entries are empty or whitespace. + * + * @param {string} key - The name of the environment variable to retrieve. + * @returns {string[] | undefined} - The list of strings or `undefined`. + */ + private static getStringListFromEnv(key: string): string[] | undefined { + return AwsLoggerProcessorProvider.getStringFromEnv(key) + ?.split(',') + .map(v => v.trim()) + .filter(s => s !== ''); + } + + /** + * Retrieves a string from an environment variable. + * - Returns `undefined` if the environment variable is empty, unset, or contains only whitespace. + * + * @param {string} key - The name of the environment variable to retrieve. + * @returns {string | undefined} - The string value or `undefined`. + */ + private static getStringFromEnv(key: string): string | undefined { + const raw = process.env[key]; + if (raw == null || raw.trim() === '') { + return undefined; + } + return raw; + } +} +// END The OpenTelemetry Authors code + // The OpenTelemetry Authors code // // ADOT JS needs the logic to (1) get the SpanExporters from Env and then (2) wrap the SpanExporters with AwsMetricAttributesSpanExporter @@ -427,7 +583,7 @@ export class AwsSpanProcessorProvider { private resource: Resource; static configureOtlp(): SpanExporter { - const otlp_exporter_traces_endpoint = process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT']; + const otlpExporterTracesEndpoint = process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT']; // eslint-disable-next-line @typescript-eslint/typedef let protocol = this.getOtlpProtocol(); @@ -444,9 +600,9 @@ export class AwsSpanProcessorProvider { case 'http/json': return new OTLPHttpTraceExporter(); case 'http/protobuf': - if (otlp_exporter_traces_endpoint && isXrayOtlpEndpoint(otlp_exporter_traces_endpoint)) { + if (otlpExporterTracesEndpoint && isAwsOtlpEndpoint(otlpExporterTracesEndpoint, 'xray')) { diag.debug('Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter'); - return new OTLPAwsSpanExporter(otlp_exporter_traces_endpoint); + return new OTLPAwsSpanExporter(otlpExporterTracesEndpoint); } return new OTLPProtoTraceExporter(); case 'udp': @@ -454,9 +610,9 @@ export class AwsSpanProcessorProvider { 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)) { + if (otlpExporterTracesEndpoint && isAwsOtlpEndpoint(otlpExporterTracesEndpoint, 'xray')) { diag.debug('Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter'); - return new OTLPAwsSpanExporter(otlp_exporter_traces_endpoint); + return new OTLPAwsSpanExporter(otlpExporterTracesEndpoint); } return new OTLPProtoTraceExporter(); } @@ -666,8 +822,51 @@ function getXrayDaemonEndpoint() { return process.env[AWS_XRAY_DAEMON_ADDRESS_CONFIG]; } -function isXrayOtlpEndpoint(otlpEndpoint: string | undefined) { - return otlpEndpoint && new RegExp(XRAY_OTLP_ENDPOINT_PATTERN).test(otlpEndpoint.toLowerCase()); +/** + * Determines if the given endpoint is either the AWS OTLP Traces or Logs endpoint. + */ + +function isAwsOtlpEndpoint(otlpEndpoint: string, service: string): boolean { + const pattern = service === 'xray' ? AWS_TRACES_OTLP_ENDPOINT_PATTERN : AWS_LOGS_OTLP_ENDPOINT_PATTERN; + + return new RegExp(pattern).test(otlpEndpoint.toLowerCase()); +} + +/** + * Checks if x-aws-log-group and x-aws-log-stream are present in the headers in order to send logs to + * AWS OTLP Logs endpoint. + */ +function validateLogsHeaders() { + const logsHeaders = process.env['OTEL_EXPORTER_OTLP_LOGS_HEADERS']; + + if (!logsHeaders) { + diag.warn( + 'Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS ' + + 'to include x-aws-log-group and x-aws-log-stream' + ); + return false; + } + + let filteredLogHeadersCount = 0; + + for (const pair of logsHeaders.split(',')) { + if (pair.includes('=')) { + const [key, value] = pair.split('=', 2); + if ((key === AWS_OTLP_LOGS_GROUP_HEADER || key === AWS_OTLP_LOGS_STREAM_HEADER) && value) { + filteredLogHeadersCount += 1; + } + } + } + + if (filteredLogHeadersCount !== 2) { + diag.warn( + 'Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS ' + + 'to have values for x-aws-log-group and x-aws-log-stream' + ); + return false; + } + + return true; } // END The OpenTelemetry Authors code diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts index c4e96eed..8bf6984e 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts @@ -1,80 +1,92 @@ -// import { diag } from '@opentelemetry/api'; -// import { getNodeVersion } from '../../../../utils'; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { diag } from '@opentelemetry/api'; +import { getNodeVersion } from '../../../../utils'; +let SignatureV4: any; +let HttpRequest: any; +let defaultProvider: any; +let Sha256: any; -// let SignatureV4: any; -// let HttpRequest: any; -// let defaultProvider: any; -// let Sha256: any; +let dependenciesLoaded = false; -// const nodeVersionSupported = getNodeVersion() >= 16; +if (getNodeVersion() >= 16) { + try { + defaultProvider = require('@aws-sdk/credential-provider-node').defaultProvider; + Sha256 = require('@aws-crypto/sha256-js').Sha256; + SignatureV4 = require('@smithy/signature-v4').SignatureV4; + HttpRequest = require('@smithy/protocol-http').HttpRequest; + dependenciesLoaded = true; + } catch (error) { + diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`); + } +} else { + diag.error('SigV4 signing requires at least Node major version 16'); +} -// if (nodeVersionSupported) { -// try { -// const { defaultProvider: awsDefaultProvider } = require('@aws-sdk/credential-provider-node'); -// const { Sha256: awsSha256 } = require('@aws-crypto/sha256-js'); -// const { SignatureV4: awsSignatureV4 } = require('@smithy/signature-v4'); -// const { HttpRequest: awsHttpRequest } = require('@smithy/protocol-http'); - -// // Assign to module-level variables -// defaultProvider = awsDefaultProvider; -// Sha256 = awsSha256; -// SignatureV4 = awsSignatureV4; -// HttpRequest = awsHttpRequest; -// } catch (error) { -// diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`); -// } -// } +export class AwsAuthenticator { + private region: string; + private service: string; -// export class AwsAuthenticator { + constructor(region: string, service: string) { + this.region = region; + this.service = service; + } -// private static readonly SERVICE_NAME: string = 'xray'; -// private endpoint: string; -// private region: string; -// private service: string; + public async authenticate(endpoint: string, headers: Record, serializedData: Uint8Array | undefined) { + // Only do SigV4 Signing if the required dependencies are installed. + if (dependenciesLoaded) { + const url = new URL(endpoint); -// // Holds the dependencies needed to sign the SigV4 headers -// private defaultProvider: any; -// private sha256: any; -// private signatureV4: any; -// private httpRequest: any; + if (serializedData === undefined) { + diag.error('Given serialized data is undefined. Not authenticating.'); + return headers; + } -// constructor(endpoint: string, region: string, service: string) { -// this.endpoint = endpoint; -// this.region = region; -// this.service = service; - -// } + const cleanedHeaders = this.removeSigV4Headers(headers); -// // if (oldHeaders) { -// // const request = new this.httpRequest({ -// // method: 'POST', -// // protocol: 'https', -// // hostname: url.hostname, -// // path: url.pathname, -// // body: serializedSpans, -// // headers: { -// // ...this.removeSigV4Headers(oldHeaders), -// // host: url.hostname, -// // }, -// // }); - -// // try { -// // const signer = new this.signatureV4({ -// // credentials: this.defaultProvider(), -// // region: this.region, -// // service: OTLPAwsSpanExporter.SERVICE_NAME, -// // sha256: this.sha256, -// // }); - -// // const signedRequest = await signer.sign(request); - -// // // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 -// // const newHeaders: () => Record = () => signedRequest.headers; -// // this['_delegate']._transport._transport._parameters.headers = newHeaders; -// // } catch (exception) { -// // diag.debug( -// // `Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}` -// // ); -// // } -// // } -// } \ No newline at end of file + const request = new HttpRequest({ + method: 'POST', + protocol: 'https', + hostname: url.hostname, + path: url.pathname, + body: serializedData, + headers: { + ...cleanedHeaders, + host: url.hostname, + }, + }); + + try { + const signer = new SignatureV4({ + credentials: defaultProvider(), + region: this.region, + service: this.service, + sha256: Sha256, + }); + + const signedRequest = await signer.sign(request); + + return signedRequest.headers; + } catch (exception) { + diag.debug( + `Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}` + ); + } + } + + return headers; + } + + // Cleans up Sigv4 from headers to avoid accidentally copying them to the new headers + private removeSigV4Headers(headers: Record) { + const newHeaders: Record = {}; + const sigV4Headers = ['x-amz-date', 'authorization', 'x-amz-content-sha256', 'x-amz-security-token']; + + for (const key in headers) { + if (!sigV4Headers.includes(key.toLowerCase())) { + newHeaders[key] = headers[key]; + } + } + return newHeaders; + } +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/utils.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/utils.ts new file mode 100644 index 00000000..15b4560c --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/utils.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; + +export function changeUrlConfig(endpoint: string, config?: OTLPExporterNodeConfigBase): OTLPExporterNodeConfigBase { + if (config) { + return { + ...config, + url: endpoint, + }; + } + + return { url: endpoint }; +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts index e69de29b..6a0fcfc0 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; +import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; +import { ProtobufLogsSerializer } from '@opentelemetry/otlp-transformer'; +import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; +import { AwsAuthenticator } from '../common/aws-authenticator'; +import { changeUrlConfig } from '../common/utils'; +import { ExportResult } from '@opentelemetry/core'; + +/** + * This exporter extends the functionality of the OTLPProtoLogExporter to allow spans to be exported + * to the CloudWatch Logs OTLP endpoint https://logs.[AWSRegion].amazonaws.com/v1/logs. Utilizes the aws-sdk + * library to sign and directly inject SigV4 Authentication to the exported request's headers. ... + * + * This only works with version >=16 Node.js environments. + */ +export class OTLPAwsLogExporter extends OTLPProtoLogExporter { + private endpoint: string; + private region: string; + private authenticator: AwsAuthenticator; + + constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { + super(changeUrlConfig(endpoint, config)); + this.region = endpoint.split('.')[1]; + this.endpoint = endpoint; + this.authenticator = new AwsAuthenticator(this.region, 'logs'); + } + + /** + * Overrides the upstream implementation of export. All behaviors are the same except if the + * endpoint is the CloudWatch Logs 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: ReadableLogRecord[], + resultCallback: (result: ExportResult) => void + ): Promise { + const serializedLogs: Uint8Array | undefined = ProtobufLogsSerializer.serializeRequest(items); + const headers = this['_delegate']._transport?._transport?._parameters?.headers(); + + if (headers) { + const signedRequest = await this.authenticator.authenticate(this.endpoint, headers, serializedLogs); + + // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 + const newHeaders: () => Record = () => signedRequest; + this['_delegate']._transport._transport._parameters.headers = newHeaders; + } + + super.export(items, resultCallback); + } +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts index 17a5b20d..390138cb 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -1,36 +1,12 @@ // 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 { ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { ExportResult } from '@opentelemetry/core'; -import { getNodeVersion } from '../../../../utils'; -let SignatureV4: any; -let HttpRequest: any; -let defaultProvider: any; -let Sha256: any; - -const nodeVersionSupported = getNodeVersion() >= 16; - -if (nodeVersionSupported) { - try { - const { defaultProvider: awsDefaultProvider } = require('@aws-sdk/credential-provider-node'); - const { Sha256: awsSha256 } = require('@aws-crypto/sha256-js'); - const { SignatureV4: awsSignatureV4 } = require('@smithy/signature-v4'); - const { HttpRequest: awsHttpRequest } = require('@smithy/protocol-http'); - - // Assign to module-level variables - defaultProvider = awsDefaultProvider; - Sha256 = awsSha256; - SignatureV4 = awsSignatureV4; - HttpRequest = awsHttpRequest; - } catch (error) { - diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`); - } -} - +import { AwsAuthenticator } from '../common/aws-authenticator'; +import { changeUrlConfig } from '../common/utils'; /** * This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported @@ -41,24 +17,15 @@ if (nodeVersionSupported) { * This only works with version >=16 Node.js environments. */ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { - private static readonly SERVICE_NAME: string = 'xray'; private endpoint: string; private region: string; - - // // Holds the dependencies needed to sign the SigV4 headers - // private defaultProvider: any; - // private sha256: any; - // private signatureV4: any; - // private httpRequest: any; - - // If the required dependencies are installed then we enable SigV4 signing. Otherwise skip it - private hasRequiredDependencies: boolean = false; + private authenticator: AwsAuthenticator; constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { - super(OTLPAwsSpanExporter.changeUrlConfig(endpoint, config)); - // this.initDependencies(); + super(changeUrlConfig(endpoint, config)); this.region = endpoint.split('.')[1]; this.endpoint = endpoint; + this.authenticator = new AwsAuthenticator(this.region, 'xray'); } /** @@ -67,102 +34,17 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { * sending it to the endpoint. Otherwise, we will skip signing. */ public override async export(items: ReadableSpan[], resultCallback: (result: ExportResult) => void): Promise { - // Only do SigV4 Signing if the required dependencies are installed. Otherwise default to the regular http/protobuf exporter. - if (this.hasRequiredDependencies) { - 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['_delegate']._transport?._transport?._parameters?.headers(); - - if (oldHeaders) { - const request = new HttpRequest.httpRequest({ - method: 'POST', - protocol: 'https', - hostname: url.hostname, - path: url.pathname, - body: serializedSpans, - headers: { - ...this.removeSigV4Headers(oldHeaders), - host: url.hostname, - }, - }); - - try { - const signer = new SignatureV4({ - credentials: defaultProvider(), - region: this.region, - service: OTLPAwsSpanExporter.SERVICE_NAME, - sha256: Sha256, - }); + const serializedSpans: Uint8Array | undefined = ProtobufTraceSerializer.serializeRequest(items); + const headers = this['_delegate']._transport?._transport?._parameters?.headers(); - const signedRequest = await signer.sign(request); + if (headers) { + const signedRequest = await this.authenticator.authenticate(this.endpoint, headers, serializedSpans); - // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 - const newHeaders: () => Record = () => signedRequest.headers; - this['_delegate']._transport._transport._parameters.headers = newHeaders; - } catch (exception) { - diag.debug( - `Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}` - ); - } - } + // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 + const newHeaders: () => Record = () => signedRequest; + this['_delegate']._transport._transport._parameters.headers = newHeaders; } super.export(items, resultCallback); } - - // Removes Sigv4 headers from old headers to avoid accidentally copying them to the new headers - private removeSigV4Headers(headers: Record) { - const newHeaders: Record = {}; - const sigV4Headers = ['x-amz-date', 'authorization', 'x-amz-content-sha256', 'x-amz-security-token']; - - for (const key in headers) { - if (!sigV4Headers.includes(key.toLowerCase())) { - newHeaders[key] = headers[key]; - } - } - return newHeaders; - } - - // private initDependencies(): any { - // if (getNodeVersion() < 16) { - // diag.error('SigV4 signing requires atleast Node major version 16'); - // return; - // } - - // try { - // const awsSdkModule = require('@aws-sdk/credential-provider-node'); - // const awsCryptoModule = require('@aws-crypto/sha256-js'); - // const signatureModule = require('@smithy/signature-v4'); - // const httpModule = require('@smithy/protocol-http'); - - // (this.defaultProvider = awsSdkModule.defaultProvider), - // (this.sha256 = awsCryptoModule.Sha256), - // (this.signatureV4 = signatureModule.SignatureV4), - // (this.httpRequest = httpModule.HttpRequest); - // this.hasRequiredDependencies = true; - // } catch (error) { - // diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`); - // } - // } - - private static changeUrlConfig(endpoint: string, config?: OTLPExporterNodeConfigBase): OTLPExporterNodeConfigBase { - const newConfig = - config == null - ? { url: endpoint } - : { - ...config, - url: endpoint, - }; - - return newConfig; - } } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts index bdea7eb9..0a1381e4 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts @@ -14,6 +14,7 @@ if (process.env.OTEL_TRACES_SAMPLER === 'xray') { } import { diag, DiagConsoleLogger, trace } from '@opentelemetry/api'; +import { logs } from '@opentelemetry/api-logs'; import { getNodeAutoInstrumentations, InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node'; import { Instrumentation } from '@opentelemetry/instrumentation'; import * as opentelemetry from '@opentelemetry/sdk-node'; @@ -83,6 +84,9 @@ try { diag.info('Setting TraceProvider for instrumentations at the end of initialization'); for (const instrumentation of instrumentations) { instrumentation.setTracerProvider(trace.getTracerProvider()); + if (instrumentation.setLoggerProvider) { + instrumentation.setLoggerProvider(logs.getLoggerProvider()); + } } diag.debug(`Environment variable OTEL_PROPAGATORS is set to '${process.env.OTEL_PROPAGATORS}'`); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-opentelemetry-configurator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-opentelemetry-configurator.test.ts index e4d3d83b..2c2f0788 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-opentelemetry-configurator.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-opentelemetry-configurator.test.ts @@ -7,6 +7,9 @@ import { OTLPMetricExporter as OTLPHttpOTLPMetricExporter } from '@opentelemetry import { OTLPTraceExporter as OTLPGrpcTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; import { OTLPTraceExporter as OTLPHttpTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; +import { OTLPLogExporter as OTLPGrpcLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc'; +import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; +import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; import { Resource } from '@opentelemetry/resources'; import { PushMetricExporter } from '@opentelemetry/sdk-metrics'; import { @@ -32,6 +35,7 @@ import { AwsBatchUnsampledSpanProcessor } from '../src/aws-batch-unsampled-span- import { AwsMetricAttributesSpanExporter } from '../src/aws-metric-attributes-span-exporter'; import { ApplicationSignalsExporterProvider, + AwsLoggerProcessorProvider, AwsOpentelemetryConfigurator, AwsSpanProcessorProvider, customBuildSamplerFromEnv, @@ -42,6 +46,9 @@ import { setAwsDefaultEnvironmentVariables } from '../src/register'; import { AwsXRayRemoteSampler } from '../src/sampler/aws-xray-remote-sampler'; import { AwsXraySamplingClient } from '../src/sampler/aws-xray-sampling-client'; import { GetSamplingRulesResponse } from '../src/sampler/remote-sampler.types'; +import { LogRecordExporter } from '@opentelemetry/sdk-logs'; +import { OTLPAwsLogExporter } from '../src/exporter/otlp/aws/logs/otlp-aws-log-exporter'; +import { OTLPAwsSpanExporter } from '../src/exporter/otlp/aws/traces/otlp-aws-span-exporter'; // Tests AwsOpenTelemetryConfigurator after running Environment Variable setup in register.ts describe('AwsOpenTelemetryConfiguratorTest', () => { @@ -635,36 +642,284 @@ describe('AwsOpenTelemetryConfiguratorTest', () => { delete process.env.OTEL_NODE_RESOURCE_DETECTORS; }); - it('AwsSpanProcessorProviderTest', () => { - let spanExporter; + describe('AwsSpanProcessorProviderTest', () => { + it('configureOtlp', () => { + let spanExporter; - // Test span exporter configurations via valid environment variables - delete process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; - spanExporter = AwsSpanProcessorProvider.configureOtlp(); - expect(spanExporter).toBeInstanceOf(OTLPProtoTraceExporter); + // Test span exporter configurations via valid environment variables + delete process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + spanExporter = AwsSpanProcessorProvider.configureOtlp(); + expect(spanExporter).toBeInstanceOf(OTLPProtoTraceExporter); - process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; - spanExporter = AwsSpanProcessorProvider.configureOtlp(); - expect(spanExporter).toBeInstanceOf(OTLPGrpcTraceExporter); + process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; + spanExporter = AwsSpanProcessorProvider.configureOtlp(); + expect(spanExporter).toBeInstanceOf(OTLPGrpcTraceExporter); - process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'http/json'; - spanExporter = AwsSpanProcessorProvider.configureOtlp(); - expect(spanExporter).toBeInstanceOf(OTLPHttpTraceExporter); + process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'http/json'; + spanExporter = AwsSpanProcessorProvider.configureOtlp(); + expect(spanExporter).toBeInstanceOf(OTLPHttpTraceExporter); - process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'http/protobuf'; - spanExporter = AwsSpanProcessorProvider.configureOtlp(); - expect(spanExporter).toBeInstanceOf(OTLPProtoTraceExporter); + process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'http/protobuf'; + spanExporter = AwsSpanProcessorProvider.configureOtlp(); + expect(spanExporter).toBeInstanceOf(OTLPProtoTraceExporter); - process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'udp'; - spanExporter = AwsSpanProcessorProvider.configureOtlp(); - expect(spanExporter).toBeInstanceOf(OTLPUdpSpanExporter); + process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'udp'; + spanExporter = AwsSpanProcessorProvider.configureOtlp(); + expect(spanExporter).toBeInstanceOf(OTLPUdpSpanExporter); - // Test that a default span exporter is configured via invalid environment variable - process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'invalid_protocol'; - spanExporter = AwsSpanProcessorProvider.configureOtlp(); - expect(spanExporter).toBeInstanceOf(OTLPProtoTraceExporter); + // Test that a default span exporter is configured via invalid environment variable + process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'invalid_protocol'; + spanExporter = AwsSpanProcessorProvider.configureOtlp(); + expect(spanExporter).toBeInstanceOf(OTLPProtoTraceExporter); - // Cleanup - delete process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + // Cleanup + delete process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + + it('configureOtlp - OtlpAwsSpanExporter', () => { + const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'; + const OTEL_TRACES_EXPORTER = 'OTEL_TRACES_EXPORTER'; + + const tracesGoodEndpoints = [ + 'https://xray.us-east-1.amazonaws.com/v1/traces', + 'https://XRAY.US-EAST-1.AMAZONAWS.COM/V1/TRACES', + 'https://xray.us-east-1.amazonaws.com/v1/traces', + 'https://XRAY.US-EAST-1.amazonaws.com/v1/traces', + 'https://xray.US-EAST-1.AMAZONAWS.com/v1/traces', + 'https://Xray.Us-East-1.amazonaws.com/v1/traces', + 'https://xRAY.us-EAST-1.amazonaws.com/v1/traces', + 'https://XRAY.us-EAST-1.AMAZONAWS.com/v1/TRACES', + 'https://xray.US-EAST-1.amazonaws.com/V1/Traces', + 'https://xray.us-east-1.AMAZONAWS.COM/v1/traces', + 'https://XrAy.Us-EaSt-1.AmAzOnAwS.cOm/V1/TrAcEs', + 'https://xray.US-EAST-1.amazonaws.com/v1/traces', + 'https://xray.us-east-1.amazonaws.com/V1/TRACES', + 'https://XRAY.US-EAST-1.AMAZONAWS.COM/v1/traces', + 'https://xray.us-east-1.AMAZONAWS.COM/V1/traces', + ]; + + const tracesBadEndpoints = [ + 'http://localhost:4318/v1/traces', + 'http://xray.us-east-1.amazonaws.com/v1/traces', + 'ftp://xray.us-east-1.amazonaws.com/v1/traces', + 'https://ray.us-east-1.amazonaws.com/v1/traces', + 'https://xra.us-east-1.amazonaws.com/v1/traces', + 'https://x-ray.us-east-1.amazonaws.com/v1/traces', + 'https://xray.amazonaws.com/v1/traces', + 'https://xray.us-east-1.amazon.com/v1/traces', + 'https://xray.us-east-1.aws.com/v1/traces', + 'https://xray.us_east_1.amazonaws.com/v1/traces', + 'https://xray.us.east.1.amazonaws.com/v1/traces', + 'https://xray..amazonaws.com/v1/traces', + 'https://xray.us-east-1.amazonaws.com/traces', + 'https://xray.us-east-1.amazonaws.com/v2/traces', + 'https://xray.us-east-1.amazonaws.com/v1/trace', + 'https://xray.us-east-1.amazonaws.com/v1/traces/', + 'https://xray.us-east-1.amazonaws.com//v1/traces', + 'https://xray.us-east-1.amazonaws.com/v1//traces', + 'https://xray.us-east-1.amazonaws.com/v1/traces?param=value', + 'https://xray.us-east-1.amazonaws.com/v1/traces#fragment', + 'https://xray.us-east-1.amazonaws.com:443/v1/traces', + 'https:/xray.us-east-1.amazonaws.com/v1/traces', + 'https:://xray.us-east-1.amazonaws.com/v1/traces', + ]; + + const goodConfigs = []; + const badConfigs = []; + + // good configurations + for (const endpoint of tracesGoodEndpoints) { + const config = { + [OTEL_TRACES_EXPORTER]: 'otlp', + [OTEL_EXPORTER_OTLP_TRACES_ENDPOINT]: endpoint, + }; + goodConfigs.push(config); + } + + // Cbad configurations with bad endpoints + for (const endpoint of tracesBadEndpoints) { + const config = { + [OTEL_TRACES_EXPORTER]: 'otlp', + [OTEL_EXPORTER_OTLP_TRACES_ENDPOINT]: endpoint, + }; + badConfigs.push(config); + } + + // Test good configurations + for (const config of goodConfigs) { + customizeExporterTest(config, () => [AwsSpanProcessorProvider.configureOtlp()], OTLPAwsSpanExporter); + } + + // Test bad configurations + for (const config of badConfigs) { + customizeExporterTest(config, () => [AwsSpanProcessorProvider.configureOtlp()], OTLPProtoTraceExporter); + } + }); + }); + + describe('AwsLoggerProcessorProvider', () => { + it('configureLogExportersFromEnv', () => { + let logsExporter: LogRecordExporter[]; + + delete process.env.OTEL_LOGS_EXPORTER; + // Test span exporter configurations via valid environment variables + delete process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL; + logsExporter = AwsLoggerProcessorProvider.configureLogExportersFromEnv(); + expect(logsExporter).toHaveLength(1); + expect(logsExporter[0]).toBeInstanceOf(OTLPProtoLogExporter); + + process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'http/protobuf'; + logsExporter = AwsLoggerProcessorProvider.configureLogExportersFromEnv(); + expect(logsExporter).toHaveLength(1); + expect(logsExporter[0]).toBeInstanceOf(OTLPProtoLogExporter); + + process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'grpc'; + logsExporter = AwsLoggerProcessorProvider.configureLogExportersFromEnv(); + expect(logsExporter).toHaveLength(1); + expect(logsExporter[0]).toBeInstanceOf(OTLPGrpcLogExporter); + + process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'http/json'; + logsExporter = AwsLoggerProcessorProvider.configureLogExportersFromEnv(); + expect(logsExporter).toHaveLength(1); + expect(logsExporter[0]).toBeInstanceOf(OTLPHttpLogExporter); + + // Test that a default span exporter is configured via invalid environment variable + process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'invalid_protocol'; + logsExporter = AwsLoggerProcessorProvider.configureLogExportersFromEnv(); + expect(logsExporter).toHaveLength(1); + expect(logsExporter[0]).toBeInstanceOf(OTLPProtoLogExporter); + + // Cleanup + delete process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL; + }); + + it('configureLogExportersFromEnv - OtlpAwsLogsExporter', () => { + const OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT'; + const OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'OTEL_EXPORTER_OTLP_LOGS_HEADERS'; + const OTEL_LOGS_EXPORTER = 'OTEL_LOGS_EXPORTER'; + + const logsGoodEndpoints = [ + 'https://logs.us-east-1.amazonaws.com/v1/logs', + 'https://LOGS.US-EAST-1.AMAZONAWS.COM/V1/LOGS', + 'https://logs.us-east-1.amazonaws.com/v1/logs', + 'https://LOGS.US-EAST-1.amazonaws.com/v1/logs', + 'https://logs.US-EAST-1.AMAZONAWS.com/v1/logs', + 'https://Logs.Us-East-1.amazonaws.com/v1/logs', + 'https://lOGS.us-EAST-1.amazonaws.com/v1/logs', + 'https://LOGS.us-EAST-1.AMAZONAWS.com/v1/LOGS', + 'https://logs.US-EAST-1.amazonaws.com/V1/Logs', + 'https://logs.us-east-1.AMAZONAWS.COM/v1/logs', + 'https://LoGs.Us-EaSt-1.AmAzOnAwS.cOm/V1/LoGs', + 'https://logs.US-EAST-1.amazonaws.com/v1/logs', + 'https://logs.us-east-1.amazonaws.com/V1/LOGS', + 'https://LOGS.US-EAST-1.AMAZONAWS.COM/v1/logs', + 'https://logs.us-east-1.AMAZONAWS.COM/V1/logs', + ]; + + const logsBadEndpoints = [ + 'http://localhost:4318/v1/logs', + 'http://logs.us-east-1.amazonaws.com/v1/logs', + 'ftp://logs.us-east-1.amazonaws.com/v1/logs', + 'https://log.us-east-1.amazonaws.com/v1/logs', + 'https://logging.us-east-1.amazonaws.com/v1/logs', + 'https://cloud-logs.us-east-1.amazonaws.com/v1/logs', + 'https://logs.amazonaws.com/v1/logs', + 'https://logs.us-east-1.amazon.com/v1/logs', + 'https://logs.us-east-1.aws.com/v1/logs', + 'https://logs.us_east_1.amazonaws.com/v1/logs', + 'https://logs.us.east.1.amazonaws.com/v1/logs', + 'https://logs..amazonaws.com/v1/logs', + 'https://logs.us-east-1.amazonaws.com/logs', + 'https://logs.us-east-1.amazonaws.com/v2/logs', + 'https://logs.us-east-1.amazonaws.com/v1/log', + 'https://logs.us-east-1.amazonaws.com/v1/logs/', + 'https://logs.us-east-1.amazonaws.com//v1/logs', + 'https://logs.us-east-1.amazonaws.com/v1//logs', + 'https://logs.us-east-1.amazonaws.com/v1/logs?param=value', + 'https://logs.us-east-1.amazonaws.com/v1/logs#fragment', + 'https://logs.us-east-1.amazonaws.com:443/v1/logs', + 'https:/logs.us-east-1.amazonaws.com/v1/logs', + 'https:://logs.us-east-1.amazonaws.com/v1/logs', + 'https://logs.us-east-1.amazonaws.com/v1/logging', + 'https://logs.us-east-1.amazonaws.com/v1/cloudwatchlogs', + 'https://logs.us-east-1.amazonaws.com/v1/cwlogs', + ]; + + const logsBadHeaders = [ + 'x-aws-log-group=,x-aws-log-stream=test', + 'x-aws-log-stream=test', + 'x-aws-log-group=test', + '', + ]; + + const goodConfigs = []; + const badConfigs = []; + + // good configurations + for (const endpoint of logsGoodEndpoints) { + const config = { + [OTEL_LOGS_EXPORTER]: 'otlp', + [OTEL_EXPORTER_OTLP_LOGS_ENDPOINT]: endpoint, + [OTEL_EXPORTER_OTLP_LOGS_HEADERS]: 'x-aws-log-group=test,x-aws-log-stream=test', + }; + goodConfigs.push(config); + } + + // Cbad configurations with bad endpoints + for (const endpoint of logsBadEndpoints) { + const config = { + [OTEL_LOGS_EXPORTER]: 'otlp', + [OTEL_EXPORTER_OTLP_LOGS_ENDPOINT]: endpoint, + [OTEL_EXPORTER_OTLP_LOGS_HEADERS]: 'x-aws-log-group=test,x-aws-log-stream=test', + }; + badConfigs.push(config); + } + + // bad configurations with bad headers + for (const headers of logsBadHeaders) { + const config = { + [OTEL_LOGS_EXPORTER]: 'otlp', + [OTEL_EXPORTER_OTLP_LOGS_ENDPOINT]: 'https://logs.us-east-1.amazonaws.com/v1/logs', + [OTEL_EXPORTER_OTLP_LOGS_HEADERS]: headers, + }; + badConfigs.push(config); + } + + // Test good configurations + for (const config of goodConfigs) { + customizeExporterTest( + config, + () => AwsLoggerProcessorProvider.configureLogExportersFromEnv(), + OTLPAwsLogExporter + ); + } + + // Test bad configurations + for (const config of badConfigs) { + customizeExporterTest( + config, + () => AwsLoggerProcessorProvider.configureLogExportersFromEnv(), + OTLPProtoLogExporter + ); + } + }); }); + + function customizeExporterTest( + config: { [x: string]: string }, + executor: () => LogRecordExporter[] | SpanExporter[], + expectedExporterType: { new (...args: any[]): any } + ) { + for (const key in config) { + process.env[key] = config[key]; + } + + const result = executor(); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(expectedExporterType); + + for (const key in config) { + delete process.env[key]; + } + } }); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts new file mode 100644 index 00000000..7096b498 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts @@ -0,0 +1,171 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it, beforeEach } from 'mocha'; +import * as proxyquire from 'proxyquire'; +import * as sinon from 'sinon'; +import expect from 'expect'; +import { AwsAuthenticator } from '../../../../../src/exporter/otlp/aws/common/aws-authenticator'; +import { + AUTHORIZATION_HEADER, + AWS_AUTH_PATH, + AWS_HTTP_MODULE, + AWS_OTLP_TRACES_ENDPOINT, + CREDENTIAL_PROVIDER_MODULE, + SHA_256_MODULE, + SIGNATURE_V4_MODULE, + X_AMZ_DATE_HEADER, + X_AMZ_SECURITY_TOKEN_HEADER, +} from './test-utils.test'; +import { getNodeVersion } from '../../../../../src/utils'; + +const mockCredentials = { + accessKeyId: 'test_access_key', + secretAccessKey: 'test_secret_key', + sessionToken: 'test_session_token', +}; + +// Sigv4 is only enabled for node version >= 16 +const version = getNodeVersion(); + +describe('AwsAuthenticator', () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('should not inject SigV4 Headers if required modules are not available', async () => { + const dependencies = [SIGNATURE_V4_MODULE, CREDENTIAL_PROVIDER_MODULE, SHA_256_MODULE, AWS_HTTP_MODULE]; + + dependencies.forEach(dependency => { + it(`should not sign headers if missing dependency: ${dependency}`, async () => { + Object.keys(require.cache).forEach(key => { + delete require.cache[key]; + }); + + const requireStub = sandbox.stub(require('module'), '_load'); + requireStub.withArgs(dependency).throws(new Error(`Cannot find module '${dependency}'`)); + requireStub.callThrough(); + + const { AwsAuthenticator: MockThrowableModuleAuthenticator } = require(AWS_AUTH_PATH); + + const result = await new MockThrowableModuleAuthenticator('us-east-1', 'xray').authenticate( + AWS_OTLP_TRACES_ENDPOINT, + {}, + new Uint8Array() + ); + + expect(result).not.toHaveProperty(AUTHORIZATION_HEADER); + expect(result).not.toHaveProperty(X_AMZ_DATE_HEADER); + expect(result).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); + }); + }); + }); + + it('should not inject SigV4 Headers if serialized data is undefined', async () => { + const authenticator = new AwsAuthenticator('us-east-1', 'xray'); + const result = await authenticator.authenticate(AWS_OTLP_TRACES_ENDPOINT, {}, undefined); + + expect(result).not.toHaveProperty(AUTHORIZATION_HEADER); + expect(result).not.toHaveProperty(X_AMZ_DATE_HEADER); + expect(result).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); + }); + + it('should inject SigV4 Headers', async () => { + const expected = { + [AUTHORIZATION_HEADER]: 'testAuth', + [X_AMZ_DATE_HEADER]: 'testDate', + [X_AMZ_SECURITY_TOKEN_HEADER]: 'testSecurityToken', + }; + + const AwsAuthenticatorWithMock = proxyquire(AWS_AUTH_PATH, { + [CREDENTIAL_PROVIDER_MODULE]: { + defaultProvider: () => Promise.resolve(mockCredentials), + }, + [SIGNATURE_V4_MODULE]: { + SignatureV4: class { + constructor() {} + sign(request: any) { + return Promise.resolve({ + headers: expected, + }); + } + }, + }, + }).AwsAuthenticator; + + const result = await new AwsAuthenticatorWithMock('us-east-1', 'xray').authenticate( + AWS_OTLP_TRACES_ENDPOINT, + { test: 'test' }, + new Uint8Array() + ); + + if (version >= 16) { + expect(result).toHaveProperty(AUTHORIZATION_HEADER); + expect(result).toHaveProperty(X_AMZ_DATE_HEADER); + expect(result).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); + + expect(result[AUTHORIZATION_HEADER]).toBe(expected[AUTHORIZATION_HEADER]); + expect(result[X_AMZ_DATE_HEADER]).toBe(expected[X_AMZ_DATE_HEADER]); + expect(result[X_AMZ_SECURITY_TOKEN_HEADER]).toBe(expected[X_AMZ_SECURITY_TOKEN_HEADER]); + } else { + expect(result).not.toHaveProperty(AUTHORIZATION_HEADER); + expect(result).not.toHaveProperty(X_AMZ_DATE_HEADER); + expect(result).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); + } + }); + + it('should clear SigV4 headers if already present ', async () => { + const notExpected = { + [AUTHORIZATION_HEADER]: 'notExpectedAuth', + [X_AMZ_DATE_HEADER]: 'notExpectedDate', + [X_AMZ_SECURITY_TOKEN_HEADER]: 'notExpectedSecurityToken', + }; + + const expected = { + [AUTHORIZATION_HEADER]: 'testAuth', + [X_AMZ_DATE_HEADER]: 'testDate', + [X_AMZ_SECURITY_TOKEN_HEADER]: 'testSecurityToken', + }; + + const AwsAuthenticatorWithMock = proxyquire(AWS_AUTH_PATH, { + [CREDENTIAL_PROVIDER_MODULE]: { + defaultProvider: () => Promise.resolve(mockCredentials), + }, + [SIGNATURE_V4_MODULE]: { + SignatureV4: class { + constructor() {} + sign(request: any) { + return Promise.resolve({ + headers: expected, + }); + } + }, + }, + }).AwsAuthenticator; + + const result = await new AwsAuthenticatorWithMock('us-east-1', 'xray').authenticate( + AWS_OTLP_TRACES_ENDPOINT, + notExpected, + new Uint8Array() + ); + expect(result).toHaveProperty(AUTHORIZATION_HEADER); + expect(result).toHaveProperty(X_AMZ_DATE_HEADER); + expect(result).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); + + if (version >= 16) { + expect(result[AUTHORIZATION_HEADER]).toBe(expected[AUTHORIZATION_HEADER]); + expect(result[X_AMZ_DATE_HEADER]).toBe(expected[X_AMZ_DATE_HEADER]); + expect(result[X_AMZ_SECURITY_TOKEN_HEADER]).toBe(expected[X_AMZ_SECURITY_TOKEN_HEADER]); + } else { + expect(result[AUTHORIZATION_HEADER]).toBe(notExpected[AUTHORIZATION_HEADER]); + expect(result[X_AMZ_DATE_HEADER]).toBe(notExpected[X_AMZ_DATE_HEADER]); + expect(result[X_AMZ_SECURITY_TOKEN_HEADER]).toBe(notExpected[X_AMZ_SECURITY_TOKEN_HEADER]); + } + }); +}); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/test-utils.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/test-utils.test.ts new file mode 100644 index 00000000..cdf04829 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/test-utils.test.ts @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +export const AWS_AUTH_PATH = '../../../../../src/exporter/otlp/aws/common/aws-authenticator'; +export const AWS_SPAN_EXPORTER_PATH = '../../../../../src/exporter/otlp/aws/traces/otlp-aws-span-exporter'; +export const AWS_LOG_EXPORTER_PATH = '../../../../../src/exporter/otlp/aws/logs/otlp-aws-log-exporter'; + +export const SIGNATURE_V4_MODULE = '@smithy/signature-v4'; +export const CREDENTIAL_PROVIDER_MODULE = '@aws-sdk/credential-provider-node'; +export const SHA_256_MODULE = '@aws-crypto/sha256-js'; +export const AWS_HTTP_MODULE = '@smithy/protocol-http'; + +export const AWS_OTLP_TRACES_ENDPOINT = 'https://xray.us-east-1.amazonaws.com'; +export const AWS_OTLP_TRACES_ENDPOINT_PATH = '/v1/traces'; + +export const AWS_OTLP_LOGS_ENDPOINT = 'https://logs.us-east-1.amazonaws.com'; +export const AWS_OTLP_LOGS_ENDPOINT_PATH = '/v1/logs'; + +export const AUTHORIZATION_HEADER = 'authorization'; +export const X_AMZ_DATE_HEADER = 'x-amz-date'; +export const X_AMZ_SECURITY_TOKEN_HEADER = 'x-amz-security-token'; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts new file mode 100644 index 00000000..3e19eeac --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import expect from 'expect'; +import { + AWS_OTLP_LOGS_ENDPOINT, + AWS_OTLP_LOGS_ENDPOINT_PATH, + AUTHORIZATION_HEADER, + X_AMZ_DATE_HEADER, + X_AMZ_SECURITY_TOKEN_HEADER, + AWS_LOG_EXPORTER_PATH, +} from '../common/test-utils.test'; +import * as sinon from 'sinon'; +import * as proxyquire from 'proxyquire'; +import * as nock from 'nock'; + +const EXPECTED_AUTH_HEADER = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/logs/aws4_request'; +const EXPECTED_AUTH_X_AMZ_DATE = 'some_date'; +const EXPECTED_AUTH_SECURITY_TOKEN = 'test_token'; + +describe('OTLPAwsLogExporter', () => { + let sandbox: sinon.SinonSandbox; + let scope: nock.Scope; + let mockModule: any; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + scope = nock(AWS_OTLP_LOGS_ENDPOINT) + .post(AWS_OTLP_LOGS_ENDPOINT_PATH) + .reply((uri: any, requestBody: any) => { + return [200, '']; + }); + + mockModule = proxyquire(AWS_LOG_EXPORTER_PATH, { + '../common/aws-authenticator': { + AwsAuthenticator: class MockAwsAuthenticator { + constructor() {} + async authenticate(endpoint: string, headers: Record) { + return { + ...headers, + [AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER, + [X_AMZ_DATE_HEADER]: EXPECTED_AUTH_X_AMZ_DATE, + [X_AMZ_SECURITY_TOKEN_HEADER]: EXPECTED_AUTH_SECURITY_TOKEN, + }; + } + }, + }, + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('Should inject SigV4 Headers successfully', done => { + const exporter = new mockModule.OTLPAwsLogExporter(AWS_OTLP_LOGS_ENDPOINT + AWS_OTLP_LOGS_ENDPOINT_PATH); + + exporter + .export([], () => {}) + .then(() => { + scope.on('request', (req, interceptor, body) => { + const headers = req.headers; + expect(headers).toHaveProperty(AUTHORIZATION_HEADER.toLowerCase()); + expect(headers).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()); + expect(headers).toHaveProperty(X_AMZ_DATE_HEADER.toLowerCase()); + + expect(headers[AUTHORIZATION_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_HEADER); + expect(headers[X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_SECURITY_TOKEN); + expect(headers[X_AMZ_DATE_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_X_AMZ_DATE); + + expect(headers['content-type']).toBe('application/x-protobuf'); + expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); + done(); + }); + }); + }); +}); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts new file mode 100644 index 00000000..4e968dbc --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts @@ -0,0 +1,77 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import expect from 'expect'; +import { + AWS_OTLP_TRACES_ENDPOINT, + AUTHORIZATION_HEADER, + X_AMZ_DATE_HEADER, + X_AMZ_SECURITY_TOKEN_HEADER, + AWS_OTLP_TRACES_ENDPOINT_PATH, + AWS_SPAN_EXPORTER_PATH, +} from '../common/test-utils.test'; +import * as sinon from 'sinon'; +import * as proxyquire from 'proxyquire'; +import * as nock from 'nock'; + +const EXPECTED_AUTH_HEADER = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request'; +const EXPECTED_AUTH_X_AMZ_DATE = 'some_date'; +const EXPECTED_AUTH_SECURITY_TOKEN = 'test_token'; + +describe('OTLPAwsSpanExporter', () => { + let sandbox: sinon.SinonSandbox; + let scope: nock.Scope; + let mockModule: any; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + scope = nock(AWS_OTLP_TRACES_ENDPOINT) + .post(AWS_OTLP_TRACES_ENDPOINT_PATH) + .reply((uri: any, requestBody: any) => { + return [200, '']; + }); + + mockModule = proxyquire(AWS_SPAN_EXPORTER_PATH, { + '../common/aws-authenticator': { + AwsAuthenticator: class MockAwsAuthenticator { + constructor() {} + async authenticate(endpoint: string, headers: Record) { + return { + ...headers, + [AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER, + [X_AMZ_DATE_HEADER]: EXPECTED_AUTH_X_AMZ_DATE, + [X_AMZ_SECURITY_TOKEN_HEADER]: EXPECTED_AUTH_SECURITY_TOKEN, + }; + } + }, + }, + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('Should inject SigV4 Headers successfully', done => { + const exporter = new mockModule.OTLPAwsSpanExporter(AWS_OTLP_TRACES_ENDPOINT + AWS_OTLP_TRACES_ENDPOINT_PATH); + + exporter + .export([], () => {}) + .then(() => { + scope.on('request', (req, interceptor, body) => { + const headers = req.headers; + expect(headers).toHaveProperty(AUTHORIZATION_HEADER.toLowerCase()); + expect(headers).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()); + expect(headers).toHaveProperty(X_AMZ_DATE_HEADER.toLowerCase()); + + expect(headers[AUTHORIZATION_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_HEADER); + expect(headers[X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_SECURITY_TOKEN); + expect(headers[X_AMZ_DATE_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_X_AMZ_DATE); + + expect(headers['content-type']).toBe('application/x-protobuf'); + expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); + done(); + }); + }); + }); +}); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/otlp-aws-span-exporter.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/otlp-aws-span-exporter.test.ts deleted file mode 100644 index e71fdfd8..00000000 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/otlp-aws-span-exporter.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -// // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// // SPDX-License-Identifier: Apache-2.0 -// import expect from 'expect'; -// import { OTLPAwsSpanExporter } from '../src/exporter/otlp/aws/traces/otlp-aws-span-exporter'; -// import * as sinon from 'sinon'; -// import * as proxyquire from 'proxyquire'; -// import * as nock from 'nock'; -// import { getNodeVersion } from '../src/utils'; - -// const XRAY_OTLP_ENDPOINT = 'https://xray.us-east-1.amazonaws.com'; -// const XRAY_OTLP_ENDPOINT_PATH = '/v1/traces'; -// const AUTHORIZATION_HEADER = 'Authorization'; -// const X_AMZ_DATE_HEADER = 'X-Amz-Date'; -// const X_AMZ_SECURITY_TOKEN_HEADER = 'X-Amz-Security-Token'; - -// const EXPECTED_AUTH_HEADER = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request'; -// const EXPECTED_AUTH_X_AMZ_DATE = 'some_date'; -// const EXPECTED_AUTH_SECURITY_TOKEN = 'test_token'; - -// const nodeVersion = getNodeVersion(); - -// // SigV4 exporter requires packages that require Node environments >= 16 -// /* istanbul ignore next */ -// if (nodeVersion >= 16) { -// describe('OTLPAwsSpanExporter', () => { -// let sandbox: sinon.SinonSandbox; -// let scope: nock.Scope; -// let mockModule: any; - -// beforeEach(() => { -// sandbox = sinon.createSandbox(); - -// scope = nock(XRAY_OTLP_ENDPOINT) -// .post(XRAY_OTLP_ENDPOINT_PATH) -// .reply((uri: any, requestBody: any) => { -// return [200, '']; -// }); - -// mockModule = proxyquire('../src/otlp-aws-span-exporter', { -// '@smithy/signature-v4': { -// SignatureV4: class MockSignatureV4 { -// sign(req: any) { -// req.headers = { -// ...req.headers, -// [AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER, -// [X_AMZ_DATE_HEADER]: EXPECTED_AUTH_X_AMZ_DATE, -// [X_AMZ_SECURITY_TOKEN_HEADER]: EXPECTED_AUTH_SECURITY_TOKEN, -// }; - -// return req; -// } -// }, -// }, -// '@aws-sdk/credential-provider-node': { -// defaultProvider: () => async () => { -// return { -// accessKeyId: 'test_access_key', -// secretAccessKey: 'test_secret_key', -// }; -// }, -// }, -// }); -// }); - -// afterEach(() => { -// sandbox.restore(); -// }); - -// it('Should inject SigV4 Headers successfully', done => { -// const exporter = new mockModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); - -// exporter -// .export([], () => {}) -// .then(() => { -// scope.on('request', (req, interceptor, body) => { -// const headers = req.headers; -// expect(headers).toHaveProperty(AUTHORIZATION_HEADER.toLowerCase()); -// expect(headers).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()); -// expect(headers).toHaveProperty(X_AMZ_DATE_HEADER.toLowerCase()); - -// expect(headers[AUTHORIZATION_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_HEADER); -// expect(headers[X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_SECURITY_TOKEN); -// expect(headers[X_AMZ_DATE_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_X_AMZ_DATE); - -// expect(headers['content-type']).toBe('application/x-protobuf'); -// expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); -// done(); -// }); -// }); -// }); - -// describe('Should not inject SigV4 headers if dependencies are missing', () => { -// const dependencies = [ -// '@aws-sdk/credential-provider-node', -// '@aws-crypto/sha256-js', -// '@smithy/signature-v4', -// '@smithy/protocol-http', -// ]; - -// dependencies.forEach(dependency => { -// it(`should not sign headers if missing dependency: ${dependency}`, done => { -// const exporter = new OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); - -// Object.keys(require.cache).forEach(key => { -// delete require.cache[key]; -// }); -// const requireStub = sandbox.stub(require('module'), '_load'); -// requireStub.withArgs(dependency).throws(new Error(`Cannot find module '${dependency}'`)); -// requireStub.callThrough(); - -// exporter -// .export([], () => {}) -// .then(() => { -// scope.on('request', (req, interceptor, body) => { -// const headers = req.headers; -// expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); -// expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); -// expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); - -// expect(headers['content-type']).toBe('application/x-protobuf'); -// expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); -// done(); -// }); -// }); -// }); -// }); -// }); - -// it('should not inject SigV4 headers if failure to sign headers', done => { -// const stubbedModule = proxyquire('../src/otlp-aws-span-exporter', { -// '@smithy/signature-v4': { -// SignatureV4: class MockSignatureV4 { -// sign() { -// throw new Error('signing error'); -// } -// }, -// }, -// }); - -// const exporter = new stubbedModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); - -// exporter -// .export([], () => {}) -// .then(() => { -// scope.on('request', (req, interceptor, body) => { -// const headers = req.headers; -// expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); -// expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); -// expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); - -// expect(headers['content-type']).toBe('application/x-protobuf'); -// expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); -// done(); -// }); -// }); -// }); - -// it('should not inject SigV4 headers if failure to retrieve credentials', done => { -// const stubbedModule = proxyquire('../src/otlp-aws-span-exporter', { -// '@aws-sdk/credential-provider-node': { -// defaultProvider: () => async () => { -// throw new Error('credentials error'); -// }, -// }, -// }); - -// const exporter = new stubbedModule.OTLPAwsSpanExporter(XRAY_OTLP_ENDPOINT + XRAY_OTLP_ENDPOINT_PATH); - -// exporter -// .export([], () => {}) -// .then(() => { -// scope.on('request', (req, interceptor, body) => { -// const headers = req.headers; -// expect(headers).not.toHaveProperty(AUTHORIZATION_HEADER); -// expect(headers).not.toHaveProperty(X_AMZ_DATE_HEADER); -// expect(headers).not.toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER); - -// expect(headers['content-type']).toBe('application/x-protobuf'); -// expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); -// done(); -// }); -// }); -// }); -// }); -// } diff --git a/package-lock.json b/package-lock.json index 03c19fb2..18722309 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,8 @@ "@opentelemetry/auto-configuration-propagators": "0.3.2", "@opentelemetry/auto-instrumentations-node": "0.56.0", "@opentelemetry/core": "1.30.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.57.1", + "@opentelemetry/exporter-logs-otlp-http": "0.57.1", "@opentelemetry/exporter-logs-otlp-proto": "0.57.1", "@opentelemetry/exporter-metrics-otlp-grpc": "0.57.1", "@opentelemetry/exporter-metrics-otlp-http": "0.57.1", @@ -54,6 +56,7 @@ "@opentelemetry/propagator-aws-xray": "1.26.2", "@opentelemetry/resource-detector-aws": "1.12.0", "@opentelemetry/resources": "1.30.1", + "@opentelemetry/sdk-logs": "0.57.1", "@opentelemetry/sdk-metrics": "1.30.1", "@opentelemetry/sdk-node": "0.57.1", "@opentelemetry/sdk-trace-base": "1.30.1", From 8f753e3c576d34657e359f68ddaf51e0a0c01377 Mon Sep 17 00:00:00 2001 From: liustve Date: Fri, 20 Jun 2025 21:34:08 +0000 Subject: [PATCH 03/11] add preprocessing for compression --- .../src/aws-opentelemetry-configurator.ts | 9 ++- .../otlp/aws/common/passthrough-serializer.ts | 46 +++++++++++++ .../otlp/aws/logs/otlp-aws-log-exporter.ts | 65 ++++++++++++++++--- .../otlp/aws/traces/otlp-aws-span-exporter.ts | 49 ++++++++++++-- .../src/register.ts | 3 - 5 files changed, 151 insertions(+), 21 deletions(-) create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/passthrough-serializer.ts diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts index e7447775..a540ae74 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts @@ -7,6 +7,7 @@ import { getPropagator } from '@opentelemetry/auto-configuration-propagators'; import { getResourceDetectors as getResourceDetectorsFromEnv } from '@opentelemetry/auto-instrumentations-node'; import { ENVIRONMENT, TracesSamplerValues, getEnv, getEnvWithoutDefaults } from '@opentelemetry/core'; import { OTLPMetricExporter as OTLPGrpcOTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc'; +import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'; import { AggregationTemporalityPreference, OTLPMetricExporter as OTLPHttpOTLPMetricExporter, @@ -474,7 +475,9 @@ export class AwsLoggerProcessorProvider { validateLogsHeaders() ) { diag.debug('Detected CloudWatch Logs OTLP endpoint. Switching exporter to OTLPAwsLogExporter'); - exporters.push(new OTLPAwsLogExporter(otlpExporterLogsEndpoint)); + exporters.push( + new OTLPAwsLogExporter(otlpExporterLogsEndpoint, { compression: CompressionAlgorithm.GZIP }) + ); } else { exporters.push(new OTLPProtoLogExporter()); } @@ -491,7 +494,9 @@ export class AwsLoggerProcessorProvider { validateLogsHeaders() ) { diag.debug('Detected CloudWatch Logs OTLP endpoint. Switching exporter to OTLPAwsLogExporter'); - exporters.push(new OTLPAwsLogExporter(otlpExporterLogsEndpoint)); + exporters.push( + new OTLPAwsLogExporter(otlpExporterLogsEndpoint, { compression: CompressionAlgorithm.GZIP }) + ); } else { exporters.push(new OTLPProtoLogExporter()); } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/passthrough-serializer.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/passthrough-serializer.ts new file mode 100644 index 00000000..74920eb8 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/passthrough-serializer.ts @@ -0,0 +1,46 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ISerializer } from '@opentelemetry/otlp-transformer'; + +/** + * A serializer that bypasses request serialization by returning pre-serialized data. + * @template Response The type of the deserialized response + */ +export class PassthroughSerializer implements ISerializer { + private serializedData: Uint8Array = new Uint8Array(); + private deserializer: (data: Uint8Array) => Response; + + /** + * Creates a new PassthroughSerializer instance. + * @param deserializer Function to deserialize response data + */ + constructor(deserializer: (data: Uint8Array) => Response) { + this.deserializer = deserializer; + } + + /** + * Sets the pre-serialized data to be returned when serializeRequest is called. + * @param data The serialized data to use + */ + setSerializedData(data: Uint8Array): void { + this.serializedData = data; + } + + /** + * Returns the pre-serialized data, ignoring the request parameter. + * @param request Ignored parameter. + * @returns The pre-serialized data + */ + serializeRequest(request: Uint8Array): Uint8Array { + return this.serializedData; + } + + /** + * Deserializes response data using the provided deserializer function. + * @param data The response data to deserialize + * @returns The deserialized response + */ + deserializeResponse(data: Uint8Array): Response { + return this.deserializer(data); + } +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts index 6a0fcfc0..be71e33c 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; -import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; -import { ProtobufLogsSerializer } from '@opentelemetry/otlp-transformer'; +import { CompressionAlgorithm, OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; +import { gzipSync } from 'zlib'; +import { IExportLogsServiceResponse, ProtobufLogsSerializer } from '@opentelemetry/otlp-transformer'; import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; import { AwsAuthenticator } from '../common/aws-authenticator'; -import { changeUrlConfig } from '../common/utils'; -import { ExportResult } from '@opentelemetry/core'; +import { ExportResult, ExportResultCode } from '@opentelemetry/core'; +import { PassthroughSerializer } from '../common/passthrough-serializer'; /** * This exporter extends the functionality of the OTLPProtoLogExporter to allow spans to be exported @@ -16,34 +17,78 @@ import { ExportResult } from '@opentelemetry/core'; * * This only works with version >=16 Node.js environments. */ + export class OTLPAwsLogExporter extends OTLPProtoLogExporter { + private compression: CompressionAlgorithm | undefined; private endpoint: string; private region: string; + private serializer: PassthroughSerializer; private authenticator: AwsAuthenticator; constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { - super(changeUrlConfig(endpoint, config)); + const modifiedConfig: OTLPExporterNodeConfigBase = { + ...config, + url: endpoint, + compression: CompressionAlgorithm.NONE, + }; + + super(modifiedConfig); + this.compression = config?.compression; this.region = endpoint.split('.')[1]; this.endpoint = endpoint; this.authenticator = new AwsAuthenticator(this.region, 'logs'); + + // This is used in order to prevent serializing and compressing the data twice. Once for signing Sigv4 and + // once when we pass the data to super.export() which will serialize and compress the data again. + this.serializer = new PassthroughSerializer(ProtobufLogsSerializer.deserializeResponse); + this['_delegate']._serializer = this.serializer; } /** - * Overrides the upstream implementation of export. All behaviors are the same except if the - * endpoint is the CloudWatch Logs OTLP endpoint, we will sign the request with SigV4 in headers before - * sending it to the endpoint. Otherwise, we will skip signing. + * Overrides the upstream implementation of export. If the + * endpoint is the CloudWatch Logs OTLP endpoint, we sign the request with SigV4 in headers. + * To prevent performance degradation from serializing and compressing data twice, we handle serialization and compression + * locally in this exporter and pass the pre-processed data to the upstream export functionality. */ + + // Upstream already implements a retry mechanism: + // https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/otlp-exporter-base/src/retrying-transport.ts + public override async export( items: ReadableLogRecord[], resultCallback: (result: ExportResult) => void ): Promise { - const serializedLogs: Uint8Array | undefined = ProtobufLogsSerializer.serializeRequest(items); + let serializedLogs: Uint8Array | undefined = ProtobufLogsSerializer.serializeRequest(items); + + if (serializedLogs === undefined) { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Nothing to send'), + }); + return; + } + + const shouldCompress = this.compression && this.compression !== CompressionAlgorithm.NONE; + if (shouldCompress) { + serializedLogs = gzipSync(serializedLogs); + } + + // Pass pre-processed data to passthrough serializer. When super.export() is called, the Passthrough Serializer will + // use the pre-processed data instead of serializing and compressing the data again. + this.serializer.setSerializedData(serializedLogs); + + // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 const headers = this['_delegate']._transport?._transport?._parameters?.headers(); if (headers) { + if (shouldCompress) { + headers['Content-Encoding'] = 'gzip'; + } else { + delete headers['Content-Encoding']; + } + const signedRequest = await this.authenticator.authenticate(this.endpoint, headers, serializedLogs); - // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 const newHeaders: () => Record = () => signedRequest; this['_delegate']._transport._transport._parameters.headers = newHeaders; } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts index 390138cb..15a5407d 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -1,12 +1,13 @@ // 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 { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; -import { ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer'; +import { CompressionAlgorithm, OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; +import { IExportTraceServiceResponse, ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { ExportResult } from '@opentelemetry/core'; +import { ExportResult, ExportResultCode } from '@opentelemetry/core'; import { AwsAuthenticator } from '../common/aws-authenticator'; -import { changeUrlConfig } from '../common/utils'; +import { PassthroughSerializer } from '../common/passthrough-serializer'; +import { gzipSync } from 'zlib'; /** * This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported @@ -17,27 +18,63 @@ import { changeUrlConfig } from '../common/utils'; * This only works with version >=16 Node.js environments. */ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { + private compression: CompressionAlgorithm | undefined; private endpoint: string; private region: string; + private serializer: PassthroughSerializer; private authenticator: AwsAuthenticator; constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { - super(changeUrlConfig(endpoint, config)); + const modifiedConfig: OTLPExporterNodeConfigBase = { + ...config, + url: endpoint, + compression: CompressionAlgorithm.NONE, + }; + + super(modifiedConfig); this.region = endpoint.split('.')[1]; this.endpoint = endpoint; this.authenticator = new AwsAuthenticator(this.region, 'xray'); + this.serializer = new PassthroughSerializer(ProtobufTraceSerializer.deserializeResponse); + this['_delegate']._serializer = this.serializer; } /** * 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. + * To prevent performance degradation from serializing and compressing data twice, we handle serialization and compression + * locally in this exporter and pass the pre-processed data to the upstream export functionality. */ public override async export(items: ReadableSpan[], resultCallback: (result: ExportResult) => void): Promise { - const serializedSpans: Uint8Array | undefined = ProtobufTraceSerializer.serializeRequest(items); + let serializedSpans: Uint8Array | undefined = ProtobufTraceSerializer.serializeRequest(items); + + if (serializedSpans === undefined) { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Nothing to send'), + }); + return; + } + + // Pass pre-processed data to passthrough serializer. When super.export() is called, the Passthrough Serializer will + // use the pre-processed data instead of serializing and compressing the data again. + const shouldCompress = this.compression && this.compression !== CompressionAlgorithm.NONE; + if (shouldCompress) { + serializedSpans = gzipSync(serializedSpans); + } + + this.serializer.setSerializedData(serializedSpans); + const headers = this['_delegate']._transport?._transport?._parameters?.headers(); if (headers) { + if (shouldCompress) { + headers['Content-Encoding'] = 'gzip'; + } else { + delete headers['Content-Encoding']; + } + const signedRequest = await this.authenticator.authenticate(this.endpoint, headers, serializedSpans); // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts index 0a1381e4..cf31fb41 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts @@ -84,9 +84,6 @@ try { diag.info('Setting TraceProvider for instrumentations at the end of initialization'); for (const instrumentation of instrumentations) { instrumentation.setTracerProvider(trace.getTracerProvider()); - if (instrumentation.setLoggerProvider) { - instrumentation.setLoggerProvider(logs.getLoggerProvider()); - } } diag.debug(`Environment variable OTEL_PROPAGATORS is set to '${process.env.OTEL_PROPAGATORS}'`); From 9df5a6766e447d12c4c49b92a2fdbd9f16795d61 Mon Sep 17 00:00:00 2001 From: liustve Date: Fri, 20 Jun 2025 21:38:06 +0000 Subject: [PATCH 04/11] linting fix --- .../src/register.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts index cf31fb41..bdea7eb9 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts @@ -14,7 +14,6 @@ if (process.env.OTEL_TRACES_SAMPLER === 'xray') { } import { diag, DiagConsoleLogger, trace } from '@opentelemetry/api'; -import { logs } from '@opentelemetry/api-logs'; import { getNodeAutoInstrumentations, InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node'; import { Instrumentation } from '@opentelemetry/instrumentation'; import * as opentelemetry from '@opentelemetry/sdk-node'; From 87b6f01f4e150efb6b26e3a3a6ab0bd016a560d4 Mon Sep 17 00:00:00 2001 From: liustve Date: Fri, 20 Jun 2025 21:47:32 +0000 Subject: [PATCH 05/11] add further refactoring --- .../src/exporter/otlp/aws/common/aws-authenticator.ts | 5 +++-- .../src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts | 3 +++ .../src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts index 8bf6984e..8bb64670 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts @@ -22,6 +22,8 @@ if (getNodeVersion() >= 16) { } else { diag.error('SigV4 signing requires at least Node major version 16'); } +// See: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html +const SIG_V4_HEADERS = ['x-amz-date', 'authorization', 'x-amz-content-sha256', 'x-amz-security-token']; export class AwsAuthenticator { private region: string; @@ -80,10 +82,9 @@ export class AwsAuthenticator { // Cleans up Sigv4 from headers to avoid accidentally copying them to the new headers private removeSigV4Headers(headers: Record) { const newHeaders: Record = {}; - const sigV4Headers = ['x-amz-date', 'authorization', 'x-amz-content-sha256', 'x-amz-security-token']; for (const key in headers) { - if (!sigV4Headers.includes(key.toLowerCase())) { + if (!SIG_V4_HEADERS.includes(key.toLowerCase())) { newHeaders[key] = headers[key]; } } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts index be71e33c..602d8e7f 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts @@ -8,6 +8,7 @@ import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; import { AwsAuthenticator } from '../common/aws-authenticator'; import { ExportResult, ExportResultCode } from '@opentelemetry/core'; import { PassthroughSerializer } from '../common/passthrough-serializer'; +import { diag } from '@opentelemetry/api'; /** * This exporter extends the functionality of the OTLPProtoLogExporter to allow spans to be exported @@ -91,6 +92,8 @@ export class OTLPAwsLogExporter extends OTLPProtoLogExporter { const newHeaders: () => Record = () => signedRequest; this['_delegate']._transport._transport._parameters.headers = newHeaders; + } else { + diag.debug('Delegate headers is undefined - unable to authenticate request to CloudWatch Logs OTLP endpoint'); } super.export(items, resultCallback); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts index 15a5407d..34e4c91d 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -8,6 +8,7 @@ import { ExportResult, ExportResultCode } from '@opentelemetry/core'; import { AwsAuthenticator } from '../common/aws-authenticator'; import { PassthroughSerializer } from '../common/passthrough-serializer'; import { gzipSync } from 'zlib'; +import { diag } from '@opentelemetry/api'; /** * This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported @@ -80,6 +81,8 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 const newHeaders: () => Record = () => signedRequest; this['_delegate']._transport._transport._parameters.headers = newHeaders; + } else { + diag.debug('Delegate headers is undefined - unable to authenticate request to XRay OTLP endpoint'); } super.export(items, resultCallback); From 3bf832bbbb65392ed3cedc16a94b810ff9e231fd Mon Sep 17 00:00:00 2001 From: liustve Date: Fri, 20 Jun 2025 22:04:43 +0000 Subject: [PATCH 06/11] add fix for proper header config --- .../src/aws-opentelemetry-configurator.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts index a540ae74..93310ee4 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts @@ -853,21 +853,24 @@ function validateLogsHeaders() { return false; } - let filteredLogHeadersCount = 0; + let hasLogGroup = false; + let hasLogStream = false; for (const pair of logsHeaders.split(',')) { if (pair.includes('=')) { const [key, value] = pair.split('=', 2); - if ((key === AWS_OTLP_LOGS_GROUP_HEADER || key === AWS_OTLP_LOGS_STREAM_HEADER) && value) { - filteredLogHeadersCount += 1; + if (key === AWS_OTLP_LOGS_GROUP_HEADER && value) { + hasLogGroup = true; + } else if (key === AWS_OTLP_LOGS_STREAM_HEADER && value) { + hasLogStream = true; } } } - if (filteredLogHeadersCount !== 2) { + if (!hasLogGroup || !hasLogStream) { diag.warn( - 'Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS ' + - 'to have values for x-aws-log-group and x-aws-log-stream' + `Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS ' + + 'to have values for ${AWS_OTLP_LOGS_GROUP_HEADER} and ${AWS_OTLP_LOGS_STREAM_HEADER}` ); return false; } From 93807d79af4f917e7b7b07d4a18c8dceceae4c57 Mon Sep 17 00:00:00 2001 From: liustve Date: Fri, 20 Jun 2025 23:39:05 +0000 Subject: [PATCH 07/11] delete utils.ts --- .../src/exporter/otlp/aws/common/utils.ts | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/utils.ts diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/utils.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/utils.ts deleted file mode 100644 index 15b4560c..00000000 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; - -export function changeUrlConfig(endpoint: string, config?: OTLPExporterNodeConfigBase): OTLPExporterNodeConfigBase { - if (config) { - return { - ...config, - url: endpoint, - }; - } - - return { url: endpoint }; -} From 694980a7da4acec2abca5e403232bd6409cbc187 Mon Sep 17 00:00:00 2001 From: liustve Date: Sat, 21 Jun 2025 00:07:53 +0000 Subject: [PATCH 08/11] add better debug messages --- .../otlp/aws/common/aws-authenticator.ts | 4 +-- .../otlp/aws/logs/otlp-aws-log-exporter.ts | 15 +++++------ .../otlp/aws/traces/otlp-aws-span-exporter.ts | 25 +++++++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts index 8bb64670..f9d88bbf 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts @@ -70,9 +70,7 @@ export class AwsAuthenticator { return signedRequest.headers; } catch (exception) { - diag.debug( - `Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}` - ); + diag.debug(`Failed to sign/authenticate the given export request with error: ${exception}`); } } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts index 602d8e7f..5cdc6f13 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts @@ -18,7 +18,6 @@ import { diag } from '@opentelemetry/api'; * * This only works with version >=16 Node.js environments. */ - export class OTLPAwsLogExporter extends OTLPProtoLogExporter { private compression: CompressionAlgorithm | undefined; private endpoint: string; @@ -30,7 +29,7 @@ export class OTLPAwsLogExporter extends OTLPProtoLogExporter { const modifiedConfig: OTLPExporterNodeConfigBase = { ...config, url: endpoint, - compression: CompressionAlgorithm.NONE, + compression: CompressionAlgorithm.NONE, // Setting Compression to NONE as compression will be handled here. }; super(modifiedConfig); @@ -51,10 +50,6 @@ export class OTLPAwsLogExporter extends OTLPProtoLogExporter { * To prevent performance degradation from serializing and compressing data twice, we handle serialization and compression * locally in this exporter and pass the pre-processed data to the upstream export functionality. */ - - // Upstream already implements a retry mechanism: - // https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/otlp-exporter-base/src/retrying-transport.ts - public override async export( items: ReadableLogRecord[], resultCallback: (result: ExportResult) => void @@ -70,6 +65,7 @@ export class OTLPAwsLogExporter extends OTLPProtoLogExporter { } const shouldCompress = this.compression && this.compression !== CompressionAlgorithm.NONE; + if (shouldCompress) { serializedLogs = gzipSync(serializedLogs); } @@ -88,10 +84,11 @@ export class OTLPAwsLogExporter extends OTLPProtoLogExporter { delete headers['Content-Encoding']; } - const signedRequest = await this.authenticator.authenticate(this.endpoint, headers, serializedLogs); + const signedRequestHeaders = await this.authenticator.authenticate(this.endpoint, headers, serializedLogs); - const newHeaders: () => Record = () => signedRequest; - this['_delegate']._transport._transport._parameters.headers = newHeaders; + if ('authorization' in signedRequestHeaders) { + this['_delegate']._transport._transport._parameters.headers = () => signedRequestHeaders; + } } else { diag.debug('Delegate headers is undefined - unable to authenticate request to CloudWatch Logs OTLP endpoint'); } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts index 34e4c91d..7416a5d3 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -29,21 +29,24 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { const modifiedConfig: OTLPExporterNodeConfigBase = { ...config, url: endpoint, - compression: CompressionAlgorithm.NONE, + compression: CompressionAlgorithm.NONE, // Setting Compression to NONE as compression will be handled here. }; super(modifiedConfig); this.region = endpoint.split('.')[1]; this.endpoint = endpoint; this.authenticator = new AwsAuthenticator(this.region, 'xray'); + + // This is used in order to prevent serializing and compressing the data twice. Once for signing Sigv4 and + // once when we pass the data to super.export() which will serialize and compress the data again. this.serializer = new PassthroughSerializer(ProtobufTraceSerializer.deserializeResponse); this['_delegate']._serializer = this.serializer; } /** - * 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. + * 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. * To prevent performance degradation from serializing and compressing data twice, we handle serialization and compression * locally in this exporter and pass the pre-processed data to the upstream export functionality. */ @@ -58,15 +61,17 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { return; } - // Pass pre-processed data to passthrough serializer. When super.export() is called, the Passthrough Serializer will - // use the pre-processed data instead of serializing and compressing the data again. const shouldCompress = this.compression && this.compression !== CompressionAlgorithm.NONE; + if (shouldCompress) { serializedSpans = gzipSync(serializedSpans); } + // Pass pre-processed data to passthrough serializer. When super.export() is called, the Passthrough Serializer will + // use the pre-processed data instead of serializing and compressing the data again. this.serializer.setSerializedData(serializedSpans); + // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 const headers = this['_delegate']._transport?._transport?._parameters?.headers(); if (headers) { @@ -76,11 +81,11 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { delete headers['Content-Encoding']; } - const signedRequest = await this.authenticator.authenticate(this.endpoint, headers, serializedSpans); + const signedRequestHeaders = await this.authenticator.authenticate(this.endpoint, headers, serializedSpans); - // See type: https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.57.1/experimental/packages/otlp-exporter-base/src/transport/http-transport-types.ts#L31 - const newHeaders: () => Record = () => signedRequest; - this['_delegate']._transport._transport._parameters.headers = newHeaders; + if ('authorization' in signedRequestHeaders) { + this['_delegate']._transport._transport._parameters.headers = () => signedRequestHeaders; + } } else { diag.debug('Delegate headers is undefined - unable to authenticate request to XRay OTLP endpoint'); } From cbcb0e9da87f2e50e7e7e4643b527ee4f5ad2b18 Mon Sep 17 00:00:00 2001 From: liustve Date: Sat, 21 Jun 2025 00:23:57 +0000 Subject: [PATCH 09/11] linting fix --- .../src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts | 2 +- .../src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts index 5cdc6f13..88ef83f0 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts @@ -65,7 +65,7 @@ export class OTLPAwsLogExporter extends OTLPProtoLogExporter { } const shouldCompress = this.compression && this.compression !== CompressionAlgorithm.NONE; - + if (shouldCompress) { serializedLogs = gzipSync(serializedLogs); } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts index 7416a5d3..ac67a483 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -44,8 +44,8 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { } /** - * 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 + * 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. * To prevent performance degradation from serializing and compressing data twice, we handle serialization and compression * locally in this exporter and pass the pre-processed data to the upstream export functionality. @@ -62,7 +62,7 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { } const shouldCompress = this.compression && this.compression !== CompressionAlgorithm.NONE; - + if (shouldCompress) { serializedSpans = gzipSync(serializedSpans); } From 3827f98ec12bc14642cfef454aee3a265f01d274 Mon Sep 17 00:00:00 2001 From: liustve Date: Sat, 21 Jun 2025 00:25:05 +0000 Subject: [PATCH 10/11] add bunyan dependency --- sample-applications/simple-express-server/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/sample-applications/simple-express-server/package.json b/sample-applications/simple-express-server/package.json index 3ecd898a..96eca1f6 100644 --- a/sample-applications/simple-express-server/package.json +++ b/sample-applications/simple-express-server/package.json @@ -18,6 +18,7 @@ "@types/express": "^4.17.21", "@types/node": "^20.14.6", "body-parser": "^1.20.2", + "bunyan": "^1.8.15", "express": "^4.19.2", "mysql": "^2.18.1", "ts-node": "^10.9.2", From 124f98e5cf53b7b61dc28aeace98395548f43b09 Mon Sep 17 00:00:00 2001 From: liustve Date: Sat, 21 Jun 2025 00:50:28 +0000 Subject: [PATCH 11/11] fix sigv4 tests --- .../src/aws-opentelemetry-configurator.ts | 4 ++-- .../otlp/aws/common/aws-authenticator.ts | 24 ++++++++----------- .../otlp/aws/logs/otlp-aws-log-exporter.ts | 6 ++--- .../otlp/aws/traces/otlp-aws-span-exporter.ts | 6 ++--- .../otlp/aws/common/aws-authenticator.test.ts | 13 ++++------ .../aws/logs/otlp-aws-log-exporter.test.ts | 3 ++- .../aws/traces/otlp-aws-span-exporter.test.ts | 2 +- 7 files changed, 24 insertions(+), 34 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts index 93310ee4..004c222d 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts @@ -608,7 +608,7 @@ export class AwsSpanProcessorProvider { case 'http/protobuf': if (otlpExporterTracesEndpoint && isAwsOtlpEndpoint(otlpExporterTracesEndpoint, 'xray')) { diag.debug('Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter'); - return new OTLPAwsSpanExporter(otlpExporterTracesEndpoint); + return new OTLPAwsSpanExporter(otlpExporterTracesEndpoint, { compression: CompressionAlgorithm.GZIP }); } return new OTLPProtoTraceExporter(); case 'udp': @@ -618,7 +618,7 @@ export class AwsSpanProcessorProvider { diag.warn(`Unsupported OTLP traces protocol: ${protocol}. Using http/protobuf.`); if (otlpExporterTracesEndpoint && isAwsOtlpEndpoint(otlpExporterTracesEndpoint, 'xray')) { diag.debug('Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter'); - return new OTLPAwsSpanExporter(otlpExporterTracesEndpoint); + return new OTLPAwsSpanExporter(otlpExporterTracesEndpoint, { compression: CompressionAlgorithm.GZIP }); } return new OTLPProtoTraceExporter(); } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts index f9d88bbf..2bdc0be8 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts @@ -26,35 +26,30 @@ if (getNodeVersion() >= 16) { const SIG_V4_HEADERS = ['x-amz-date', 'authorization', 'x-amz-content-sha256', 'x-amz-security-token']; export class AwsAuthenticator { + private endpoint: URL; private region: string; private service: string; - constructor(region: string, service: string) { - this.region = region; + constructor(endpoint: string, service: string) { + this.endpoint = new URL(endpoint); + this.region = endpoint.split('.')[1]; this.service = service; } - public async authenticate(endpoint: string, headers: Record, serializedData: Uint8Array | undefined) { + public async authenticate(headers: Record, serializedData: Uint8Array | undefined) { // Only do SigV4 Signing if the required dependencies are installed. - if (dependenciesLoaded) { - const url = new URL(endpoint); - - if (serializedData === undefined) { - diag.error('Given serialized data is undefined. Not authenticating.'); - return headers; - } - + if (dependenciesLoaded && serializedData) { const cleanedHeaders = this.removeSigV4Headers(headers); const request = new HttpRequest({ method: 'POST', protocol: 'https', - hostname: url.hostname, - path: url.pathname, + hostname: this.endpoint.hostname, + path: this.endpoint.pathname, body: serializedData, headers: { ...cleanedHeaders, - host: url.hostname, + host: this.endpoint.hostname, }, }); @@ -74,6 +69,7 @@ export class AwsAuthenticator { } } + diag.debug('Given serialized data is undefined. Not authenticating.'); return headers; } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts index 88ef83f0..b7242ebb 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts @@ -21,7 +21,6 @@ import { diag } from '@opentelemetry/api'; export class OTLPAwsLogExporter extends OTLPProtoLogExporter { private compression: CompressionAlgorithm | undefined; private endpoint: string; - private region: string; private serializer: PassthroughSerializer; private authenticator: AwsAuthenticator; @@ -34,9 +33,8 @@ export class OTLPAwsLogExporter extends OTLPProtoLogExporter { super(modifiedConfig); this.compression = config?.compression; - this.region = endpoint.split('.')[1]; this.endpoint = endpoint; - this.authenticator = new AwsAuthenticator(this.region, 'logs'); + this.authenticator = new AwsAuthenticator(this.endpoint, 'logs'); // This is used in order to prevent serializing and compressing the data twice. Once for signing Sigv4 and // once when we pass the data to super.export() which will serialize and compress the data again. @@ -84,7 +82,7 @@ export class OTLPAwsLogExporter extends OTLPProtoLogExporter { delete headers['Content-Encoding']; } - const signedRequestHeaders = await this.authenticator.authenticate(this.endpoint, headers, serializedLogs); + const signedRequestHeaders = await this.authenticator.authenticate(headers, serializedLogs); if ('authorization' in signedRequestHeaders) { this['_delegate']._transport._transport._parameters.headers = () => signedRequestHeaders; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts index ac67a483..28a2e7c1 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -21,7 +21,6 @@ import { diag } from '@opentelemetry/api'; export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { private compression: CompressionAlgorithm | undefined; private endpoint: string; - private region: string; private serializer: PassthroughSerializer; private authenticator: AwsAuthenticator; @@ -33,9 +32,8 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { }; super(modifiedConfig); - this.region = endpoint.split('.')[1]; this.endpoint = endpoint; - this.authenticator = new AwsAuthenticator(this.region, 'xray'); + this.authenticator = new AwsAuthenticator(this.endpoint, 'xray'); // This is used in order to prevent serializing and compressing the data twice. Once for signing Sigv4 and // once when we pass the data to super.export() which will serialize and compress the data again. @@ -81,7 +79,7 @@ export class OTLPAwsSpanExporter extends OTLPProtoTraceExporter { delete headers['Content-Encoding']; } - const signedRequestHeaders = await this.authenticator.authenticate(this.endpoint, headers, serializedSpans); + const signedRequestHeaders = await this.authenticator.authenticate(headers, serializedSpans); if ('authorization' in signedRequestHeaders) { this['_delegate']._transport._transport._parameters.headers = () => signedRequestHeaders; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts index 7096b498..38ba76b2 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts @@ -54,8 +54,7 @@ describe('AwsAuthenticator', () => { const { AwsAuthenticator: MockThrowableModuleAuthenticator } = require(AWS_AUTH_PATH); - const result = await new MockThrowableModuleAuthenticator('us-east-1', 'xray').authenticate( - AWS_OTLP_TRACES_ENDPOINT, + const result = await new MockThrowableModuleAuthenticator(AWS_OTLP_TRACES_ENDPOINT, 'xray').authenticate( {}, new Uint8Array() ); @@ -68,8 +67,8 @@ describe('AwsAuthenticator', () => { }); it('should not inject SigV4 Headers if serialized data is undefined', async () => { - const authenticator = new AwsAuthenticator('us-east-1', 'xray'); - const result = await authenticator.authenticate(AWS_OTLP_TRACES_ENDPOINT, {}, undefined); + const authenticator = new AwsAuthenticator(AWS_OTLP_TRACES_ENDPOINT, 'xray'); + const result = await authenticator.authenticate({}, undefined); expect(result).not.toHaveProperty(AUTHORIZATION_HEADER); expect(result).not.toHaveProperty(X_AMZ_DATE_HEADER); @@ -99,8 +98,7 @@ describe('AwsAuthenticator', () => { }, }).AwsAuthenticator; - const result = await new AwsAuthenticatorWithMock('us-east-1', 'xray').authenticate( - AWS_OTLP_TRACES_ENDPOINT, + const result = await new AwsAuthenticatorWithMock(AWS_OTLP_TRACES_ENDPOINT, 'xray').authenticate( { test: 'test' }, new Uint8Array() ); @@ -149,8 +147,7 @@ describe('AwsAuthenticator', () => { }, }).AwsAuthenticator; - const result = await new AwsAuthenticatorWithMock('us-east-1', 'xray').authenticate( - AWS_OTLP_TRACES_ENDPOINT, + const result = await new AwsAuthenticatorWithMock(AWS_OTLP_TRACES_ENDPOINT, 'xray').authenticate( notExpected, new Uint8Array() ); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts index 3e19eeac..2772193a 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts @@ -36,7 +36,7 @@ describe('OTLPAwsLogExporter', () => { '../common/aws-authenticator': { AwsAuthenticator: class MockAwsAuthenticator { constructor() {} - async authenticate(endpoint: string, headers: Record) { + async authenticate(headers: Record, serializedData: Uint8Array | undefined) { return { ...headers, [AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER, @@ -61,6 +61,7 @@ describe('OTLPAwsLogExporter', () => { .then(() => { scope.on('request', (req, interceptor, body) => { const headers = req.headers; + console.log('SDJASDJAJSDAJSDJJDAJSDJAS' + JSON.stringify(req.headers)); expect(headers).toHaveProperty(AUTHORIZATION_HEADER.toLowerCase()); expect(headers).toHaveProperty(X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()); expect(headers).toHaveProperty(X_AMZ_DATE_HEADER.toLowerCase()); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts index 4e968dbc..90089549 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts @@ -35,7 +35,7 @@ describe('OTLPAwsSpanExporter', () => { '../common/aws-authenticator': { AwsAuthenticator: class MockAwsAuthenticator { constructor() {} - async authenticate(endpoint: string, headers: Record) { + async authenticate(headers: Record, serializedData: Uint8Array | undefined) { return { ...headers, [AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER,