Skip to content

Commit 28836c8

Browse files
committed
add sigv4 for lgos
1 parent 6c49bd5 commit 28836c8

File tree

7 files changed

+167
-211
lines changed

7 files changed

+167
-211
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"@opentelemetry/sdk-metrics": "1.30.1",
117117
"@opentelemetry/sdk-node": "0.57.1",
118118
"@opentelemetry/sdk-trace-base": "1.30.1",
119+
"@opentelemetry/sdk-logs": "0.57.1",
119120
"@opentelemetry/semantic-conventions": "1.28.0"
120121
},
121122
"files": [
Lines changed: 84 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,92 @@
1-
// import { diag } from '@opentelemetry/api';
2-
// import { getNodeVersion } from '../../../../utils';
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { diag } from '@opentelemetry/api';
4+
import { getNodeVersion } from '../../../../utils';
5+
let SignatureV4: any;
6+
let HttpRequest: any;
7+
let defaultProvider: any;
8+
let Sha256: any;
39

4-
// let SignatureV4: any;
5-
// let HttpRequest: any;
6-
// let defaultProvider: any;
7-
// let Sha256: any;
10+
let dependenciesLoaded = false;
811

9-
// const nodeVersionSupported = getNodeVersion() >= 16;
12+
if (getNodeVersion() >= 16) {
13+
try {
14+
defaultProvider = require('@aws-sdk/credential-provider-node').defaultProvider;
15+
Sha256 = require('@aws-crypto/sha256-js').Sha256;
16+
SignatureV4 = require('@smithy/signature-v4').SignatureV4;
17+
HttpRequest = require('@smithy/protocol-http').HttpRequest;
18+
dependenciesLoaded = true;
19+
} catch (error) {
20+
diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`);
21+
}
22+
} else {
23+
diag.error('SigV4 signing requires at least Node major version 16');
24+
}
1025

11-
// if (nodeVersionSupported) {
12-
// try {
13-
// const { defaultProvider: awsDefaultProvider } = require('@aws-sdk/credential-provider-node');
14-
// const { Sha256: awsSha256 } = require('@aws-crypto/sha256-js');
15-
// const { SignatureV4: awsSignatureV4 } = require('@smithy/signature-v4');
16-
// const { HttpRequest: awsHttpRequest } = require('@smithy/protocol-http');
17-
18-
// // Assign to module-level variables
19-
// defaultProvider = awsDefaultProvider;
20-
// Sha256 = awsSha256;
21-
// SignatureV4 = awsSignatureV4;
22-
// HttpRequest = awsHttpRequest;
23-
// } catch (error) {
24-
// diag.error(`Failed to load required AWS dependency for SigV4 Signing: ${error}`);
25-
// }
26-
// }
26+
export class AwsAuthenticator {
27+
private region: string;
28+
private service: string;
2729

28-
// export class AwsAuthenticator {
30+
constructor(region: string, service: string) {
31+
this.region = region;
32+
this.service = service;
33+
}
2934

30-
// private static readonly SERVICE_NAME: string = 'xray';
31-
// private endpoint: string;
32-
// private region: string;
33-
// private service: string;
35+
public async authenticate(endpoint: string, headers: Record<string, string>, serializedData: Uint8Array | undefined) {
36+
// Only do SigV4 Signing if the required dependencies are installed.
37+
if (dependenciesLoaded) {
38+
const url = new URL(endpoint);
3439

35-
// // Holds the dependencies needed to sign the SigV4 headers
36-
// private defaultProvider: any;
37-
// private sha256: any;
38-
// private signatureV4: any;
39-
// private httpRequest: any;
40+
if (serializedData == undefined) {
41+
diag.error('Given serialized data is undefined. Not authenticating.');
42+
return;
43+
}
4044

41-
// constructor(endpoint: string, region: string, service: string) {
42-
// this.endpoint = endpoint;
43-
// this.region = region;
44-
// this.service = service;
45-
46-
// }
45+
const cleanedHeaders = this.removeSigV4Headers(headers);
4746

48-
// // if (oldHeaders) {
49-
// // const request = new this.httpRequest({
50-
// // method: 'POST',
51-
// // protocol: 'https',
52-
// // hostname: url.hostname,
53-
// // path: url.pathname,
54-
// // body: serializedSpans,
55-
// // headers: {
56-
// // ...this.removeSigV4Headers(oldHeaders),
57-
// // host: url.hostname,
58-
// // },
59-
// // });
60-
61-
// // try {
62-
// // const signer = new this.signatureV4({
63-
// // credentials: this.defaultProvider(),
64-
// // region: this.region,
65-
// // service: OTLPAwsSpanExporter.SERVICE_NAME,
66-
// // sha256: this.sha256,
67-
// // });
68-
69-
// // const signedRequest = await signer.sign(request);
70-
71-
// // // 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
72-
// // const newHeaders: () => Record<string, string> = () => signedRequest.headers;
73-
// // this['_delegate']._transport._transport._parameters.headers = newHeaders;
74-
// // } catch (exception) {
75-
// // diag.debug(
76-
// // `Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}`
77-
// // );
78-
// // }
79-
// // }
80-
// }
47+
const request = new HttpRequest.httpRequest({
48+
method: 'POST',
49+
protocol: 'https',
50+
hostname: url.hostname,
51+
path: url.pathname,
52+
body: serializedData,
53+
headers: {
54+
...cleanedHeaders,
55+
host: url.hostname,
56+
},
57+
});
58+
59+
try {
60+
const signer = new SignatureV4({
61+
credentials: defaultProvider(),
62+
region: this.region,
63+
service: this.service,
64+
sha256: Sha256,
65+
});
66+
67+
const signedRequest = await signer.sign(request);
68+
69+
return signedRequest.headers;
70+
} catch (exception) {
71+
diag.debug(
72+
`Failed to sign/authenticate the given exported Span request to OTLP XRay endpoint with error: ${exception}`
73+
);
74+
}
75+
}
76+
77+
return headers;
78+
}
79+
80+
// Cleans up Sigv4 headers from headers to avoid accidentally copying them to the new headers
81+
private removeSigV4Headers(headers: Record<string, string>) {
82+
const newHeaders: Record<string, string> = {};
83+
const sigV4Headers = ['x-amz-date', 'authorization', 'x-amz-content-sha256', 'x-amz-security-token'];
84+
85+
for (const key in headers) {
86+
if (!sigV4Headers.includes(key.toLowerCase())) {
87+
newHeaders[key] = headers[key];
88+
}
89+
}
90+
return newHeaders;
91+
}
92+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base';
4+
5+
export function changeUrlConfig(endpoint: string, config?: OTLPExporterNodeConfigBase): OTLPExporterNodeConfigBase {
6+
if (config) {
7+
return {
8+
...config,
9+
url: endpoint,
10+
};
11+
}
12+
13+
return { url: endpoint };
14+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
4+
import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base';
5+
import { ProtobufLogsSerializer } from '@opentelemetry/otlp-transformer';
6+
import { ReadableLogRecord } from '@opentelemetry/sdk-logs';
7+
import { AwsAuthenticator } from '../common/aws-authenticator';
8+
import { changeUrlConfig } from '../common/utils';
9+
import { ExportResult } from '@opentelemetry/core';
10+
11+
/**
12+
* This exporter extends the functionality of the OTLPProtoLogExporter to allow spans to be exported
13+
* to the CloudWatch Logs OTLP endpoint https://logs.[AWSRegion].amazonaws.com/v1/logs. Utilizes the aws-sdk
14+
* library to sign and directly inject SigV4 Authentication to the exported request's headers. <a
15+
* href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html">...</a>
16+
*
17+
* This only works with version >=16 Node.js environments.
18+
*/
19+
export class OTLPAwsLogExporter extends OTLPProtoLogExporter {
20+
private endpoint: string;
21+
private region: string;
22+
private authenticator;
23+
24+
constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) {
25+
super(changeUrlConfig(endpoint, config));
26+
this.region = endpoint.split('.')[1];
27+
this.endpoint = endpoint;
28+
this.authenticator = new AwsAuthenticator(this.region, 'xray');
29+
}
30+
31+
/**
32+
* Overrides the upstream implementation of export. All behaviors are the same except if the
33+
* endpoint is the CloudWatch Logs OTLP endpoint, we will sign the request with SigV4 in headers before
34+
* sending it to the endpoint. Otherwise, we will skip signing.
35+
*/
36+
public override async export(
37+
items: ReadableLogRecord[],
38+
resultCallback: (result: ExportResult) => void
39+
): Promise<void> {
40+
const serializedLogs: Uint8Array | undefined = ProtobufLogsSerializer.serializeRequest(items);
41+
const headers = this['_delegate']._transport?._transport?._parameters?.headers();
42+
43+
if (headers) {
44+
const signedRequest = await this.authenticator.authenticate(this.endpoint, headers, serializedLogs);
45+
46+
// 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
47+
const newHeaders: () => Record<string, string> = () => signedRequest;
48+
this['_delegate']._transport._transport._parameters.headers = newHeaders;
49+
}
50+
51+
super.export(items, resultCallback);
52+
}
53+
}

0 commit comments

Comments
 (0)