Skip to content

Commit cda18c3

Browse files
authored
Merge branch 'main' into aws-service-type-resource
2 parents 6564573 + ad3e0d3 commit cda18c3

File tree

15 files changed

+1432
-454
lines changed

15 files changed

+1432
-454
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
],
4242
"exclude": [
4343
"src/third-party/**/*.ts",
44-
"src/otlp-aws-span-exporter.ts"
44+
"src/exporter/otlp/aws/common/aws-authenticator.ts"
4545
]
4646
},
4747
"bugs": {
@@ -108,6 +108,9 @@
108108
"@opentelemetry/exporter-metrics-otlp-grpc": "0.57.1",
109109
"@opentelemetry/exporter-metrics-otlp-http": "0.57.1",
110110
"@opentelemetry/exporter-trace-otlp-proto": "0.57.1",
111+
"@opentelemetry/exporter-logs-otlp-grpc": "0.57.1",
112+
"@opentelemetry/exporter-logs-otlp-http": "0.57.1",
113+
"@opentelemetry/exporter-logs-otlp-proto": "0.57.1",
111114
"@opentelemetry/exporter-zipkin": "1.30.1",
112115
"@opentelemetry/id-generator-aws-xray": "1.2.3",
113116
"@opentelemetry/instrumentation": "0.57.1",
@@ -119,6 +122,7 @@
119122
"@opentelemetry/sdk-metrics": "1.30.1",
120123
"@opentelemetry/sdk-node": "0.57.1",
121124
"@opentelemetry/sdk-trace-base": "1.30.1",
125+
"@opentelemetry/sdk-logs": "0.57.1",
122126
"@opentelemetry/semantic-conventions": "1.28.0"
123127
},
124128
"files": [

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

Lines changed: 229 additions & 10 deletions
Large diffs are not rendered by default.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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;
9+
10+
let dependenciesLoaded = false;
11+
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+
}
25+
26+
// See: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
27+
export const AUTHORIZATION_HEADER = 'authorization';
28+
export const X_AMZ_DATE_HEADER = 'x-amz-date';
29+
export const X_AMZ_SECURITY_TOKEN_HEADER = 'x-amz-security-token';
30+
export const X_AMZ_CONTENT_SHA256_HEADER = 'x-amz-content-sha256';
31+
32+
export class AwsAuthenticator {
33+
private endpoint: URL;
34+
private region: string;
35+
private service: string;
36+
37+
constructor(endpoint: string, service: string) {
38+
// The endpoint is pre-validated by the config with isAwsOtlpEndpoint, so then endpoint is guaranteed to be well formatted and
39+
// new URL() will not throw
40+
this.endpoint = new URL(endpoint);
41+
this.region = endpoint.split('.')[1];
42+
this.service = service;
43+
}
44+
45+
public async authenticate(headers: Record<string, string>, serializedData: Uint8Array | undefined) {
46+
// Only do SigV4 Signing if the required dependencies are installed.
47+
if (dependenciesLoaded && serializedData) {
48+
const cleanedHeaders = this.removeSigV4Headers(headers);
49+
50+
const request = new HttpRequest({
51+
method: 'POST',
52+
protocol: 'https',
53+
hostname: this.endpoint.hostname,
54+
path: this.endpoint.pathname,
55+
body: serializedData,
56+
headers: {
57+
...cleanedHeaders,
58+
host: this.endpoint.hostname,
59+
},
60+
});
61+
62+
try {
63+
const signer = new SignatureV4({
64+
credentials: defaultProvider(),
65+
region: this.region,
66+
service: this.service,
67+
sha256: Sha256,
68+
});
69+
70+
const signedRequest = await signer.sign(request);
71+
72+
return signedRequest.headers;
73+
} catch (exception) {
74+
diag.debug(`Failed to sign/authenticate the given export request with error: ${exception}`);
75+
return undefined;
76+
}
77+
}
78+
79+
diag.debug('No serialized data provided. Not authenticating.');
80+
return undefined;
81+
}
82+
83+
// Cleans up Sigv4 from headers to avoid accidentally copying them to the new headers
84+
private removeSigV4Headers(headers: Record<string, string>) {
85+
const newHeaders: Record<string, string> = {};
86+
const sigv4Headers = [
87+
AUTHORIZATION_HEADER,
88+
X_AMZ_CONTENT_SHA256_HEADER,
89+
X_AMZ_DATE_HEADER,
90+
X_AMZ_CONTENT_SHA256_HEADER,
91+
];
92+
93+
for (const key in headers) {
94+
if (!sigv4Headers.includes(key.toLowerCase())) {
95+
newHeaders[key] = headers[key];
96+
}
97+
}
98+
return newHeaders;
99+
}
100+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { CompressionAlgorithm, OTLPExporterBase } from '@opentelemetry/otlp-exporter-base';
4+
import { gzipSync } from 'zlib';
5+
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
6+
import { AwsAuthenticator } from './aws-authenticator';
7+
import { ISerializer } from '@opentelemetry/otlp-transformer';
8+
9+
/**
10+
* Base class for AWS OTLP exporters
11+
*/
12+
export abstract class OTLPAwsBaseExporter<Payload, Response> extends OTLPExporterBase<Payload> {
13+
protected parentExporter: OTLPExporterBase<Payload>;
14+
private readonly compression?: CompressionAlgorithm;
15+
private endpoint: string;
16+
private serializer: PassthroughSerializer<Response>;
17+
private authenticator: AwsAuthenticator;
18+
private parentSerializer: ISerializer<Payload, Response>;
19+
20+
constructor(
21+
endpoint: string,
22+
service: string,
23+
parentExporter: OTLPExporterBase<Payload>,
24+
parentSerializer: ISerializer<Payload, Response>,
25+
compression?: CompressionAlgorithm
26+
) {
27+
super(parentExporter['_delegate']);
28+
this.compression = compression;
29+
this.endpoint = endpoint;
30+
this.authenticator = new AwsAuthenticator(this.endpoint, service);
31+
this.parentExporter = parentExporter;
32+
this.parentSerializer = parentSerializer;
33+
34+
// To prevent performance degradation from serializing and compressing data twice, we handle serialization and compression
35+
// locally in this exporter and pass the pre-processed data to the upstream export.
36+
// This is used in order to prevent serializing and compressing the data again when calling parentExporter.export().
37+
// To see why this works:
38+
// https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/otlp-exporter-base/src/otlp-export-delegate.ts#L69
39+
this.serializer = new PassthroughSerializer<Response>(this.parentSerializer.deserializeResponse);
40+
this.parentExporter['_delegate']._serializer = this.serializer;
41+
}
42+
43+
/**
44+
* Overrides the upstream implementation of export.
45+
* All behaviors are the same except if the endpoint is an AWS OTLP endpoint, we will sign the request with SigV4
46+
* in headers before sending it to the endpoint.
47+
* @param items - Array of signal data to export
48+
* @param resultCallback - Callback function to handle export result
49+
*/
50+
override async export(items: Payload, resultCallback: (result: ExportResult) => void): Promise<void> {
51+
const headers = this.parentExporter['_delegate']._transport?._transport?._parameters?.headers();
52+
53+
if (!headers) {
54+
resultCallback({
55+
code: ExportResultCode.FAILED,
56+
error: new Error(`Request headers are unset - unable to export to ${this.endpoint}`),
57+
});
58+
return;
59+
}
60+
61+
let serializedData: Uint8Array | undefined = this.parentSerializer.serializeRequest(items);
62+
63+
if (!serializedData) {
64+
resultCallback({
65+
code: ExportResultCode.FAILED,
66+
error: new Error('Nothing to send'),
67+
});
68+
return;
69+
}
70+
71+
delete headers['Content-Encoding'];
72+
const shouldCompress = this.compression && this.compression !== CompressionAlgorithm.NONE;
73+
74+
if (shouldCompress) {
75+
try {
76+
serializedData = gzipSync(serializedData);
77+
headers['Content-Encoding'] = 'gzip';
78+
} catch (exception) {
79+
resultCallback({
80+
code: ExportResultCode.FAILED,
81+
error: new Error(`Failed to compress: ${exception}`),
82+
});
83+
return;
84+
}
85+
}
86+
87+
this.serializer.setSerializedData(serializedData);
88+
const signedHeaders = await this.authenticator.authenticate(headers, serializedData);
89+
90+
if (!signedHeaders) {
91+
resultCallback({
92+
code: ExportResultCode.FAILED,
93+
error: new Error('Sigv4 Signing Failed. Not exporting'),
94+
});
95+
return;
96+
}
97+
98+
this.parentExporter['_delegate']._transport._transport._parameters.headers = () => signedHeaders;
99+
this.parentExporter.export(items, resultCallback);
100+
}
101+
102+
override shutdown(): Promise<void> {
103+
return this.parentExporter.shutdown();
104+
}
105+
106+
override forceFlush(): Promise<void> {
107+
return this.parentExporter.forceFlush();
108+
}
109+
}
110+
111+
/**
112+
* A serializer that bypasses request serialization by returning pre-serialized data.
113+
* @template Response The type of the deserialized response
114+
*/
115+
class PassthroughSerializer<Response> implements ISerializer<Uint8Array, Response> {
116+
private serializedData: Uint8Array = new Uint8Array();
117+
private deserializer: (data: Uint8Array) => Response;
118+
119+
/**
120+
* Creates a new PassthroughSerializer instance.
121+
* @param deserializer Function to deserialize response data
122+
*/
123+
constructor(deserializer: (data: Uint8Array) => Response) {
124+
this.deserializer = deserializer;
125+
}
126+
127+
/**
128+
* Sets the pre-serialized data to be returned when serializeRequest is called.
129+
* @param data The serialized data to use
130+
*/
131+
setSerializedData(data: Uint8Array): void {
132+
this.serializedData = data;
133+
}
134+
135+
/**
136+
* Returns the pre-serialized data, ignoring the request parameter.
137+
* @param request Ignored parameter.
138+
* @returns The pre-serialized data
139+
*/
140+
serializeRequest(request: Uint8Array): Uint8Array {
141+
return this.serializedData;
142+
}
143+
144+
/**
145+
* Deserializes response data using the provided deserializer function.
146+
* @param data The response data to deserialize
147+
* @returns The deserialized response
148+
*/
149+
deserializeResponse(data: Uint8Array): Response {
150+
return this.deserializer(data);
151+
}
152+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 { CompressionAlgorithm, OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base';
5+
import { IExportLogsServiceResponse, ProtobufLogsSerializer } from '@opentelemetry/otlp-transformer';
6+
import { LogRecordExporter, ReadableLogRecord } from '@opentelemetry/sdk-logs';
7+
import { OTLPAwsBaseExporter } from '../common/otlp-aws-base-exporter';
8+
9+
/**
10+
* This exporter extends the functionality of the OTLPProtoLogExporter to allow logs to be exported
11+
* to the CloudWatch Logs OTLP endpoint https://logs.[AWSRegion].amazonaws.com/v1/logs. Utilizes the aws-sdk
12+
* library to sign and directly inject SigV4 Authentication to the exported request's headers. <a
13+
* href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html">...</a>
14+
*
15+
* This only works with version >=16 Node.js environments.
16+
* @param endpoint - The AWS CloudWatch Logs OTLP endpoint URL
17+
* @param config - Optional OTLP exporter configuration
18+
*/
19+
export class OTLPAwsLogExporter
20+
extends OTLPAwsBaseExporter<ReadableLogRecord[], IExportLogsServiceResponse>
21+
implements LogRecordExporter
22+
{
23+
constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) {
24+
const modifiedConfig: OTLPExporterNodeConfigBase = {
25+
...config,
26+
url: endpoint,
27+
compression: CompressionAlgorithm.NONE,
28+
};
29+
30+
super(endpoint, 'logs', new OTLPProtoLogExporter(modifiedConfig), ProtobufLogsSerializer, config?.compression);
31+
}
32+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
4+
import { CompressionAlgorithm, OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base';
5+
import { IExportTraceServiceResponse, ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer';
6+
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
7+
import { OTLPAwsBaseExporter } from '../common/otlp-aws-base-exporter';
8+
import { LLOHandler } from '../../../../llo-handler';
9+
import { LoggerProvider as APILoggerProvider, logs } from '@opentelemetry/api-logs';
10+
import { ExportResult } from '@opentelemetry/core';
11+
import { isAgentObservabilityEnabled } from '../../../../utils';
12+
import { diag } from '@opentelemetry/api';
13+
import { LoggerProvider } from '@opentelemetry/sdk-logs';
14+
15+
/**
16+
* This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported
17+
* to the XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. Utilizes the aws-sdk
18+
* library to sign and directly inject SigV4 Authentication to the exported request's headers. <a
19+
* href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html">...</a>
20+
*
21+
* This only works with version >=16 Node.js environments.
22+
*
23+
* @param endpoint - The AWS X-Ray OTLP endpoint URL
24+
* @param config - Optional OTLP exporter configuration
25+
*/
26+
export class OTLPAwsSpanExporter
27+
extends OTLPAwsBaseExporter<ReadableSpan[], IExportTraceServiceResponse>
28+
implements SpanExporter
29+
{
30+
private loggerProvider: APILoggerProvider | undefined;
31+
private lloHandler: LLOHandler | undefined;
32+
33+
constructor(endpoint: string, config?: OTLPExporterNodeConfigBase, loggerProvider?: APILoggerProvider) {
34+
const modifiedConfig: OTLPExporterNodeConfigBase = {
35+
...config,
36+
url: endpoint,
37+
compression: CompressionAlgorithm.NONE,
38+
};
39+
40+
super(endpoint, 'xray', new OTLPProtoTraceExporter(modifiedConfig), ProtobufTraceSerializer, config?.compression);
41+
42+
this.lloHandler = undefined;
43+
this.loggerProvider = loggerProvider;
44+
}
45+
46+
// Lazily initialize LLO handler when needed to avoid initialization order issues
47+
private ensureLloHandler(): boolean {
48+
if (!this.lloHandler && isAgentObservabilityEnabled()) {
49+
// If loggerProvider wasn't provided, try to get the current one
50+
if (!this.loggerProvider) {
51+
try {
52+
this.loggerProvider = logs.getLoggerProvider();
53+
} catch (e: unknown) {
54+
diag.debug('Failed to get logger provider', e);
55+
return false;
56+
}
57+
}
58+
59+
if (this.loggerProvider instanceof LoggerProvider) {
60+
this.lloHandler = new LLOHandler(this.loggerProvider);
61+
return true;
62+
}
63+
}
64+
65+
return !!this.lloHandler;
66+
}
67+
68+
override async export(items: ReadableSpan[], resultCallback: (result: ExportResult) => void): Promise<void> {
69+
let itemsToSerialize: ReadableSpan[] = items;
70+
if (isAgentObservabilityEnabled() && this.ensureLloHandler() && this.lloHandler) {
71+
// items to serialize are now the lloProcessedSpans
72+
itemsToSerialize = this.lloHandler.processSpans(items);
73+
}
74+
75+
return super.export(itemsToSerialize, resultCallback);
76+
}
77+
}

0 commit comments

Comments
 (0)