Skip to content

Commit 6b3ceb1

Browse files
committed
sigv4 authentication support for otlp logs exporter
1 parent 6d12166 commit 6b3ceb1

File tree

14 files changed

+1007
-420
lines changed

14 files changed

+1007
-420
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
],
4242
"exclude": [
4343
"src/third-party/**/*.ts",
44-
"src/otlp-aws-span-exporter.ts"
44+
"src/exporter/otlp/aws/common/aws-authenticator.ts"
4545
]
4646
},
4747
"bugs": {
@@ -104,6 +104,8 @@
104104
"@opentelemetry/exporter-metrics-otlp-grpc": "0.57.1",
105105
"@opentelemetry/exporter-metrics-otlp-http": "0.57.1",
106106
"@opentelemetry/exporter-trace-otlp-proto": "0.57.1",
107+
"@opentelemetry/exporter-logs-otlp-grpc": "0.57.1",
108+
"@opentelemetry/exporter-logs-otlp-http": "0.57.1",
107109
"@opentelemetry/exporter-logs-otlp-proto": "0.57.1",
108110
"@opentelemetry/exporter-zipkin": "1.30.1",
109111
"@opentelemetry/id-generator-aws-xray": "1.2.3",
@@ -116,6 +118,7 @@
116118
"@opentelemetry/sdk-metrics": "1.30.1",
117119
"@opentelemetry/sdk-node": "0.57.1",
118120
"@opentelemetry/sdk-trace-base": "1.30.1",
121+
"@opentelemetry/sdk-logs": "0.57.1",
119122
"@opentelemetry/semantic-conventions": "1.28.0"
120123
},
121124
"files": [

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts

Lines changed: 207 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
import { OTLPTraceExporter as OTLPGrpcTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
1515
import { OTLPTraceExporter as OTLPHttpTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
1616
import { 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';
1720
import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';
1821
import { AWSXRayIdGenerator } from '@opentelemetry/id-generator-aws-xray';
1922
import { 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';
5364
import { SEMRESATTRS_TELEMETRY_AUTO_VERSION } from '@opentelemetry/semantic-conventions';
5465
import { AlwaysRecordSampler } from './always-record-sampler';
5566
import { AttributePropagatingSpanProcessorBuilder } from './attribute-propagating-span-processor-builder';
@@ -61,14 +72,20 @@ import { OTLPUdpSpanExporter } from './otlp-udp-exporter';
6172
import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler';
6273
// This file is generated via `npm run compile`
6374
import { 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

6783
const APPLICATION_SIGNALS_ENABLED_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_ENABLED';
6884
const APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT';
6985
const METRIC_EXPORT_INTERVAL_CONFIG: string = 'OTEL_METRIC_EXPORT_INTERVAL';
7086
const DEFAULT_METRIC_EXPORT_INTERVAL_MILLIS: number = 60000;
7187
export const AWS_LAMBDA_FUNCTION_NAME_CONFIG: string = 'AWS_LAMBDA_FUNCTION_NAME';
88+
export const AGENT_OBSERVABILITY_ENABLED = 'AGENT_OBSERVABILITY_ENABLED';
7289
const AWS_XRAY_DAEMON_ADDRESS_CONFIG: string = 'AWS_XRAY_DAEMON_ADDRESS';
7390
const FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = 'T1S';
7491
const 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

Comments
 (0)