diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/package.json b/aws-distro-opentelemetry-node-autoinstrumentation/package.json index 9db79ef0..2d99048f 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": { @@ -108,6 +108,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", @@ -119,6 +122,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 436d62a3..67d5c4af 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,28 +54,43 @@ 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'; + import { isAgentObservabilityEnabled } from './utils'; import { BaggageSpanProcessor } from '@opentelemetry/baggage-span-processor'; import { logs } from '@opentelemetry/api-logs'; -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'; @@ -99,6 +118,7 @@ export class AwsOpentelemetryConfigurator { private idGenerator: IdGenerator; private sampler: Sampler; private spanProcessors: SpanProcessor[]; + private logRecordProcessors: LogRecordProcessor[]; private propagator: TextMapPropagator; /** @@ -182,6 +202,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); } @@ -210,6 +231,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, }; @@ -242,7 +264,7 @@ export class AwsOpentelemetryConfigurator { let spanExporter: SpanExporter; // Create the appropriate span exporter based on the endpoint - if (isXrayOtlpEndpoint(tracesEndpoint)) { + if (isAwsOtlpEndpoint(tracesEndpoint, 'xray')) { spanExporter = new OTLPAwsSpanExporter(tracesEndpoint, undefined, logs.getLoggerProvider()); } else { spanExporter = new OTLPAwsSpanExporter(tracesEndpoint); @@ -429,6 +451,150 @@ 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.toLowerCase(), { + 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.toLowerCase(), { + 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 @@ -472,7 +638,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(); @@ -489,9 +655,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.toLowerCase()); } return new OTLPProtoTraceExporter(); case 'udp': @@ -499,9 +665,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.toLowerCase()); } return new OTLPProtoTraceExporter(); } @@ -711,8 +877,61 @@ 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 { + let pattern = ''; + if (service === 'xray') { + pattern = AWS_TRACES_OTLP_ENDPOINT_PATTERN; + } else if (service === 'logs') { + pattern = AWS_LOGS_OTLP_ENDPOINT_PATTERN; + } else { + return false; + } + + 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( + 'Missing required configuration: The environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS must be set with ' + + `required headers ${AWS_OTLP_LOGS_GROUP_HEADER} and ${AWS_OTLP_LOGS_STREAM_HEADER}. ` + + `Example: OTEL_EXPORTER_OTLP_LOGS_HEADERS="${AWS_OTLP_LOGS_GROUP_HEADER}=my-log-group,${AWS_OTLP_LOGS_STREAM_HEADER}=my-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( + 'Incomplete 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..864fb692 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/aws-authenticator.ts @@ -0,0 +1,100 @@ +// 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 +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'; +export const X_AMZ_CONTENT_SHA256_HEADER = 'x-amz-content-sha256'; + +export class AwsAuthenticator { + private endpoint: URL; + private region: string; + private service: string; + + constructor(endpoint: string, service: string) { + // The endpoint is pre-validated by the config with isAwsOtlpEndpoint, so then endpoint is guaranteed to be well formatted and + // new URL() will not throw + 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}`); + return undefined; + } + } + + diag.debug('No serialized data provided. Not authenticating.'); + return undefined; + } + + // Cleans up Sigv4 from headers to avoid accidentally copying them to the new headers + private removeSigV4Headers(headers: Record) { + const newHeaders: Record = {}; + const sigv4Headers = [ + AUTHORIZATION_HEADER, + X_AMZ_CONTENT_SHA256_HEADER, + X_AMZ_DATE_HEADER, + X_AMZ_CONTENT_SHA256_HEADER, + ]; + + for (const key in headers) { + if (!sigv4Headers.includes(key.toLowerCase())) { + newHeaders[key] = headers[key]; + } + } + return newHeaders; + } +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/otlp-aws-base-exporter.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/otlp-aws-base-exporter.ts new file mode 100644 index 00000000..15f5bafd --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/common/otlp-aws-base-exporter.ts @@ -0,0 +1,152 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { CompressionAlgorithm, OTLPExporterBase } from '@opentelemetry/otlp-exporter-base'; +import { gzipSync } from 'zlib'; +import { ExportResult, ExportResultCode } from '@opentelemetry/core'; +import { AwsAuthenticator } from './aws-authenticator'; +import { ISerializer } from '@opentelemetry/otlp-transformer'; + +/** + * Base class for AWS OTLP exporters + */ +export abstract class OTLPAwsBaseExporter extends OTLPExporterBase { + protected parentExporter: OTLPExporterBase; + private readonly compression?: CompressionAlgorithm; + private endpoint: string; + private serializer: PassthroughSerializer; + private authenticator: AwsAuthenticator; + private parentSerializer: ISerializer; + + constructor( + endpoint: string, + service: string, + parentExporter: OTLPExporterBase, + parentSerializer: ISerializer, + compression?: CompressionAlgorithm + ) { + super(parentExporter['_delegate']); + this.compression = compression; + this.endpoint = endpoint; + this.authenticator = new AwsAuthenticator(this.endpoint, service); + this.parentExporter = parentExporter; + this.parentSerializer = parentSerializer; + + // 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. + // This is used in order to prevent serializing and compressing the data again when calling parentExporter.export(). + // To see why this works: + // https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/otlp-exporter-base/src/otlp-export-delegate.ts#L69 + this.serializer = new PassthroughSerializer(this.parentSerializer.deserializeResponse); + this.parentExporter['_delegate']._serializer = this.serializer; + } + + /** + * Overrides the upstream implementation of export. + * All behaviors are the same except if the endpoint is an AWS OTLP endpoint, we will sign the request with SigV4 + * in headers before sending it to the endpoint. + * @param items - Array of signal data to export + * @param resultCallback - Callback function to handle export result + */ + override async export(items: Payload, resultCallback: (result: ExportResult) => void): Promise { + const headers = this.parentExporter['_delegate']._transport?._transport?._parameters?.headers(); + + if (!headers) { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error(`Request headers are unset - unable to export to ${this.endpoint}`), + }); + return; + } + + let serializedData: Uint8Array | undefined = this.parentSerializer.serializeRequest(items); + + if (!serializedData) { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Nothing to send'), + }); + return; + } + + delete headers['Content-Encoding']; + const shouldCompress = this.compression && this.compression !== CompressionAlgorithm.NONE; + + if (shouldCompress) { + try { + serializedData = gzipSync(serializedData); + headers['Content-Encoding'] = 'gzip'; + } catch (exception) { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error(`Failed to compress: ${exception}`), + }); + return; + } + } + + this.serializer.setSerializedData(serializedData); + const signedHeaders = await this.authenticator.authenticate(headers, serializedData); + + if (!signedHeaders) { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Sigv4 Signing Failed. Not exporting'), + }); + return; + } + + this.parentExporter['_delegate']._transport._transport._parameters.headers = () => signedHeaders; + this.parentExporter.export(items, resultCallback); + } + + override shutdown(): Promise { + return this.parentExporter.shutdown(); + } + + override forceFlush(): Promise { + return this.parentExporter.forceFlush(); + } +} + +/** + * A serializer that bypasses request serialization by returning pre-serialized data. + * @template Response The type of the deserialized response + */ +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..55ba4170 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/logs/otlp-aws-log-exporter.ts @@ -0,0 +1,32 @@ +// 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 { IExportLogsServiceResponse, ProtobufLogsSerializer } from '@opentelemetry/otlp-transformer'; +import { LogRecordExporter, ReadableLogRecord } from '@opentelemetry/sdk-logs'; +import { OTLPAwsBaseExporter } from '../common/otlp-aws-base-exporter'; + +/** + * This exporter extends the functionality of the OTLPProtoLogExporter to allow logs 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. + * @param endpoint - The AWS CloudWatch Logs OTLP endpoint URL + * @param config - Optional OTLP exporter configuration + */ +export class OTLPAwsLogExporter + extends OTLPAwsBaseExporter + implements LogRecordExporter +{ + constructor(endpoint: string, config?: OTLPExporterNodeConfigBase) { + const modifiedConfig: OTLPExporterNodeConfigBase = { + ...config, + url: endpoint, + compression: CompressionAlgorithm.NONE, + }; + + super(endpoint, 'logs', new OTLPProtoLogExporter(modifiedConfig), ProtobufLogsSerializer, config?.compression); + } +} 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..ee63011d --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/exporter/otlp/aws/traces/otlp-aws-span-exporter.ts @@ -0,0 +1,77 @@ +// 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, SpanExporter } from '@opentelemetry/sdk-trace-base'; +import { OTLPAwsBaseExporter } from '../common/otlp-aws-base-exporter'; +import { LLOHandler } from '../../../../llo-handler'; +import { LoggerProvider as APILoggerProvider, logs } from '@opentelemetry/api-logs'; +import { ExportResult } from '@opentelemetry/core'; +import { isAgentObservabilityEnabled } from '../../../../utils'; +import { diag } from '@opentelemetry/api'; +import { LoggerProvider } from '@opentelemetry/sdk-logs'; + +/** + * 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. + * + * @param endpoint - The AWS X-Ray OTLP endpoint URL + * @param config - Optional OTLP exporter configuration + */ +export class OTLPAwsSpanExporter + extends OTLPAwsBaseExporter + implements SpanExporter +{ + private loggerProvider: APILoggerProvider | undefined; + private lloHandler: LLOHandler | undefined; + + constructor(endpoint: string, config?: OTLPExporterNodeConfigBase, loggerProvider?: APILoggerProvider) { + const modifiedConfig: OTLPExporterNodeConfigBase = { + ...config, + url: endpoint, + compression: CompressionAlgorithm.NONE, + }; + + super(endpoint, 'xray', new OTLPProtoTraceExporter(modifiedConfig), ProtobufTraceSerializer, config?.compression); + + this.lloHandler = undefined; + this.loggerProvider = loggerProvider; + } + + // Lazily initialize LLO handler when needed to avoid initialization order issues + private ensureLloHandler(): boolean { + if (!this.lloHandler && isAgentObservabilityEnabled()) { + // If loggerProvider wasn't provided, try to get the current one + if (!this.loggerProvider) { + try { + this.loggerProvider = logs.getLoggerProvider(); + } catch (e: unknown) { + diag.debug('Failed to get logger provider', e); + return false; + } + } + + if (this.loggerProvider instanceof LoggerProvider) { + this.lloHandler = new LLOHandler(this.loggerProvider); + return true; + } + } + + return !!this.lloHandler; + } + + override async export(items: ReadableSpan[], resultCallback: (result: ExportResult) => void): Promise { + let itemsToSerialize: ReadableSpan[] = items; + if (isAgentObservabilityEnabled() && this.ensureLloHandler() && this.lloHandler) { + // items to serialize are now the lloProcessedSpans + itemsToSerialize = this.lloHandler.processSpans(items); + } + + return super.export(itemsToSerialize, 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 668365c5..00000000 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/otlp-aws-span-exporter.ts +++ /dev/null @@ -1,181 +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 { 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 { LLOHandler } from './llo-handler'; -import { LoggerProvider as APILoggerProvider, logs } from '@opentelemetry/api-logs'; -import { LoggerProvider } from '@opentelemetry/sdk-logs'; -import { getNodeVersion, isAgentObservabilityEnabled } from './utils'; -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 static readonly SERVICE_NAME: string = 'xray'; - private endpoint: string; - private region: string; - - // Holds the dependencies needed to sign the SigV4 headers - private defaultProvider: any; - private sha256: any; - private signatureV4: any; - private httpRequest: any; - - // If the required dependencies are installed then we enable SigV4 signing. Otherwise skip it - private hasRequiredDependencies: boolean = false; - - private lloHandler: LLOHandler | undefined; - private loggerProvider: APILoggerProvider | undefined; - - constructor(endpoint: string, config?: OTLPExporterNodeConfigBase, loggerProvider?: APILoggerProvider) { - super(OTLPAwsSpanExporter.changeUrlConfig(endpoint, config)); - this.initDependencies(); - this.region = endpoint.split('.')[1]; - this.endpoint = endpoint; - - this.lloHandler = undefined; - this.loggerProvider = loggerProvider; - } - - // Lazily initialize LLO handler when needed to avoid initialization order issues - private ensureLloHandler(): boolean { - if (!this.lloHandler && isAgentObservabilityEnabled()) { - // If loggerProvider wasn't provided, try to get the current one - if (!this.loggerProvider) { - try { - this.loggerProvider = logs.getLoggerProvider(); - } catch (e: unknown) { - diag.debug('Failed to get logger provider', e); - return false; - } - } - - if (this.loggerProvider instanceof LoggerProvider) { - this.lloHandler = new LLOHandler(this.loggerProvider); - return true; - } - } - - return !!this.lloHandler; - } - - /** - * 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 { - let itemsToSerialize: ReadableSpan[] = items; - if (isAgentObservabilityEnabled() && this.ensureLloHandler() && this.lloHandler) { - // items to serialize are now the lloProcessedSpans - itemsToSerialize = this.lloHandler.processSpans(items); - } - - // 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(itemsToSerialize); - - 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(itemsToSerialize, 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 8f26f5e6..65306730 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,8 +46,15 @@ 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 { OTLPAwsSpanExporter } from '../src/otlp-aws-span-exporter'; import { BaggageSpanProcessor } from '@opentelemetry/baggage-span-processor'; +import { + BatchLogRecordProcessor, + ConsoleLogRecordExporter, + LogRecordExporter, + SimpleLogRecordProcessor, +} 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', () => { @@ -685,6 +696,45 @@ describe('AwsOpenTelemetryConfiguratorTest', () => { delete process.env.OTEL_TRACES_EXPORTER; }); + it('OtelLogExporterInputValidationTest', () => { + let config; + + // Default scenario where no log exporter is specified + process.env.OTEL_LOGS_EXPORTER = 'none'; + config = new AwsOpentelemetryConfigurator([]).configure(); + expect(config.logRecordProcessors?.length).toEqual(0); + + // Scenario where otlp log exporter is specified + process.env.OTEL_LOGS_EXPORTER = 'otlp'; + config = new AwsOpentelemetryConfigurator([]).configure(); + expect(config.logRecordProcessors?.length).toEqual(1); + expect((config.logRecordProcessors as any)[0]._exporter).toBeInstanceOf(OTLPProtoLogExporter); + + // Specify invalid exporter, same result as default scenario + process.env.OTEL_LOGS_EXPORTER = 'invalid_exporter_name'; + config = new AwsOpentelemetryConfigurator([]).configure(); + expect(config.logRecordProcessors?.length).toEqual(0); + + // Test console exporter + process.env.OTEL_LOGS_EXPORTER = 'console'; + config = new AwsOpentelemetryConfigurator([]).configure(); + expect(config.logRecordProcessors?.length).toEqual(1); + expect((config.logRecordProcessors as any)[0]._exporter).toBeInstanceOf(ConsoleLogRecordExporter); + + // Test AWS OTLP logs endpoint uses OTLPAwsLogExporter + process.env.OTEL_LOGS_EXPORTER = 'otlp'; + process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'https://logs.us-east-1.amazonaws.com/v1/logs'; + process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'x-aws-log-group=my-group,x-aws-log-stream=my-stream'; + config = new AwsOpentelemetryConfigurator([]).configure(); + expect(config.logRecordProcessors?.length).toEqual(1); + expect((config.logRecordProcessors as any)[0]._exporter).toBeInstanceOf(OTLPAwsLogExporter); + + // Cleanup + delete process.env.OTEL_LOGS_EXPORTER; + delete process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; + delete process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS; + }); + it('ResourceDetectorInputValidationTest', () => { let config; process.env.OTEL_SERVICE_NAME = 'test_service_name'; @@ -715,107 +765,376 @@ 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('ExportUnsampledSpanForAgentObservabilityTest', () => { - const spanProcessorsToTest: SpanProcessor[] = []; + it('ExportUnsampledSpanForAgentObservabilityTest', () => { + const spanProcessorsToTest: SpanProcessor[] = []; - // Test with agent observability disabled - AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty()); - expect(spanProcessorsToTest).toEqual([]); + // Test with agent observability disabled + AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty()); + expect(spanProcessorsToTest).toEqual([]); - // Test with agent observability enabled - process.env.AGENT_OBSERVABILITY_ENABLED = 'true'; - process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'https://xray.us-east-1.amazonaws.com/v1/traces'; + // Test with agent observability enabled + process.env.AGENT_OBSERVABILITY_ENABLED = 'true'; + process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'https://xray.us-east-1.amazonaws.com/v1/traces'; - AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty()); - expect(spanProcessorsToTest.length).toEqual(1); + AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty()); + expect(spanProcessorsToTest.length).toEqual(1); - const processor = spanProcessorsToTest[0]; - expect(processor).toBeInstanceOf(AwsBatchUnsampledSpanProcessor); + const processor = spanProcessorsToTest[0]; + expect(processor).toBeInstanceOf(AwsBatchUnsampledSpanProcessor); - // Cleanup - delete process.env.AGENT_OBSERVABILITY_ENABLED; - delete process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; - }); + // Cleanup + delete process.env.AGENT_OBSERVABILITY_ENABLED; + delete process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; + }); - it('ExportUnsampledSpanForAgentObservabilityUsesOtlpAwsSpanExporterTest', () => { - const spanProcessorsToTest: SpanProcessor[] = []; + it('ExportUnsampledSpanForAgentObservabilityUsesOtlpAwsSpanExporterTest', () => { + const spanProcessorsToTest: SpanProcessor[] = []; - process.env.AGENT_OBSERVABILITY_ENABLED = 'true'; - process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'https://xray.us-east-1.amazonaws.com/v1/traces'; + process.env.AGENT_OBSERVABILITY_ENABLED = 'true'; + process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'https://xray.us-east-1.amazonaws.com/v1/traces'; - AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty()); + AwsOpentelemetryConfigurator.exportUnsampledSpanForAgentObservability(spanProcessorsToTest, Resource.empty()); - // Verify AwsBatchUnsampledSpanProcessor was created with the AWS exporter - expect(spanProcessorsToTest[0]).toBeInstanceOf(AwsBatchUnsampledSpanProcessor); - const otlpAwsSpanExporter = (spanProcessorsToTest[0] as AwsBatchUnsampledSpanProcessor)['_exporter']; + // Verify AwsBatchUnsampledSpanProcessor was created with the AWS exporter + expect(spanProcessorsToTest[0]).toBeInstanceOf(AwsBatchUnsampledSpanProcessor); + const otlpAwsSpanExporter = (spanProcessorsToTest[0] as AwsBatchUnsampledSpanProcessor)['_exporter']; - // Verify OTLPAwsSpanExporter was created with correct parameters - expect(otlpAwsSpanExporter).toBeInstanceOf(OTLPAwsSpanExporter); - expect(otlpAwsSpanExporter['endpoint']).toEqual('https://xray.us-east-1.amazonaws.com/v1/traces'); - expect(otlpAwsSpanExporter['loggerProvider']).toBeDefined(); + // Verify OTLPAwsSpanExporter was created with correct parameters + expect(otlpAwsSpanExporter).toBeInstanceOf(OTLPAwsSpanExporter); + expect(otlpAwsSpanExporter['endpoint']).toEqual('https://xray.us-east-1.amazonaws.com/v1/traces'); + expect(otlpAwsSpanExporter['loggerProvider']).toBeDefined(); - // Cleanup environment variables - delete process.env.AGENT_OBSERVABILITY_ENABLED; - delete process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; + // Cleanup environment variables + delete process.env.AGENT_OBSERVABILITY_ENABLED; + delete process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; + }); + + it('CustomizeSpanProcessorsCallsExportUnsampledSpanTest', () => { + const spanProcessorsToTest: SpanProcessor[] = []; + + // Create spy for exportUnsampledSpanForAgentObservability + const exportUnsampledSpanSpy = sinon.spy( + AwsOpentelemetryConfigurator, + 'exportUnsampledSpanForAgentObservability' + ); + + try { + // Test that function is NOT called when agent observability is disabled + delete process.env.AGENT_OBSERVABILITY_ENABLED; + AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty()); + expect(exportUnsampledSpanSpy.called).toBeFalsy(); + + // Test that function is called when agent observability is enabled + exportUnsampledSpanSpy.resetHistory(); + process.env.AGENT_OBSERVABILITY_ENABLED = 'true'; + AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty()); + expect(exportUnsampledSpanSpy.calledOnce).toBeTruthy(); + expect(exportUnsampledSpanSpy.calledWith(spanProcessorsToTest, Resource.empty())).toBeTruthy(); + } finally { + // Restore original implementation + exportUnsampledSpanSpy.restore(); + + // Cleanup + delete process.env.AGENT_OBSERVABILITY_ENABLED; + } + }); + + 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); + } + + // bad 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); + } + }); }); - it('CustomizeSpanProcessorsCallsExportUnsampledSpanTest', () => { - const spanProcessorsToTest: SpanProcessor[] = []; + describe('AwsLoggerProcessorProvider', () => { + it('getlogRecordProcessors', () => { + process.env.OTEL_LOGS_EXPORTER = 'otlp'; + let logRecordProcessors = AwsLoggerProcessorProvider.getlogRecordProcessors(); - // Create spy for exportUnsampledSpanForAgentObservability - const exportUnsampledSpanSpy = sinon.spy(AwsOpentelemetryConfigurator, 'exportUnsampledSpanForAgentObservability'); + expect(logRecordProcessors).toHaveLength(1); + expect(logRecordProcessors[0]).toBeInstanceOf(BatchLogRecordProcessor); - try { - // Test that function is NOT called when agent observability is disabled - delete process.env.AGENT_OBSERVABILITY_ENABLED; - AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty()); - expect(exportUnsampledSpanSpy.called).toBeFalsy(); + process.env.OTEL_LOGS_EXPORTER = 'console'; + logRecordProcessors = AwsLoggerProcessorProvider.getlogRecordProcessors(); - // Test that function is called when agent observability is enabled - exportUnsampledSpanSpy.resetHistory(); - process.env.AGENT_OBSERVABILITY_ENABLED = 'true'; - AwsOpentelemetryConfigurator.customizeSpanProcessors(spanProcessorsToTest, Resource.empty()); - expect(exportUnsampledSpanSpy.calledOnce).toBeTruthy(); - expect(exportUnsampledSpanSpy.calledWith(spanProcessorsToTest, Resource.empty())).toBeTruthy(); - } finally { - // Restore original implementation - exportUnsampledSpanSpy.restore(); + expect(logRecordProcessors).toHaveLength(1); + expect(logRecordProcessors[0]).toBeInstanceOf(SimpleLogRecordProcessor); + + delete process.env.OTEL_LOGS_EXPORTER; + }); + + 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.AGENT_OBSERVABILITY_ENABLED; - } + 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-group=test,x-aws-log-group=test', + 'x-aws-log-stream=test,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..655e7073 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/aws-authenticator.test.ts @@ -0,0 +1,130 @@ +// 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 { + AUTHORIZATION_HEADER, + AwsAuthenticator, + X_AMZ_CONTENT_SHA256_HEADER, + X_AMZ_DATE_HEADER, + X_AMZ_SECURITY_TOKEN_HEADER, +} from '../../../../../src/exporter/otlp/aws/common/aws-authenticator'; +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 = [ + '@smithy/signature-v4', + '@aws-sdk/credential-provider-node', + '@aws-crypto/sha256-js', + '@smithy/protocol-http', + ]; + + dependencies.forEach(dependency => { + it(`should not sign headers if missing dependency: ${dependency}`, async () => { + const stubs: { [key: string]: any } = {}; + stubs[dependency] = new Proxy( + {}, + { + get() { + throw new Error(`Cannot find module '${dependency}'`); + }, + } + ); + + const { AwsAuthenticator } = proxyquire('../../../../../src/exporter/otlp/aws/common/aws-authenticator', stubs); + + const result = await new AwsAuthenticator( + 'https://xray.us-east-1.amazonaws.com/v1/traces', + 'xray' + ).authenticate({}, new Uint8Array()); + + expect(result).toBe(undefined); + }); + }); + }); + + it('should not inject SigV4 Headers if serialized data is undefined', async () => { + const authenticator = new AwsAuthenticator('https://xray.us-east-1.amazonaws.com/v1/traces', 'xray'); + const result = await authenticator.authenticate({}, undefined); + + expect(result).toBe(undefined); + }); + + it('should inject SigV4 Headers', async () => { + const AwsAuthenticatorWithMock = proxyquire('../../../../../src/exporter/otlp/aws/common/aws-authenticator', { + '@aws-sdk/credential-provider-node': { + defaultProvider: sandbox.stub().resolves(mockCredentials), + }, + }).AwsAuthenticator; + + const result = await new AwsAuthenticatorWithMock( + 'https://xray.us-east-1.amazonaws.com/v1/traces', + '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).toHaveProperty(X_AMZ_CONTENT_SHA256_HEADER); + } else { + expect(result).toBe(undefined); + } + }); + + it('should clear SigV4 headers if already present ', async () => { + const oldHeaders = { + [AUTHORIZATION_HEADER]: 'notExpectedAuth', + [X_AMZ_DATE_HEADER]: 'notExpectedDate', + [X_AMZ_SECURITY_TOKEN_HEADER]: 'notExpectedSecurityToken', + [X_AMZ_CONTENT_SHA256_HEADER]: 'notExpectedSha256Content', + }; + + const AwsAuthenticatorWithMock = proxyquire('../../../../../src/exporter/otlp/aws/common/aws-authenticator', { + '@aws-sdk/credential-provider-node': { + defaultProvider: sandbox.stub().resolves(mockCredentials), + }, + }).AwsAuthenticator; + + const result = await new AwsAuthenticatorWithMock( + 'https://xray.us-east-1.amazonaws.com/v1/traces', + 'xray' + ).authenticate(oldHeaders, 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).toHaveProperty(X_AMZ_CONTENT_SHA256_HEADER); + expect(result[AUTHORIZATION_HEADER]).not.toBe(oldHeaders[AUTHORIZATION_HEADER]); + expect(result[X_AMZ_DATE_HEADER]).not.toBe(oldHeaders[X_AMZ_DATE_HEADER]); + expect(result[X_AMZ_SECURITY_TOKEN_HEADER]).not.toBe(oldHeaders[X_AMZ_SECURITY_TOKEN_HEADER]); + expect(result[X_AMZ_CONTENT_SHA256_HEADER]).not.toBe(oldHeaders[X_AMZ_CONTENT_SHA256_HEADER]); + } else { + expect(result).toBe(undefined); + } + }); +}); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/otlp-aws-base-exporter.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/otlp-aws-base-exporter.test.ts new file mode 100644 index 00000000..4c0c5dc5 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/common/otlp-aws-base-exporter.test.ts @@ -0,0 +1,237 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import expect from 'expect'; +import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'; +import * as sinon from 'sinon'; +import * as nock from 'nock'; +import { ExportResult, ExportResultCode } from '@opentelemetry/core'; +import { + AUTHORIZATION_HEADER, + AwsAuthenticator, + X_AMZ_CONTENT_SHA256_HEADER, + X_AMZ_DATE_HEADER, + X_AMZ_SECURITY_TOKEN_HEADER, +} from '../../../../../src/exporter/otlp/aws/common/aws-authenticator'; + +const EXPECTED_AUTH_HEADER = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/logs/aws4_request'; +const EXPECTED_X_AMZ_DATE = 'some_date'; +const EXPECTED_X_AMZ_SECURITY_TOKEN = 'test_token'; +const EXPECTED_X_AMZ_SHA_256 = 'test_sha256'; + +export abstract class OTLPAwsBaseExporterTest { + protected sandbox!: sinon.SinonSandbox; + protected scope!: nock.Scope; + + protected abstract getEndpoint(): string; + protected abstract getEndpointPath(): string; + protected abstract getExporter(): any; + + public beforeEach() { + this.sandbox = sinon.createSandbox(); + + this.scope = nock(this.getEndpoint()) + .post(this.getEndpointPath()) + .reply((uri: any, requestBody: any) => { + return [200, '']; + }); + + // Stub AWS authenticator + this.sandbox + .stub(AwsAuthenticator.prototype, 'authenticate') + .callsFake((headers: Record, serializedData: Uint8Array | undefined) => { + return Promise.resolve({ + ...headers, + [AUTHORIZATION_HEADER]: EXPECTED_AUTH_HEADER, + [X_AMZ_DATE_HEADER]: EXPECTED_X_AMZ_DATE, + [X_AMZ_SECURITY_TOKEN_HEADER]: EXPECTED_X_AMZ_SECURITY_TOKEN, + [X_AMZ_CONTENT_SHA256_HEADER]: EXPECTED_X_AMZ_SHA_256, + }); + }); + } + + public afterEach() { + this.sandbox.restore(); + } + + public testCommon(): Array<{ description: string; test: (done: () => void) => void }> { + return [ + { + description: 'Should inject SigV4 Headers successfully', + test: (done: () => void) => this.testSigV4Headers(done), + }, + { + description: 'Should enable compression with gzip', + test: (done: () => void) => this.testEnableCompression(done), + }, + { + description: 'Should call serializer and gzip only once during export', + test: (done: () => void) => this.testSerializerAndGzipCalledOnce(done), + }, + { + description: 'Should fail when gzip compression throws exception', + test: (done: () => void) => this.testGzipException(done), + }, + { + description: 'Should fail when serialization returns undefined', + test: (done: () => void) => this.testEmptySerialize(done), + }, + { + description: 'Should fail when headers are undefined', + test: (done: () => void) => this.testUndefinedHeaders(done), + }, + { + description: 'Should continue when authenticate returns undefined', + test: (done: () => void) => this.testSigningFails(done), + }, + ]; + } + + private testSigV4Headers(done: () => void) { + const exporterClass = this.getExporter(); + const exporter = new exporterClass(this.getEndpoint() + this.getEndpointPath()); + + exporter + .export([], (result: ExportResult) => { + expect(result.code).toBe(ExportResultCode.SUCCESS); + expect(result.error?.message).toBe(undefined); + }) + .then(() => { + this.scope.on('request', (req, interceptor, body) => { + this.assertHeaders(req.headers); + expect(req.headers).not.toHaveProperty('content-encoding'); + done(); + }); + }); + } + + private testEnableCompression(done: () => void) { + const exporterClass = this.getExporter(); + const exporter = new exporterClass(this.getEndpoint() + this.getEndpointPath(), { + compression: CompressionAlgorithm.GZIP, + }); + + exporter + .export([], (result: ExportResult) => { + expect(result.code).toBe(ExportResultCode.SUCCESS); + expect(result.error?.message).toBe(undefined); + }) + .then(() => { + this.scope.on('request', (req, interceptor, body) => { + this.assertHeaders(req.headers); + expect(req.headers['content-encoding']).toBe('gzip'); + + //Gzip first 10 bytes are reserved for metadata headers: + //https://www.loc.gov/preservation/digital/formats/fdd/fdd000599.shtml?loclr=blogsig + const data = Buffer.from(body, 'hex'); + expect(data.length).toBeGreaterThanOrEqual(10); + expect(data.subarray(0, 2)).toEqual(Buffer.from([0x1f, 0x8b])); + + done(); + }); + }); + } + + private testSerializerAndGzipCalledOnce(done: () => void) { + const exporterClass = this.getExporter(); + const exporter = new exporterClass(this.getEndpoint() + this.getEndpointPath(), { + compression: CompressionAlgorithm.GZIP, + }); + + const serializeStub = this.sandbox + .stub(exporter.parentSerializer, 'serializeRequest') + .returns(new Uint8Array([1, 2, 3])); + const gzipStub = this.sandbox.stub(require('zlib'), 'gzipSync').returns(new Uint8Array([0x1f, 0x8b, 1, 2, 3])); + + exporter + .export([], (result: ExportResult) => { + expect(result.code).toBe(ExportResultCode.SUCCESS); + expect(serializeStub.callCount).toBe(1); + expect(gzipStub.callCount).toBe(1); + }) + .then(() => { + this.scope.on('request', (req, interceptor, body) => { + this.assertHeaders(req.headers); + expect(req.headers['content-encoding']).toBe('gzip'); + done(); + }); + }); + } + + private testSigningFails(done: () => void) { + this.sandbox.restore(); + this.sandbox = sinon.createSandbox(); + this.sandbox.stub(AwsAuthenticator.prototype, 'authenticate').resolves(undefined); + + const exporterClass = this.getExporter(); + const exporter = new exporterClass(this.getEndpoint() + this.getEndpointPath()); + + exporter.export([], (result: ExportResult) => { + expect(result.code).toBe(ExportResultCode.FAILED); + expect(this.scope.isDone()).toBe(false); + done(); + }); + } + + private testEmptySerialize(done: () => void) { + const exporterClass = this.getExporter(); + const exporter = new exporterClass(this.getEndpoint() + this.getEndpointPath()); + exporter.parentSerializer = { + serializeRequest: this.sandbox.stub().returns(undefined), + deserializeResponse: this.sandbox.stub(), + }; + + exporter.export([], (result: ExportResult) => { + expect(result.code).toBe(ExportResultCode.FAILED); + expect(result.error?.message).toBe('Nothing to send'); + expect(this.scope.isDone()).toBe(false); + done(); + }); + } + + private testGzipException(done: () => void) { + const gzipStub = this.sandbox.stub(require('zlib'), 'gzipSync').throws(new Error('Compression failed')); + + const exporterClass = this.getExporter(); + const exporter = new exporterClass(this.getEndpoint() + this.getEndpointPath(), { + compression: CompressionAlgorithm.GZIP, + }); + + exporter.export([], (result: ExportResult) => { + expect(result.code).toBe(ExportResultCode.FAILED); + expect(result.error?.message).toContain('Failed to compress'); + expect(this.scope.isDone()).toBe(false); + gzipStub.restore(); + done(); + }); + } + + private testUndefinedHeaders(done: () => void) { + const exporterClass = this.getExporter(); + const exporter = new exporterClass(this.getEndpoint() + this.getEndpointPath()); + exporter.parentExporter['_delegate']._transport._transport._parameters.headers = this.sandbox + .stub() + .returns(undefined); + + exporter.export([], (result: ExportResult) => { + expect(result.code).toBe(ExportResultCode.FAILED); + expect(this.scope.isDone()).toBe(false); + done(); + }); + } + + private assertHeaders(headers: Record) { + 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).toHaveProperty(X_AMZ_CONTENT_SHA256_HEADER.toLowerCase()); + + expect(headers[AUTHORIZATION_HEADER.toLowerCase()]).toBe(EXPECTED_AUTH_HEADER); + expect(headers[X_AMZ_SECURITY_TOKEN_HEADER.toLowerCase()]).toBe(EXPECTED_X_AMZ_SECURITY_TOKEN); + expect(headers[X_AMZ_CONTENT_SHA256_HEADER.toLowerCase()]).toBe(EXPECTED_X_AMZ_SHA_256); + expect(headers[X_AMZ_DATE_HEADER.toLowerCase()]).toBe(EXPECTED_X_AMZ_DATE); + + expect(headers['content-type']).toBe('application/x-protobuf'); + expect(headers['user-agent']).toMatch(/^OTel-OTLP-Exporter-JavaScript\/\d+\.\d+\.\d+$/); + } +} 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..be28034f --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/logs/otlp-aws-log-exporter.test.ts @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { OTLPAwsBaseExporterTest } from '../common/otlp-aws-base-exporter.test'; +import { OTLPAwsLogExporter } from '../../../../../src/exporter/otlp/aws/logs/otlp-aws-log-exporter'; + +class OTLPAwsLogExporterTest extends OTLPAwsBaseExporterTest { + protected override getExporter() { + return OTLPAwsLogExporter; + } + protected getEndpoint(): string { + return 'https://logs.us-east-1.amazonaws.com'; + } + + protected getEndpointPath(): string { + return '/v1/logs'; + } +} + +describe('OTLPAwsLogExporter', () => { + const test = new OTLPAwsLogExporterTest(); + + beforeEach(() => { + test.beforeEach(); + }); + + afterEach(() => { + test.afterEach(); + }); + + test.testCommon().forEach(testCase => { + it(testCase.description, done => { + testCase.test(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..196e8279 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/exporter/otlp/aws/traces/otlp-aws-span-exporter.test.ts @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { OTLPAwsBaseExporterTest } from '../common/otlp-aws-base-exporter.test'; +import { OTLPAwsSpanExporter } from '../../../../../src/exporter/otlp/aws/traces/otlp-aws-span-exporter'; + +class OTLPAwsSpanExporterTest extends OTLPAwsBaseExporterTest { + protected override getExporter() { + return OTLPAwsSpanExporter; + } + protected getEndpoint(): string { + return 'https://xray.us-east-1.amazonaws.com'; + } + + protected getEndpointPath(): string { + return '/v1/traces'; + } +} + +describe('OTLPAwsSpanExporter', () => { + const test = new OTLPAwsSpanExporterTest(); + + beforeEach(() => { + test.beforeEach(); + }); + + afterEach(() => { + test.afterEach(); + }); + + test.testCommon().forEach(testCase => { + it(testCase.description, done => { + testCase.test(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 17ffe584..5d39605f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,9 @@ "@opentelemetry/auto-instrumentations-node": "0.56.0", "@opentelemetry/baggage-span-processor": "0.3.1", "@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", 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",