@@ -14,6 +14,9 @@ import {
1414import { OTLPTraceExporter as OTLPGrpcTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc' ;
1515import { OTLPTraceExporter as OTLPHttpTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' ;
1616import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' ;
17+ import { OTLPLogExporter as OTLPGrpcLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc' ;
18+ import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http' ;
19+ import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto' ;
1720import { ZipkinExporter } from '@opentelemetry/exporter-zipkin' ;
1821import { AWSXRayIdGenerator } from '@opentelemetry/id-generator-aws-xray' ;
1922import { Instrumentation } from '@opentelemetry/instrumentation' ;
@@ -50,6 +53,14 @@ import {
5053 SpanProcessor ,
5154 TraceIdRatioBasedSampler ,
5255} from '@opentelemetry/sdk-trace-base' ;
56+
57+ import {
58+ BatchLogRecordProcessor ,
59+ ConsoleLogRecordExporter ,
60+ LogRecordExporter ,
61+ LogRecordProcessor ,
62+ SimpleLogRecordProcessor ,
63+ } from '@opentelemetry/sdk-logs' ;
5364import { SEMRESATTRS_TELEMETRY_AUTO_VERSION } from '@opentelemetry/semantic-conventions' ;
5465import { AlwaysRecordSampler } from './always-record-sampler' ;
5566import { AttributePropagatingSpanProcessorBuilder } from './attribute-propagating-span-processor-builder' ;
@@ -61,14 +72,20 @@ import { OTLPUdpSpanExporter } from './otlp-udp-exporter';
6172import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler' ;
6273// This file is generated via `npm run compile`
6374import { LIB_VERSION } from './version' ;
75+ import { OTLPAwsLogExporter } from './exporter/otlp/aws/logs/otlp-aws-log-exporter' ;
6476
65- const XRAY_OTLP_ENDPOINT_PATTERN = '^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$' ;
77+ const AWS_TRACES_OTLP_ENDPOINT_PATTERN = '^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$' ;
78+ const AWS_LOGS_OTLP_ENDPOINT_PATTERN = '^https://logs\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/logs$' ;
79+
80+ const AWS_OTLP_LOGS_GROUP_HEADER = 'x-aws-log-group' ;
81+ const AWS_OTLP_LOGS_STREAM_HEADER = 'x-aws-log-stream' ;
6682
6783const APPLICATION_SIGNALS_ENABLED_CONFIG : string = 'OTEL_AWS_APPLICATION_SIGNALS_ENABLED' ;
6884const APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG : string = 'OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT' ;
6985const METRIC_EXPORT_INTERVAL_CONFIG : string = 'OTEL_METRIC_EXPORT_INTERVAL' ;
7086const DEFAULT_METRIC_EXPORT_INTERVAL_MILLIS : number = 60000 ;
7187export const AWS_LAMBDA_FUNCTION_NAME_CONFIG : string = 'AWS_LAMBDA_FUNCTION_NAME' ;
88+ export const AGENT_OBSERVABILITY_ENABLED = 'AGENT_OBSERVABILITY_ENABLED' ;
7289const AWS_XRAY_DAEMON_ADDRESS_CONFIG : string = 'AWS_XRAY_DAEMON_ADDRESS' ;
7390const FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = 'T1S' ;
7491const FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = 'T1U' ;
@@ -95,6 +112,7 @@ export class AwsOpentelemetryConfigurator {
95112 private idGenerator : IdGenerator ;
96113 private sampler : Sampler ;
97114 private spanProcessors : SpanProcessor [ ] ;
115+ private logRecordProcessors : LogRecordProcessor [ ] ;
98116 private propagator : TextMapPropagator ;
99117
100118 /**
@@ -178,6 +196,7 @@ export class AwsOpentelemetryConfigurator {
178196 // default SpanProcessors with Span Exporters wrapped inside AwsMetricAttributesSpanExporter
179197 const awsSpanProcessorProvider : AwsSpanProcessorProvider = new AwsSpanProcessorProvider ( this . resource ) ;
180198 this . spanProcessors = awsSpanProcessorProvider . getSpanProcessors ( ) ;
199+ this . logRecordProcessors = AwsLoggerProcessorProvider . getlogRecordProcessors ( ) ;
181200 AwsOpentelemetryConfigurator . customizeSpanProcessors ( this . spanProcessors , this . resource ) ;
182201 }
183202
@@ -206,6 +225,7 @@ export class AwsOpentelemetryConfigurator {
206225 // span processors are specified
207226 // https://github.com/open-telemetry/opentelemetry-js/issues/3449
208227 spanProcessors : this . spanProcessors ,
228+ logRecordProcessors : this . logRecordProcessors ,
209229 autoDetectResources : false ,
210230 textMapPropagator : this . propagator ,
211231 } ;
@@ -384,6 +404,142 @@ export class ApplicationSignalsExporterProvider {
384404 } ;
385405}
386406
407+ // The OpenTelemetry Authors code
408+ // 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
409+ // 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,
410+ // 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.
411+ // Long term, we want to contribute these changes to upstream.
412+ //
413+ // https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-sdk-node/src/sdk.ts#L443
414+ //
415+ // The upstream OpenTelemetry SDK has changed its API by deprecating `getEnv()` and
416+ // `getEnvWithoutDefaults()` in favor of specific methods like `getStringListFromEnv`
417+ // and `getStringFromEnv`. Since these newer methods aren't available in our current
418+ // supported version, we've also needed to copy them down here.
419+ //
420+ // https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-core/src/platform/node/environment.ts#L52
421+ // https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-core/src/platform/node/environment.ts#L100
422+ //
423+ // TODO: Remove getStringListFromEnv and getStringFromEnv implementations
424+ // once we upgrade to @opentelemetry /core 2.0.0 or higher, which provides these methods natively.
425+ //
426+ export class AwsLoggerProcessorProvider {
427+ public static getlogRecordProcessors ( ) : LogRecordProcessor [ ] {
428+ const exporters = AwsLoggerProcessorProvider . configureLogExportersFromEnv ( ) ;
429+
430+ return exporters . map ( exporter => {
431+ if ( exporter instanceof ConsoleLogRecordExporter ) {
432+ return new SimpleLogRecordProcessor ( exporter ) ;
433+ } else {
434+ return new BatchLogRecordProcessor ( exporter ) ;
435+ }
436+ } ) ;
437+ }
438+
439+ static configureLogExportersFromEnv ( ) : LogRecordExporter [ ] {
440+ const otlpExporterLogsEndpoint = process . env . OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ;
441+ const enabledExporters = AwsLoggerProcessorProvider . getStringListFromEnv ( 'OTEL_LOGS_EXPORTER' ) ?? [ ] ;
442+
443+ if ( enabledExporters . length === 0 ) {
444+ diag . debug ( 'OTEL_LOGS_EXPORTER is empty. Using default otlp exporter.' ) ;
445+ enabledExporters . push ( 'otlp' ) ;
446+ }
447+
448+ if ( enabledExporters . includes ( 'none' ) ) {
449+ diag . info ( 'OTEL_LOGS_EXPORTER contains "none". Logger provider will not be initialized.' ) ;
450+ return [ ] ;
451+ }
452+
453+ const exporters : LogRecordExporter [ ] = [ ] ;
454+
455+ enabledExporters . forEach ( exporter => {
456+ if ( exporter === 'otlp' ) {
457+ const protocol = (
458+ AwsLoggerProcessorProvider . getStringFromEnv ( 'OTEL_EXPORTER_OTLP_LOGS_PROTOCOL' ) ??
459+ AwsLoggerProcessorProvider . getStringFromEnv ( 'OTEL_EXPORTER_OTLP_PROTOCOL' )
460+ ) ?. trim ( ) ;
461+
462+ switch ( protocol ) {
463+ case 'grpc' :
464+ exporters . push ( new OTLPGrpcLogExporter ( ) ) ;
465+ break ;
466+ case 'http/json' :
467+ exporters . push ( new OTLPHttpLogExporter ( ) ) ;
468+ break ;
469+ case 'http/protobuf' :
470+ if (
471+ otlpExporterLogsEndpoint &&
472+ isAwsOtlpEndpoint ( otlpExporterLogsEndpoint , 'logs' ) &&
473+ validateLogsHeaders ( )
474+ ) {
475+ diag . debug ( 'Detected CloudWatch Logs OTLP endpoint. Switching exporter to OTLPAwsLogExporter' ) ;
476+ exporters . push ( new OTLPAwsLogExporter ( otlpExporterLogsEndpoint ) ) ;
477+ } else {
478+ exporters . push ( new OTLPProtoLogExporter ( ) ) ;
479+ }
480+ break ;
481+ case undefined :
482+ case '' :
483+ exporters . push ( new OTLPProtoLogExporter ( ) ) ;
484+ break ;
485+ default :
486+ diag . warn ( `Unsupported OTLP logs protocol: "${ protocol } ". Using http/protobuf.` ) ;
487+ if (
488+ otlpExporterLogsEndpoint &&
489+ isAwsOtlpEndpoint ( otlpExporterLogsEndpoint , 'logs' ) &&
490+ validateLogsHeaders ( )
491+ ) {
492+ diag . debug ( 'Detected CloudWatch Logs OTLP endpoint. Switching exporter to OTLPAwsLogExporter' ) ;
493+ exporters . push ( new OTLPAwsLogExporter ( otlpExporterLogsEndpoint ) ) ;
494+ } else {
495+ exporters . push ( new OTLPProtoLogExporter ( ) ) ;
496+ }
497+ }
498+ } else if ( exporter === 'console' ) {
499+ exporters . push ( new ConsoleLogRecordExporter ( ) ) ;
500+ } else {
501+ diag . warn ( `Unsupported OTEL_LOGS_EXPORTER value: "${ exporter } ". Supported values are: otlp, console, none.` ) ;
502+ }
503+ } ) ;
504+
505+ return exporters ;
506+ }
507+
508+ /**
509+ * Retrieves a list of strings from an environment variable.
510+ * - Uses ',' as the delimiter.
511+ * - Trims leading and trailing whitespace from each entry.
512+ * - Excludes empty entries.
513+ * - Returns `undefined` if the environment variable is empty or contains only whitespace.
514+ * - Returns an empty array if all entries are empty or whitespace.
515+ *
516+ * @param {string } key - The name of the environment variable to retrieve.
517+ * @returns {string[] | undefined } - The list of strings or `undefined`.
518+ */
519+ private static getStringListFromEnv ( key : string ) : string [ ] | undefined {
520+ return AwsLoggerProcessorProvider . getStringFromEnv ( key )
521+ ?. split ( ',' )
522+ . map ( v => v . trim ( ) )
523+ . filter ( s => s !== '' ) ;
524+ }
525+
526+ /**
527+ * Retrieves a string from an environment variable.
528+ * - Returns `undefined` if the environment variable is empty, unset, or contains only whitespace.
529+ *
530+ * @param {string } key - The name of the environment variable to retrieve.
531+ * @returns {string | undefined } - The string value or `undefined`.
532+ */
533+ private static getStringFromEnv ( key : string ) : string | undefined {
534+ const raw = process . env [ key ] ;
535+ if ( raw == null || raw . trim ( ) === '' ) {
536+ return undefined ;
537+ }
538+ return raw ;
539+ }
540+ }
541+ // END The OpenTelemetry Authors code
542+
387543// The OpenTelemetry Authors code
388544//
389545// ADOT JS needs the logic to (1) get the SpanExporters from Env and then (2) wrap the SpanExporters with AwsMetricAttributesSpanExporter
@@ -427,7 +583,7 @@ export class AwsSpanProcessorProvider {
427583 private resource : Resource ;
428584
429585 static configureOtlp ( ) : SpanExporter {
430- const otlp_exporter_traces_endpoint = process . env [ 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' ] ;
586+ const otlpExporterTracesEndpoint = process . env [ 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' ] ;
431587 // eslint-disable-next-line @typescript-eslint/typedef
432588 let protocol = this . getOtlpProtocol ( ) ;
433589
@@ -444,19 +600,19 @@ export class AwsSpanProcessorProvider {
444600 case 'http/json' :
445601 return new OTLPHttpTraceExporter ( ) ;
446602 case 'http/protobuf' :
447- if ( otlp_exporter_traces_endpoint && isXrayOtlpEndpoint ( otlp_exporter_traces_endpoint ) ) {
603+ if ( otlpExporterTracesEndpoint && isAwsOtlpEndpoint ( otlpExporterTracesEndpoint , 'xray' ) ) {
448604 diag . debug ( 'Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter' ) ;
449- return new OTLPAwsSpanExporter ( otlp_exporter_traces_endpoint ) ;
605+ return new OTLPAwsSpanExporter ( otlpExporterTracesEndpoint ) ;
450606 }
451607 return new OTLPProtoTraceExporter ( ) ;
452608 case 'udp' :
453609 diag . debug ( 'Detected AWS Lambda environment and enabling UDPSpanExporter' ) ;
454610 return new OTLPUdpSpanExporter ( getXrayDaemonEndpoint ( ) , FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX ) ;
455611 default :
456612 diag . warn ( `Unsupported OTLP traces protocol: ${ protocol } . Using http/protobuf.` ) ;
457- if ( otlp_exporter_traces_endpoint && isXrayOtlpEndpoint ( otlp_exporter_traces_endpoint ) ) {
613+ if ( otlpExporterTracesEndpoint && isAwsOtlpEndpoint ( otlpExporterTracesEndpoint , 'xray' ) ) {
458614 diag . debug ( 'Detected XRay OTLP Traces endpoint. Switching exporter to OtlpAwsSpanExporter' ) ;
459- return new OTLPAwsSpanExporter ( otlp_exporter_traces_endpoint ) ;
615+ return new OTLPAwsSpanExporter ( otlpExporterTracesEndpoint ) ;
460616 }
461617 return new OTLPProtoTraceExporter ( ) ;
462618 }
@@ -666,8 +822,51 @@ function getXrayDaemonEndpoint() {
666822 return process . env [ AWS_XRAY_DAEMON_ADDRESS_CONFIG ] ;
667823}
668824
669- function isXrayOtlpEndpoint ( otlpEndpoint : string | undefined ) {
670- return otlpEndpoint && new RegExp ( XRAY_OTLP_ENDPOINT_PATTERN ) . test ( otlpEndpoint . toLowerCase ( ) ) ;
825+ /**
826+ * Determines if the given endpoint is either the AWS OTLP Traces or Logs endpoint.
827+ */
828+
829+ function isAwsOtlpEndpoint ( otlpEndpoint : string , service : string ) : boolean {
830+ const pattern = service === 'xray' ? AWS_TRACES_OTLP_ENDPOINT_PATTERN : AWS_LOGS_OTLP_ENDPOINT_PATTERN ;
831+
832+ return new RegExp ( pattern ) . test ( otlpEndpoint . toLowerCase ( ) ) ;
833+ }
834+
835+ /**
836+ * Checks if x-aws-log-group and x-aws-log-stream are present in the headers in order to send logs to
837+ * AWS OTLP Logs endpoint.
838+ */
839+ function validateLogsHeaders ( ) {
840+ const logsHeaders = process . env [ 'OTEL_EXPORTER_OTLP_LOGS_HEADERS' ] ;
841+
842+ if ( ! logsHeaders ) {
843+ diag . warn (
844+ 'Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS ' +
845+ 'to include x-aws-log-group and x-aws-log-stream'
846+ ) ;
847+ return false ;
848+ }
849+
850+ let filteredLogHeadersCount = 0 ;
851+
852+ for ( const pair of logsHeaders . split ( ',' ) ) {
853+ if ( pair . includes ( '=' ) ) {
854+ const [ key , value ] = pair . split ( '=' , 2 ) ;
855+ if ( ( key === AWS_OTLP_LOGS_GROUP_HEADER || key === AWS_OTLP_LOGS_STREAM_HEADER ) && value ) {
856+ filteredLogHeadersCount += 1 ;
857+ }
858+ }
859+ }
860+
861+ if ( filteredLogHeadersCount !== 2 ) {
862+ diag . warn (
863+ 'Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS ' +
864+ 'to have values for x-aws-log-group and x-aws-log-stream'
865+ ) ;
866+ return false ;
867+ }
868+
869+ return true ;
671870}
672871
673872// END The OpenTelemetry Authors code
0 commit comments