UNPKG

@aws/aws-distro-opentelemetry-node-autoinstrumentation

Version:

This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.

871 lines 49.3 kB
"use strict"; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.createEmfExporter = exports.checkEmfExporterEnabled = exports.validateAndFetchLogsHeader = exports.isAwsOtlpEndpoint = exports.isLambdaEnvironment = exports.buildSamplerFromEnv = exports.AwsSpanProcessorProvider = exports.AwsLoggerProcessorProvider = exports.ApplicationSignalsExporterProvider = exports.customBuildSamplerFromEnv = exports.AwsOpentelemetryConfigurator = exports.LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT = exports.AGENT_OBSERVABILITY_ENABLED = exports.AWS_LAMBDA_FUNCTION_NAME_CONFIG = void 0; const api_1 = require("@opentelemetry/api"); const auto_configuration_propagators_1 = require("@opentelemetry/auto-configuration-propagators"); const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node"); const core_1 = require("@opentelemetry/core"); const exporter_metrics_otlp_grpc_1 = require("@opentelemetry/exporter-metrics-otlp-grpc"); const otlp_exporter_base_1 = require("@opentelemetry/otlp-exporter-base"); const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http"); const exporter_trace_otlp_grpc_1 = require("@opentelemetry/exporter-trace-otlp-grpc"); const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http"); const exporter_trace_otlp_proto_1 = require("@opentelemetry/exporter-trace-otlp-proto"); const exporter_logs_otlp_grpc_1 = require("@opentelemetry/exporter-logs-otlp-grpc"); const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http"); const exporter_logs_otlp_proto_1 = require("@opentelemetry/exporter-logs-otlp-proto"); const exporter_zipkin_1 = require("@opentelemetry/exporter-zipkin"); const id_generator_aws_xray_1 = require("@opentelemetry/id-generator-aws-xray"); const resource_detector_aws_1 = require("@opentelemetry/resource-detector-aws"); const resources_1 = require("@opentelemetry/resources"); const sdk_metrics_1 = require("@opentelemetry/sdk-metrics"); const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base"); const sdk_logs_1 = require("@opentelemetry/sdk-logs"); const semantic_conventions_1 = require("@opentelemetry/semantic-conventions"); const always_record_sampler_1 = require("./always-record-sampler"); const attribute_propagating_span_processor_builder_1 = require("./attribute-propagating-span-processor-builder"); const aws_batch_unsampled_span_processor_1 = require("./aws-batch-unsampled-span-processor"); const aws_metric_attributes_span_exporter_builder_1 = require("./aws-metric-attributes-span-exporter-builder"); const aws_span_metrics_processor_builder_1 = require("./aws-span-metrics-processor-builder"); const otlp_aws_span_exporter_1 = require("./exporter/otlp/aws/traces/otlp-aws-span-exporter"); const otlp_udp_exporter_1 = require("./otlp-udp-exporter"); const aws_xray_remote_sampler_1 = require("./sampler/aws-xray-remote-sampler"); // This file is generated via `npm run compile` const version_1 = require("./version"); const aws_cloudwatch_emf_exporter_1 = require("./exporter/aws/metrics/aws-cloudwatch-emf-exporter"); const otlp_aws_log_exporter_1 = require("./exporter/otlp/aws/logs/otlp-aws-log-exporter"); const utils_1 = require("./utils"); const gen_ai_nested_client_span_processor_1 = require("./gen-ai-nested-client-span-processor"); const baggage_span_processor_1 = require("@opentelemetry/baggage-span-processor"); const api_logs_1 = require("@opentelemetry/api-logs"); const aws_attribute_keys_1 = require("./aws-attribute-keys"); const aws_cw_otlp_batch_log_record_processor_1 = require("./exporter/otlp/aws/logs/aws-cw-otlp-batch-log-record-processor"); const console_emf_exporter_1 = require("./exporter/aws/metrics/console-emf-exporter"); const compact_console_log_exporter_1 = require("./exporter/console/logs/compact-console-log-exporter"); 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 APPLICATION_SIGNALS_ENABLED_CONFIG = 'OTEL_AWS_APPLICATION_SIGNALS_ENABLED'; const APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG = 'OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT'; const METRIC_EXPORT_INTERVAL_CONFIG = 'OTEL_METRIC_EXPORT_INTERVAL'; const DEFAULT_METRIC_EXPORT_INTERVAL_MILLIS = 60000; exports.AWS_LAMBDA_FUNCTION_NAME_CONFIG = 'AWS_LAMBDA_FUNCTION_NAME'; exports.AGENT_OBSERVABILITY_ENABLED = 'AGENT_OBSERVABILITY_ENABLED'; const AWS_XRAY_DAEMON_ADDRESS_CONFIG = 'AWS_XRAY_DAEMON_ADDRESS'; const FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = 'T1S'; const FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = 'T1U'; // Follow Python SDK Impl to set the max span batch size // which will reduce the chance of UDP package size is larger than 64KB const LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10; exports.LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT = 'LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT'; const AWS_OTLP_LOGS_GROUP_HEADER = 'x-aws-log-group'; const AWS_OTLP_LOGS_STREAM_HEADER = 'x-aws-log-stream'; const AWS_EMF_METRICS_NAMESPACE = 'x-aws-metric-namespace'; /** * Aws Application Signals Config Provider creates a configuration object that can be provided to * the OTel NodeJS SDK for Auto Instrumentation with Application Signals Functionality. * * The config includes: * - Use AlwaysRecordSampler (wraps around a specified Sampler) to record all spans. * - Add SpanMetricsProcessor to create metrics. * - Add AttributePropagatingSpanProcessor to propagate span attributes from parent to child spans. * - Add AwsMetricAttributesSpanExporter to add more attributes to all spans. * * You can control when these customizations are applied using the environment variable * OTEL_AWS_APPLICATION_SIGNALS_ENABLED. This flag is disabled by default. */ class AwsOpentelemetryConfigurator { /** * The constructor will setup the AwsOpentelemetryConfigurator object to be able to provide a * configuration for ADOT JavaScript Auto-Instrumentation. * * The `instrumentations` are the desired Node auto-instrumentations to be used when using ADOT JavaScript. * The auto-Instrumentions are usually populated from OTel's `getNodeAutoInstrumentations()` method from the * `@opentelemetry/auto-instrumentations-node` NPM package, and may have instrumentation patching applied. * * @constructor * @param {Instrumentation[]} instrumentations - Auto-Instrumentations to be added to the ADOT Config */ constructor(instrumentations, useXraySampler = false) { // When Agent Observability is enabled, disable Application Signals dimensions by default // This can be overridden by explicitly setting the env var if ((0, utils_1.isAgentObservabilityEnabled)() && process.env['OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS'] === undefined) { process.env['OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS'] = 'false'; } /* * Set and Detect Resources via Resource Detectors * * The configurator must create and detect resources in order to populate any detected * resources into the Resource that is provided to the processors, exporters, and samplers * that are instantiated in the configurator. Otherwise, if only OTel handles resource * detection in the SDK, the AWS processors/exporters/samplers will lack such detected * resources in their respective resources. */ // Start with defaultResource() to include the default service.name (unknown_service:<process>) // OTel 2.x: resourceFromAttributes({}) doesn't include default service name let autoResource = (0, resources_1.defaultResource)(); autoResource = this.customizeVersions(autoResource); // The following if/else block is based on upstream's logic // https://github.com/open-telemetry/opentelemetry-js/blob/95edbd9992434f31f50532fedb3c7e8db5164479/experimental/packages/opentelemetry-sdk-node/src/sdk.ts#L125-L129 // In all cases, we want to include the Env Detector (Sync) and the AWS Resource Detectors let defaultDetectors = []; if (process.env.OTEL_NODE_RESOURCE_DETECTORS != null) { defaultDetectors = (0, auto_instrumentations_node_1.getResourceDetectors)(); // Add Env/AWS Resource Detectors if not present const resourceDetectorsFromEnv = process.env.OTEL_NODE_RESOURCE_DETECTORS.split(','); if (!resourceDetectorsFromEnv.includes('aws')) { defaultDetectors.push(resource_detector_aws_1.awsEc2Detector, resource_detector_aws_1.awsEcsDetector, resource_detector_aws_1.awsEksDetector); } if (!resourceDetectorsFromEnv.includes('env')) { defaultDetectors.push(resources_1.envDetector); } } else if (isLambdaEnvironment() || (0, utils_1.isAgentObservabilityEnabled)()) { // Only keep env detector here defaultDetectors.push(resources_1.envDetector); } else { // envDetector needs to be last so it can override any conflicting resource attributes. defaultDetectors = [resources_1.processDetector, resources_1.hostDetector, resource_detector_aws_1.awsEc2Detector, resource_detector_aws_1.awsEcsDetector, resource_detector_aws_1.awsEksDetector, resources_1.envDetector]; } const internalConfig = { detectors: defaultDetectors, }; autoResource = this.customizeResource(autoResource.merge((0, resources_1.detectResources)(internalConfig))); this.resource = autoResource; this.instrumentations = instrumentations; this.propagator = (0, auto_configuration_propagators_1.getPropagator)(); // TODO: Consider removing AWSXRayIdGenerator as it is not needed // Similarly to Java, always use AWS X-Ray Id Generator // https://github.com/aws-observability/aws-otel-java-instrumentation/blob/a011b8cc29ee32b7f668c04ccfdf64cd30de467c/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsTracerCustomizerProvider.java#L36 this.idGenerator = new id_generator_aws_xray_1.AWSXRayIdGenerator(); this.sampler = AwsOpentelemetryConfigurator.customizeSampler(customBuildSamplerFromEnv(this.resource, useXraySampler)); // default SpanProcessors with Span Exporters wrapped inside AwsMetricAttributesSpanExporter const awsSpanProcessorProvider = new AwsSpanProcessorProvider(this.resource); this.spanProcessors = awsSpanProcessorProvider.getSpanProcessors(); this.logRecordProcessors = AwsLoggerProcessorProvider.getlogRecordProcessors(); AwsOpentelemetryConfigurator.customizeSpanProcessors(this.spanProcessors, this.resource); const isEmfEnabled = checkEmfExporterEnabled(); this.customizeMetricReader(isEmfEnabled); } customizeVersions(autoResource) { // eslint-disable-next-line @typescript-eslint/typedef const DISTRO_VERSION = version_1.LIB_VERSION; const versionSuffix = DISTRO_VERSION + '-aws'; // OTel 2.x: Resource attributes are immutable, so merge with a new resource const versionResource = (0, resources_1.resourceFromAttributes)({ [semantic_conventions_1.SEMRESATTRS_TELEMETRY_AUTO_VERSION]: versionSuffix, }); api_1.diag.debug(`@aws/aws-distro-opentelemetry-node-autoinstrumentation - version: ${versionSuffix}`); return autoResource.merge(versionResource); } customizeResource(resource) { if ((0, utils_1.isAgentObservabilityEnabled)()) { // Add aws.service.type if it doesn't exist in the resource if (!resource.attributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_SERVICE_TYPE]) { // OTel 2.x: Resource attributes are immutable, so merge with a new resource const serviceTypeResource = (0, resources_1.resourceFromAttributes)({ [aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_SERVICE_TYPE]: 'gen_ai_agent', }); return resource.merge(serviceTypeResource); } } return resource; } configure() { // config.autoDetectResources is set to False, as the resources are detected and added to the // resource ahead of time. The resource is needed to be populated ahead of time instead of letting // the OTel Node SDK do the population work because the constructed resource was required to build // the sampler (if using XRay sampler) and the AwsMetricAttributesSpanExporter and AwsSpanMetricsProcessor const config = { instrumentations: this.instrumentations, resource: this.resource, idGenerator: this.idGenerator, sampler: this.sampler, // Error message 'Exporter "otlp" requested through environment variable is unavailable.' // will appear from BasicTracerProvider that is used in the OTel JS SDK, even though the // span processors are specified // https://github.com/open-telemetry/opentelemetry-js/issues/3449 spanProcessors: this.spanProcessors, logRecordProcessors: this.logRecordProcessors, autoDetectResources: false, textMapPropagator: this.propagator, }; if (this.metricReader) { config.metricReader = this.metricReader; } return config; } static isApplicationSignalsEnabled() { const isApplicationSignalsEnabled = process.env[APPLICATION_SIGNALS_ENABLED_CONFIG]; if (isApplicationSignalsEnabled === undefined) { return false; } return isApplicationSignalsEnabled.toLowerCase() === 'true'; } static geMetricExportInterval() { let exportIntervalMillis = Number(process.env[METRIC_EXPORT_INTERVAL_CONFIG]); api_1.diag.debug(`AWS Application Signals Metrics export interval: ${exportIntervalMillis}`); // Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly. if (isNaN(exportIntervalMillis) || exportIntervalMillis.valueOf() > DEFAULT_METRIC_EXPORT_INTERVAL_MILLIS) { exportIntervalMillis = DEFAULT_METRIC_EXPORT_INTERVAL_MILLIS; api_1.diag.info(`AWS Application Signals metrics export interval capped to ${exportIntervalMillis}`); } return exportIntervalMillis; } static exportUnsampledSpanForAgentObservability(spanProcessors, resource) { if (!(0, utils_1.isAgentObservabilityEnabled)()) { return; } let tracesEndpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; if (!tracesEndpoint && process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { tracesEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/+$/, '') + '/v1/traces'; } if (!tracesEndpoint) { api_1.diag.warn('No traces endpoint configured for agent observability unsampled spans'); return; } let spanExporter; // Create the appropriate span exporter based on the endpoint if (isAwsOtlpEndpoint(tracesEndpoint, 'xray')) { spanExporter = new otlp_aws_span_exporter_1.OTLPAwsSpanExporter(tracesEndpoint, undefined, api_logs_1.logs.getLoggerProvider()); } else { spanExporter = new otlp_aws_span_exporter_1.OTLPAwsSpanExporter(tracesEndpoint); } // Add the unsampled span processor spanProcessors.push(new aws_batch_unsampled_span_processor_1.AwsBatchUnsampledSpanProcessor(spanExporter)); } static customizeSpanProcessors(spanProcessors, resource) { const baggageKeys = (0, utils_1.parseOtelBaggageKeysEnvVar)(); if ((0, utils_1.isAgentObservabilityEnabled)()) { // We always send 100% spans to Bedrock AgentCore platform for agent observability because // AI applications typically have low throughput traffic patterns and require // comprehensive monitoring to catch subtle failure modes like hallucinations // and quality degradation that sampling could miss. this.exportUnsampledSpanForAgentObservability(spanProcessors, resource); spanProcessors.push(new gen_ai_nested_client_span_processor_1.GenAiNestedClientSpanProcessor()); // Add session.id baggage attribute to span attributes to support AI Agent use cases // enabling session ID tracking in spans. baggageKeys.add('session.id'); } spanProcessors.push(new baggage_span_processor_1.BaggageSpanProcessor((key) => baggageKeys.has(key))); if (!AwsOpentelemetryConfigurator.isApplicationSignalsEnabled()) { return; } api_1.diag.info('AWS Application Signals enabled.'); spanProcessors.push(attribute_propagating_span_processor_builder_1.AttributePropagatingSpanProcessorBuilder.create().build()); const applicationSignalsMetricExporter = ApplicationSignalsExporterProvider.Instance.createExporter(); const periodicExportingMetricReader = new sdk_metrics_1.PeriodicExportingMetricReader({ exporter: applicationSignalsMetricExporter, exportIntervalMillis: AwsOpentelemetryConfigurator.geMetricExportInterval(), }); // Register BatchUnsampledSpanProcessor to export unsampled traces in Lambda // when Application Signals enabled if (isLambdaEnvironment() && !hasCustomOtlpTraceEndpoint()) { const udpSpanExporter = new otlp_udp_exporter_1.OTLPUdpSpanExporter(getXrayDaemonEndpoint(), FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX); const configuredExporter = aws_metric_attributes_span_exporter_builder_1.AwsMetricAttributesSpanExporterBuilder.create(udpSpanExporter, resource).build(); spanProcessors.push(new aws_batch_unsampled_span_processor_1.AwsBatchUnsampledSpanProcessor(configuredExporter, { maxExportBatchSize: getSpanExportBatchSize(), })); api_1.diag.info('Enabled batch unsampled span processor for Lambda environment.'); } // Disable Application Metrics for Lambda environment if (!isLambdaEnvironment()) { const meterProvider = new sdk_metrics_1.MeterProvider({ /** Resource associated with metric telemetry */ resource: resource, readers: [periodicExportingMetricReader], }); spanProcessors.push(aws_span_metrics_processor_builder_1.AwsSpanMetricsProcessorBuilder.create(meterProvider, resource, meterProvider.forceFlush.bind(meterProvider)).build()); } } customizeMetricReader(isEmfEnabled) { let exporter = undefined; if (isEmfEnabled) { exporter = createEmfExporter(); } if (exporter) { const periodicExportingMetricReader = new sdk_metrics_1.PeriodicExportingMetricReader({ exporter: exporter, }); this.metricReader = periodicExportingMetricReader; } } static customizeSampler(sampler) { if (AwsOpentelemetryConfigurator.isApplicationSignalsEnabled()) { return always_record_sampler_1.AlwaysRecordSampler.create(sampler); } return sampler; } } exports.AwsOpentelemetryConfigurator = AwsOpentelemetryConfigurator; function customBuildSamplerFromEnv(resource, useXraySampler = false) { if (useXraySampler || process.env.OTEL_TRACES_SAMPLER === 'xray') { const samplerArgumentEnv = process.env.OTEL_TRACES_SAMPLER_ARG; let endpoint = undefined; let pollingInterval = undefined; if (samplerArgumentEnv !== undefined) { const args = samplerArgumentEnv.split(','); for (const arg of args) { const equalIndex = arg.indexOf('='); if (equalIndex === -1) { continue; } const keyValue = [arg.substring(0, equalIndex), arg.substring(equalIndex + 1)]; if (keyValue[0] === 'endpoint') { endpoint = keyValue[1]; } else if (keyValue[0] === 'polling_interval') { pollingInterval = Number(keyValue[1]); if (isNaN(pollingInterval)) { pollingInterval = undefined; api_1.diag.error('polling_interval in OTEL_TRACES_SAMPLER_ARG must be a valid number'); } } } } api_1.diag.info('AWS XRay Sampler enabled'); api_1.diag.debug(`XRay Sampler Endpoint: ${endpoint}`); api_1.diag.debug(`XRay Sampler Polling Interval: ${pollingInterval}`); return new aws_xray_remote_sampler_1.AwsXRayRemoteSampler({ resource: resource, endpoint: endpoint, pollingInterval: pollingInterval }); } return buildSamplerFromEnv(); } exports.customBuildSamplerFromEnv = customBuildSamplerFromEnv; class ApplicationSignalsExporterProvider { constructor() { this.aggregationSelector = (instrumentType) => { switch (instrumentType) { case sdk_metrics_1.InstrumentType.HISTOGRAM: { return { type: sdk_metrics_1.AggregationType.EXPONENTIAL_HISTOGRAM }; } } return { type: sdk_metrics_1.AggregationType.DEFAULT }; }; } static get Instance() { return this._instance || (this._instance = new this()); } createExporter() { let protocol = process.env['OTEL_EXPORTER_OTLP_METRICS_PROTOCOL']; if (protocol === undefined) { protocol = process.env['OTEL_EXPORTER_OTLP_PROTOCOL']; } if (protocol === undefined) { protocol = 'grpc'; } api_1.diag.debug(`AWS Application Signals export protocol: ${protocol}`); const temporalityPreference = exporter_metrics_otlp_http_1.AggregationTemporalityPreference.DELTA; const aggregationPreference = this.aggregationSelector; if (protocol === 'http/protobuf') { let applicationSignalsEndpoint = process.env[APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG]; if (applicationSignalsEndpoint === undefined) { applicationSignalsEndpoint = 'http://localhost:4316/v1/metrics'; } api_1.diag.debug(`AWS Application Signals export endpoint: ${applicationSignalsEndpoint}`); return new exporter_metrics_otlp_http_1.OTLPMetricExporter({ url: applicationSignalsEndpoint, temporalityPreference: temporalityPreference, aggregationPreference: aggregationPreference, }); } if (protocol === 'grpc') { let applicationSignalsEndpoint = process.env[APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG]; if (applicationSignalsEndpoint === undefined) { applicationSignalsEndpoint = 'http://localhost:4315'; } api_1.diag.debug(`AWS Application Signals export endpoint: ${applicationSignalsEndpoint}`); return new exporter_metrics_otlp_grpc_1.OTLPMetricExporter({ url: applicationSignalsEndpoint, temporalityPreference: temporalityPreference, aggregationPreference: aggregationPreference, }); } throw new Error(`Unsupported AWS Application Signals export protocol: ${protocol}`); } } exports.ApplicationSignalsExporterProvider = 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 // // Note: OTel 2.x provides getStringFromEnv and getStringListFromEnv natively from @opentelemetry/core. // class AwsLoggerProcessorProvider { static getlogRecordProcessors() { const exporters = AwsLoggerProcessorProvider.configureLogExportersFromEnv(); return exporters.map(exporter => { if (exporter instanceof sdk_logs_1.ConsoleLogRecordExporter) { return new sdk_logs_1.SimpleLogRecordProcessor(exporter); } else if (exporter instanceof otlp_aws_log_exporter_1.OTLPAwsLogExporter && (0, utils_1.isAgentObservabilityEnabled)()) { return new aws_cw_otlp_batch_log_record_processor_1.AwsCloudWatchOtlpBatchLogRecordProcessor(exporter); } return new sdk_logs_1.BatchLogRecordProcessor(exporter); }); } static configureLogExportersFromEnv() { var _b; const otlpExporterLogsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; const enabledExporters = (_b = (0, core_1.getStringListFromEnv)('OTEL_LOGS_EXPORTER')) !== null && _b !== void 0 ? _b : []; if (enabledExporters.length === 0) { api_1.diag.debug('OTEL_LOGS_EXPORTER is empty. Using default otlp exporter.'); enabledExporters.push('otlp'); } if (enabledExporters.includes('none')) { api_1.diag.info('OTEL_LOGS_EXPORTER contains "none". Logger provider will not be initialized.'); return []; } const exporters = []; enabledExporters.forEach(exporter => { var _b, _c; if (exporter === 'otlp') { const protocol = (_c = ((_b = (0, core_1.getStringFromEnv)('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL')) !== null && _b !== void 0 ? _b : (0, core_1.getStringFromEnv)('OTEL_EXPORTER_OTLP_PROTOCOL'))) === null || _c === void 0 ? void 0 : _c.trim(); switch (protocol) { case 'grpc': exporters.push(new exporter_logs_otlp_grpc_1.OTLPLogExporter()); break; case 'http/json': exporters.push(new exporter_logs_otlp_http_1.OTLPLogExporter()); break; case 'http/protobuf': { let logExporter = undefined; if (otlpExporterLogsEndpoint && isAwsOtlpEndpoint(otlpExporterLogsEndpoint, 'logs')) { api_1.diag.debug('Detected CloudWatch Logs OTLP endpoint. Switching exporter to OTLPAwsLogExporter'); if (validateAndFetchLogsHeader().isValid) { logExporter = new otlp_aws_log_exporter_1.OTLPAwsLogExporter(otlpExporterLogsEndpoint.toLowerCase(), { compression: otlp_exporter_base_1.CompressionAlgorithm.GZIP, }); } else { api_1.diag.warn(`Invalid configuration for OTLPAwsLogExporter, 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}. Falling back to OTLPProtoLogExporter`); } } if (!logExporter) { logExporter = new exporter_logs_otlp_proto_1.OTLPLogExporter(); } exporters.push(logExporter); break; } case undefined: case '': exporters.push(new exporter_logs_otlp_proto_1.OTLPLogExporter()); break; default: { api_1.diag.warn(`Unsupported OTLP logs protocol: "${protocol}". Using http/protobuf.`); let logExporter = undefined; if (otlpExporterLogsEndpoint && isAwsOtlpEndpoint(otlpExporterLogsEndpoint, 'logs')) { api_1.diag.debug('Detected CloudWatch Logs OTLP endpoint. Switching exporter to OTLPAwsLogExporter'); if (validateAndFetchLogsHeader().isValid) { logExporter = new otlp_aws_log_exporter_1.OTLPAwsLogExporter(otlpExporterLogsEndpoint.toLowerCase(), { compression: otlp_exporter_base_1.CompressionAlgorithm.GZIP, }); } else { api_1.diag.warn(`Invalid configuration for OTLPAwsLogExporter, 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}. Falling back to OTLPProtoLogExporter`); } } if (!logExporter) { logExporter = new exporter_logs_otlp_proto_1.OTLPLogExporter(); } exporters.push(logExporter); } } } else if (exporter === 'console') { let logExporter = undefined; if (isLambdaEnvironment()) { api_1.diag.debug('Lambda environment detected, using CompactConsoleLogRecordExporter instead of ConsoleLogRecordExporter'); logExporter = new compact_console_log_exporter_1.CompactConsoleLogRecordExporter(); } else { logExporter = new sdk_logs_1.ConsoleLogRecordExporter(); } exporters.push(logExporter); } else { api_1.diag.warn(`Unsupported OTEL_LOGS_EXPORTER value: "${exporter}". Supported values are: otlp, console, none.`); } }); return exporters; } } exports.AwsLoggerProcessorProvider = AwsLoggerProcessorProvider; // 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 // However, the logic to perform (1) is only in the `TracerProviderWithEnvExporters` class, which is not exported publicly. // `TracerProviderWithEnvExporters` is also responsible for (3) wrapping the SpanExporters inside the Simple/Batch SpanProcessors // which must happen after (2). Thus in order to perform (1), (2), and (3), we need to add these non-exported methods here. // // https://github.com/open-telemetry/opentelemetry-js/blob/01cea7caeb130142cc017f77ea74834a35d0e8d6/experimental/packages/opentelemetry-sdk-node/src/TracerProviderWithEnvExporter.ts // // This class is a modified version of TracerProviderWithEnvExporters (extends NodeTracerProvider), without // any of the TracerProvider functionalities. The AwsSpanProcessorProvider retains the functionality to // only create the default span processors with exporters specified in `OTEL_TRACES_EXPORTER`. These span // exporters are wrapped with AwsMetricAttributesSpanExporter when configuring the configureSpanProcessors // // Unlike `TracerProviderWithEnvExporters`, `AwsSpanProcessorProvider` does not extend `NodeTracerProvider`. // The following class member variables are unmodified: // - _configuredExporters // - _spanProcessors // The following class member variables are modified: // - _hasSpanProcessors (removed) // - resource (new) // The following methods are unmodified: // - configureOtlp(), getOtlpProtocol(), configureJaeger(), createExportersFromList(), _getSpanExporter(), filterBlanksAndNulls() // The following methods are modified: // - constructor() (modified) // - removed usage of `this.addSpanProcessor(...)`, which calls `super.addSpanProcessor(...)` // to register it to the BasicTracerProvider, which should be done later by the OTel JS SDK // - configureSpanProcessors(exporters) (modified) // - wrap exporters with customizeSpanExporter() // - customizeSpanExporter() (new) // - getSpanProcessors() (new) // - override addSpanProcessor() (removed) // - override register() (removed) // // TODO: `TracerProviderWithEnvExporters` is not exported, thus its useful static methods that // provides some default SpanExporter configurations are unavailable. Ideally, we could contribute // to upstream to export `TracerProviderWithEnvExporters` class AwsSpanProcessorProvider { static configureOtlp() { const otlpExporterTracesEndpoint = process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT']; // eslint-disable-next-line @typescript-eslint/typedef let protocol = this.getOtlpProtocol(); // If `isLambdaEnvironment` is true, we will default to exporting OTel spans via `udp_exporter` to Fluxpump, // regardless of whether `AppSignals` is true or false. // However, if the customer has explicitly set the `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, // we will continue using the `otlp_exporter` to send OTel traces to the specified endpoint. if (!hasCustomOtlpTraceEndpoint() && isLambdaEnvironment()) { protocol = 'udp'; } switch (protocol) { case 'grpc': return new exporter_trace_otlp_grpc_1.OTLPTraceExporter(); case 'http/json': return new exporter_trace_otlp_http_1.OTLPTraceExporter(); case 'http/protobuf': if (otlpExporterTracesEndpoint && isAwsOtlpEndpoint(otlpExporterTracesEndpoint, 'xray')) { api_1.diag.debug('Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter'); return new otlp_aws_span_exporter_1.OTLPAwsSpanExporter(otlpExporterTracesEndpoint.toLowerCase()); } return new exporter_trace_otlp_proto_1.OTLPTraceExporter(); case 'udp': api_1.diag.debug('Detected AWS Lambda environment and enabling UDPSpanExporter'); return new otlp_udp_exporter_1.OTLPUdpSpanExporter(getXrayDaemonEndpoint(), FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX); default: api_1.diag.warn(`Unsupported OTLP traces protocol: ${protocol}. Using http/protobuf.`); if (otlpExporterTracesEndpoint && isAwsOtlpEndpoint(otlpExporterTracesEndpoint, 'xray')) { api_1.diag.debug('Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter'); return new otlp_aws_span_exporter_1.OTLPAwsSpanExporter(otlpExporterTracesEndpoint.toLowerCase()); } return new exporter_trace_otlp_proto_1.OTLPTraceExporter(); } } // Aligned with upstream: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-sdk-node/src/utils.ts static getOtlpProtocol() { var _b, _c; return ((_c = (_b = (0, core_1.getStringFromEnv)('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL')) !== null && _b !== void 0 ? _b : (0, core_1.getStringFromEnv)('OTEL_EXPORTER_OTLP_PROTOCOL')) !== null && _c !== void 0 ? _c : 'http/protobuf'); } static configureJaeger() { // The JaegerExporter does not support being required in bundled // environments. By delaying the require statement to here, we only crash when // the exporter is actually used in such an environment. try { // eslint-disable-next-line @typescript-eslint/no-var-requires const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); return new JaegerExporter(); } catch (e) { throw new Error(`Could not instantiate JaegerExporter. This could be due to the JaegerExporter's lack of support for bundling. If possible, use @opentelemetry/exporter-trace-otlp-proto instead. Original Error: ${e}`); } } constructor(resource) { var _b; this._configuredExporters = []; this._spanProcessors = []; this.resource = resource; // Aligned with upstream: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-sdk-node/src/utils.ts let traceExportersList = this.filterBlanksAndNulls(Array.from(new Set((_b = (0, core_1.getStringListFromEnv)('OTEL_TRACES_EXPORTER')) !== null && _b !== void 0 ? _b : []))); if (traceExportersList[0] === 'none') { api_1.diag.warn('OTEL_TRACES_EXPORTER contains "none". SDK will not be initialized.'); } else if (traceExportersList.length === 0) { api_1.diag.warn('OTEL_TRACES_EXPORTER is empty. Using default otlp exporter.'); traceExportersList = ['otlp']; this.createExportersFromList(traceExportersList); this._spanProcessors = this.configureSpanProcessors(this._configuredExporters); } else { if (traceExportersList.length > 1 && traceExportersList.includes('none')) { api_1.diag.warn('OTEL_TRACES_EXPORTER contains "none" along with other exporters. Using default otlp exporter.'); traceExportersList = ['otlp']; } this.createExportersFromList(traceExportersList); if (this._configuredExporters.length > 0) { this._spanProcessors = this.configureSpanProcessors(this._configuredExporters); } else { api_1.diag.warn('Unable to set up trace exporter(s) due to invalid exporter and/or protocol values.'); } } } createExportersFromList(exporterList) { exporterList.forEach(exporterName => { // eslint-disable-next-line @typescript-eslint/typedef const exporter = this._getSpanExporter(exporterName); if (exporter) { this._configuredExporters.push(exporter); } else { api_1.diag.warn(`Unrecognized OTEL_TRACES_EXPORTER value: ${exporterName}.`); } }); } _getSpanExporter(name) { var _b; return (_b = AwsSpanProcessorProvider._registeredExporters.get(name)) === null || _b === void 0 ? void 0 : _b(); } configureSpanProcessors(exporters) { return exporters.map(exporter => { const configuredExporter = AwsSpanProcessorProvider.customizeSpanExporter(exporter, this.resource); if (exporter instanceof sdk_trace_base_1.ConsoleSpanExporter) { return new sdk_trace_base_1.SimpleSpanProcessor(configuredExporter); } else { return new sdk_trace_base_1.BatchSpanProcessor(configuredExporter, { maxExportBatchSize: getSpanExportBatchSize(), }); } }); } filterBlanksAndNulls(list) { return list.map(item => item.trim()).filter(s => s !== 'null' && s !== ''); } static customizeSpanExporter(spanExporter, resource) { if (AwsOpentelemetryConfigurator.isApplicationSignalsEnabled()) { return aws_metric_attributes_span_exporter_builder_1.AwsMetricAttributesSpanExporterBuilder.create(spanExporter, resource).build(); } return spanExporter; } getSpanProcessors() { return this._spanProcessors; } } _a = AwsSpanProcessorProvider; AwsSpanProcessorProvider._registeredExporters = new Map([ ['otlp', () => _a.configureOtlp()], ['zipkin', () => new exporter_zipkin_1.ZipkinExporter()], ['jaeger', () => _a.configureJaeger()], ['console', () => new sdk_trace_base_1.ConsoleSpanExporter()], ]); exports.AwsSpanProcessorProvider = AwsSpanProcessorProvider; // END The OpenTelemetry Authors code // The OpenTelemetry Authors code // // We need the logic to build the Sampler from user-defined Environment variables in order // to wrap the Sampler with an AlwaysRecord sampler. However, this logic is not exported // in an `index.ts` file, so the portion of code that does this is added here. // // TODO: Ideally, upstream's `buildSamplerFromEnv()` method should be exported // https://github.com/open-telemetry/opentelemetry-js/blob/f047db9da20a7d4394169f812b2d255d934883f1/packages/opentelemetry-sdk-trace-base/src/config.ts#L62 // // An alternative method is to instantiate a new OTel JS Tracer with an empty config, which // would also have the (private+readonly) sampler from the `buildSamplerFromEnv()` method. // https://github.com/open-telemetry/opentelemetry-js/blob/01cea7caeb130142cc017f77ea74834a35d0e8d6/packages/opentelemetry-sdk-trace-base/src/Tracer.ts#L36-L53 // Sampler values copied from upstream (const enum, not importable at runtime): // https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/config.ts const SamplerValues = { AlwaysOn: 'always_on', AlwaysOff: 'always_off', ParentBasedAlwaysOn: 'parentbased_always_on', ParentBasedAlwaysOff: 'parentbased_always_off', TraceIdRatio: 'traceidratio', ParentBasedTraceIdRatio: 'parentbased_traceidratio', }; const FALLBACK_OTEL_TRACES_SAMPLER = SamplerValues.AlwaysOn; const DEFAULT_RATIO = 1; /** * Based on environment, builds a sampler, complies with specification. * Uses the new OTel 2.x getStringFromEnv API instead of deprecated getEnv(). */ function buildSamplerFromEnv() { var _b; const samplerName = (_b = (0, core_1.getStringFromEnv)('OTEL_TRACES_SAMPLER')) !== null && _b !== void 0 ? _b : SamplerValues.ParentBasedAlwaysOn; switch (samplerName) { case SamplerValues.AlwaysOn: return new sdk_trace_base_1.AlwaysOnSampler(); case SamplerValues.AlwaysOff: return new sdk_trace_base_1.AlwaysOffSampler(); case SamplerValues.ParentBasedAlwaysOn: return new sdk_trace_base_1.ParentBasedSampler({ root: new sdk_trace_base_1.AlwaysOnSampler(), }); case SamplerValues.ParentBasedAlwaysOff: return new sdk_trace_base_1.ParentBasedSampler({ root: new sdk_trace_base_1.AlwaysOffSampler(), }); case SamplerValues.TraceIdRatio: return new sdk_trace_base_1.TraceIdRatioBasedSampler(getSamplerProbabilityFromEnv()); case SamplerValues.ParentBasedTraceIdRatio: return new sdk_trace_base_1.ParentBasedSampler({ root: new sdk_trace_base_1.TraceIdRatioBasedSampler(getSamplerProbabilityFromEnv()), }); default: api_1.diag.error(`OTEL_TRACES_SAMPLER value "${samplerName}" invalid, defaulting to ${FALLBACK_OTEL_TRACES_SAMPLER}.`); return new sdk_trace_base_1.AlwaysOnSampler(); } } exports.buildSamplerFromEnv = buildSamplerFromEnv; function getSamplerProbabilityFromEnv() { const samplerArg = (0, core_1.getStringFromEnv)('OTEL_TRACES_SAMPLER_ARG'); if (samplerArg === undefined || samplerArg === '') { api_1.diag.error(`OTEL_TRACES_SAMPLER_ARG is blank, defaulting to ${DEFAULT_RATIO}.`); return DEFAULT_RATIO; } // eslint-disable-next-line @typescript-eslint/typedef const probability = Number(samplerArg); if (isNaN(probability)) { api_1.diag.error(`OTEL_TRACES_SAMPLER_ARG=${samplerArg} was given, but it is invalid, defaulting to ${DEFAULT_RATIO}.`); return DEFAULT_RATIO; } if (probability < 0 || probability > 1) { api_1.diag.error(`OTEL_TRACES_SAMPLER_ARG=${samplerArg} was given, but it is out of range ([0..1]), defaulting to ${DEFAULT_RATIO}.`); return DEFAULT_RATIO; } return probability; } // END The OpenTelemetry Authors code function getSpanExportBatchSize() { if (isLambdaEnvironment()) { return LAMBDA_SPAN_EXPORT_BATCH_SIZE; } return undefined; } function isLambdaEnvironment() { // detect if running in AWS Lambda environment return process.env[exports.AWS_LAMBDA_FUNCTION_NAME_CONFIG] !== undefined; } exports.isLambdaEnvironment = isLambdaEnvironment; function hasCustomOtlpTraceEndpoint() { return process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] !== undefined; } function getXrayDaemonEndpoint() { return process.env[AWS_XRAY_DAEMON_ADDRESS_CONFIG]; } /** * Determines if the given endpoint is either the AWS OTLP Traces or Logs endpoint. */ function isAwsOtlpEndpoint(otlpEndpoint, service) { 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()); } exports.isAwsOtlpEndpoint = isAwsOtlpEndpoint; /** * 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 validateAndFetchLogsHeader() { const logHeaders = process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS; if (!logHeaders) { if (!isLambdaEnvironment()) { api_1.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 { logGroup: undefined, logStream: undefined, namespace: undefined, isValid: false, }; } let logGroup = undefined; let logStream = undefined; let namespace = undefined; for (const pair of logHeaders.split(',')) { const splitIndex = pair.indexOf('='); if (splitIndex > -1) { const key = pair.substring(0, splitIndex); const value = pair.substring(splitIndex + 1); if (key === AWS_OTLP_LOGS_GROUP_HEADER && value) { logGroup = value; } else if (key === AWS_OTLP_LOGS_STREAM_HEADER && value) { logStream = value; } else if (key === AWS_EMF_METRICS_NAMESPACE && value) { namespace = value; } } } const isValid = !!logGroup && !!logStream; return { logGroup: logGroup, logStream: logStream, namespace: namespace, isValid: isValid, }; } exports.validateAndFetchLogsHeader = validateAndFetchLogsHeader; function checkEmfExporterEnabled() { const exporterValue = process.env.OTEL_METRICS_EXPORTER; if (exporterValue === undefined) { return false; } const exporters = exporterValue.split(',').map(exporter => exporter.trim()); const index = exporters.indexOf('awsemf'); if (index === -1) { return false; } exporters.splice(index, 1); const newValue = exporters ? exporters.join(',') : undefined; if (typeof newValue === 'string' && newValue !== '') { process.env.OTEL_METRICS_EXPORTER = newValue; } else { delete process.env.OTEL_METRICS_EXPORTER; } return true; } exports.checkEmfExporterEnabled = checkEmfExporterEnabled; /** * Create the appropriate EMF exporter based on the environment and configuration. * * @returns {EMFExporterBase | undefined} */ function createEmfExporter() { let exporter = undefined; const otlpLogHeaderSetting = validateAndFetchLogsHeader(); if (isLambdaEnvironment() && !otlpLogHeaderSetting.isValid) { // Lambda without valid logs http headers - use Console EMF exporter exporter = new console_emf_exporter_1.ConsoleEMFExporter(otlpLogHeaderSetting.namespace); } else if (otlpLogHeaderSetting.isValid) { // Non-Lambda environment - use CloudWatch EMF exporter // If headersResult.isValid is true, then headersResult.logGroup and headersResult.logStream are guaranteed to be strings exporter = new aws_cloudwatch_emf_exporter_1.AWSCloudWatchEMFExporter(otlpLogHeaderSetting.namespace, otlpLogHeaderSetting.logGroup, otlpLogHeaderSetting.logStream); } return exporter; } exports.createEmfExporter = createEmfExporter; //# sourceMappingURL=aws-opentelemetry-configurator.js.map