diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/package.json b/aws-distro-opentelemetry-node-autoinstrumentation/package.json index 28d63edf..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,9 @@ "@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", "@opentelemetry/instrumentation": "0.57.1", @@ -115,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 dac8e45d..004c222d 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, @@ -14,6 +15,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,25 +54,39 @@ 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'; 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` 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'; @@ -96,6 +114,7 @@ export class AwsOpentelemetryConfigurator { private idGenerator: IdGenerator; private sampler: Sampler; private spanProcessors: SpanProcessor[]; + private logRecordProcessors: LogRecordProcessor[]; private propagator: TextMapPropagator; /** @@ -179,6 +198,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); } @@ -207,6 +227,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, }; @@ -385,6 +406,146 @@ 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, { compression: CompressionAlgorithm.GZIP }) + ); + } 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, { compression: CompressionAlgorithm.GZIP }) + ); + } 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 @@ -428,7 +589,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(); @@ -445,9 +606,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, { compression: CompressionAlgorithm.GZIP }); } return new OTLPProtoTraceExporter(); case 'udp': @@ -455,9 +616,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, { compression: CompressionAlgorithm.GZIP }); } return new OTLPProtoTraceExporter(); } @@ -667,8 +828,54 @@ 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 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 && value) { + hasLogGroup = true; + } else if (key === AWS_OTLP_LOGS_STREAM_HEADER && value) { + hasLogStream = true; + } + } + } + + if (!hasLogGroup || !hasLogStream) { + diag.warn( + `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; + } + + 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 new file mode 100644 index 00000000..2bdc0be8 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts @@ -0,0 +1,87 @@ +// 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 dependenciesLoaded = false; + +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'); +} +// 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 endpoint: URL; + private region: string; + private service: string; + + constructor(endpoint: string, service: string) { + this.endpoint = new URL(endpoint); + this.region = endpoint.split('.')[1]; + this.service = service; + } + + public async authenticate(headers: Record, serializedData: Uint8Array | undefined) { + // Only do SigV4 Signing if the required dependencies are installed. + if (dependenciesLoaded && serializedData) { + const cleanedHeaders = this.removeSigV4Headers(headers); + + const request = new HttpRequest({ + method: 'POST', + protocol: 'https', + hostname: this.endpoint.hostname, + path: this.endpoint.pathname, + body: serializedData, + headers: { + ...cleanedHeaders, + host: this.endpoint.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 export request with error: ${exception}`); + } + } + + diag.debug('Given serialized data is undefined. Not authenticating.'); + return headers; + } + + // Cleans up Sigv4 from headers to avoid accidentally copying them to the new headers + private removeSigV4Headers(headers: Record) { + const newHeaders: Record = {}; + + for (const key in headers) { + if (!SIG_V4_HEADERS.includes(key.toLowerCase())) { + newHeaders[key] = headers[key]; + } + } + return newHeaders; + } +} 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 new file mode 100644 index 00000000..b7242ebb --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts @@ -0,0 +1,96 @@ +// 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 { 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 { 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 + * 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 compression: CompressionAlgorithm | undefined; + private endpoint: string; + private serializer: PassthroughSerializer; + private authenticator: AwsAuthenticator; + + constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { + const modifiedConfig: OTLPExporterNodeConfigBase = { + ...config, + url: endpoint, + compression: CompressionAlgorithm.NONE, // Setting Compression to NONE as compression will be handled here. + }; + + super(modifiedConfig); + this.compression = config?.compression; + this.endpoint = endpoint; + 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. + this.serializer = new PassthroughSerializer(ProtobufLogsSerializer.deserializeResponse); + this['_delegate']._serializer = this.serializer; + } + + /** + * 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. + */ + public override async export( + items: ReadableLogRecord[], + resultCallback: (result: ExportResult) => void + ): Promise { + 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 signedRequestHeaders = await this.authenticator.authenticate(headers, serializedLogs); + + 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'); + } + + 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 new file mode 100644 index 00000000..28a2e7c1 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -0,0 +1,93 @@ +// 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 { CompressionAlgorithm, OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; +import { IExportTraceServiceResponse, ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer'; +import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +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 + * to the XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. 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 OTLPAwsSpanExporter extends OTLPProtoTraceExporter { + private compression: CompressionAlgorithm | undefined; + private endpoint: string; + private serializer: PassthroughSerializer; + private authenticator: AwsAuthenticator; + + constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { + const modifiedConfig: OTLPExporterNodeConfigBase = { + ...config, + url: endpoint, + compression: CompressionAlgorithm.NONE, // Setting Compression to NONE as compression will be handled here. + }; + + super(modifiedConfig); + this.endpoint = endpoint; + 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. + 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. + * 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 { + let serializedSpans: Uint8Array | undefined = ProtobufTraceSerializer.serializeRequest(items); + + if (serializedSpans === undefined) { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Nothing to send'), + }); + return; + } + + 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) { + if (shouldCompress) { + headers['Content-Encoding'] = 'gzip'; + } else { + delete headers['Content-Encoding']; + } + + const signedRequestHeaders = await this.authenticator.authenticate(headers, serializedSpans); + + 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'); + } + + super.export(items, resultCallback); + } +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/otlp-aws-span-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/otlp-aws-span-exporter.ts deleted file mode 100644 index 406d0bea..00000000 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/otlp-aws-span-exporter.ts +++ /dev/null @@ -1,144 +0,0 @@ -// 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'; - -/** - * This exporter extends the functionality of the OTLPProtoTraceExporter to allow spans to be exported - * to the XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. 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 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; - - constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { - super(OTLPAwsSpanExporter.changeUrlConfig(endpoint, config)); - this.initDependencies(); - this.region = endpoint.split('.')[1]; - this.endpoint = endpoint; - } - - /** - * 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. - */ - 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 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}` - ); - } - } - } - - 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/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..38ba76b2 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts @@ -0,0 +1,168 @@ +// 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(AWS_OTLP_TRACES_ENDPOINT, 'xray').authenticate( + {}, + 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(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); + 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(AWS_OTLP_TRACES_ENDPOINT, 'xray').authenticate( + { 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(AWS_OTLP_TRACES_ENDPOINT, 'xray').authenticate( + 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..2772193a --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts @@ -0,0 +1,79 @@ +// 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(headers: Record, serializedData: Uint8Array | undefined) { + 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; + 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()); + + 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..90089549 --- /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(headers: Record, serializedData: Uint8Array | undefined) { + 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 4002293c..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/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..18722309 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,9 @@ "@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", "@opentelemetry/exporter-trace-otlp-proto": "0.57.1", @@ -53,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", 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",