@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
JavaScript
;
// 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