Skip to content

Commit 5e967ac

Browse files
authored
Optimize Instrumentation Configure for Lambda cases (#68)
*Description of changes:* Update Instrumentation Configure for the following changes 1. Disable `AwsSpanMetricsProcessor` for Lambda 2. Disable AWS platform Resource Detectors for Lambda 3. Update Lambda Instrumentation propagation for supporting PassThru case 4. Update Lambda Local Operation to `{function_name}/Hander` 5. Set Batch Sampled/UnSampled Span processor size to 10 for Lambda only By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 93fffad commit 5e967ac

File tree

10 files changed

+336
-238
lines changed

10 files changed

+336
-238
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-batch-unsampled-span-processor.ts

Lines changed: 8 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,10 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
44

5-
import { context, Context, diag, TraceFlags } from '@opentelemetry/api';
6-
import {
7-
BindOnceFuture,
8-
ExportResultCode,
9-
getEnv,
10-
globalErrorHandler,
11-
suppressTracing,
12-
unrefTimer,
13-
} from '@opentelemetry/core';
14-
import { BufferConfig, ReadableSpan, Span, SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base';
5+
import { Context, TraceFlags } from '@opentelemetry/api';
6+
import { ReadableSpan, BufferConfig, Span } from '@opentelemetry/sdk-trace-base';
157
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
8+
import { BatchSpanProcessorBase } from '@opentelemetry/sdk-trace-base/build/src/export/BatchSpanProcessorBase';
169

1710
/**
1811
* This class is a customized version of the `BatchSpanProcessorBase` from the
@@ -41,211 +34,24 @@ import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
4134
* The rest of the behavior—batch processing, queuing, and exporting spans in
4235
* batches—is inherited from the base class and remains largely the same.
4336
*/
44-
export class AwsBatchUnsampledSpanProcessor implements SpanProcessor {
45-
private readonly _maxExportBatchSize: number;
46-
private readonly _maxQueueSize: number;
47-
private readonly _scheduledDelayMillis: number;
48-
private readonly _exportTimeoutMillis: number;
49-
50-
private _isExporting: boolean = false;
51-
private _finishedSpans: ReadableSpan[] = [];
52-
private _timer: NodeJS.Timeout | undefined;
53-
private _shutdownOnce: BindOnceFuture<void>;
54-
private _droppedSpansCount: number = 0;
55-
56-
constructor(private readonly _exporter: SpanExporter, config?: BufferConfig) {
57-
const env = getEnv();
58-
this._maxExportBatchSize =
59-
typeof config?.maxExportBatchSize === 'number' ? config.maxExportBatchSize : env.OTEL_BSP_MAX_EXPORT_BATCH_SIZE;
60-
this._maxQueueSize = typeof config?.maxQueueSize === 'number' ? config.maxQueueSize : env.OTEL_BSP_MAX_QUEUE_SIZE;
61-
this._scheduledDelayMillis =
62-
typeof config?.scheduledDelayMillis === 'number' ? config.scheduledDelayMillis : env.OTEL_BSP_SCHEDULE_DELAY;
63-
this._exportTimeoutMillis =
64-
typeof config?.exportTimeoutMillis === 'number' ? config.exportTimeoutMillis : env.OTEL_BSP_EXPORT_TIMEOUT;
65-
66-
this._shutdownOnce = new BindOnceFuture(this._shutdown, this);
67-
68-
if (this._maxExportBatchSize > this._maxQueueSize) {
69-
diag.warn(
70-
'BatchSpanProcessor: maxExportBatchSize must be smaller or equal to maxQueueSize, setting maxExportBatchSize to match maxQueueSize'
71-
);
72-
this._maxExportBatchSize = this._maxQueueSize;
73-
}
74-
}
75-
76-
forceFlush(): Promise<void> {
77-
if (this._shutdownOnce.isCalled) {
78-
return this._shutdownOnce.promise;
79-
}
80-
return this._flushAll();
81-
}
82-
83-
onStart(span: Span, _parentContext: Context): void {
37+
export class AwsBatchUnsampledSpanProcessor extends BatchSpanProcessorBase<BufferConfig> {
38+
override onStart(span: Span, _parentContext: Context): void {
8439
if ((span.spanContext().traceFlags & TraceFlags.SAMPLED) === 0) {
8540
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_TRACE_FLAG_UNSAMPLED, true);
8641
return;
8742
}
8843
}
8944

90-
onEnd(span: ReadableSpan): void {
91-
if (this._shutdownOnce.isCalled) {
45+
override onEnd(span: ReadableSpan): void {
46+
if ((this as any)._shutdownOnce.isCalled) {
9247
return;
9348
}
9449

9550
if ((span.spanContext().traceFlags & TraceFlags.SAMPLED) === 1) {
9651
return;
9752
}
9853

99-
this._addToBuffer(span);
100-
}
101-
102-
shutdown(): Promise<void> {
103-
return this._shutdownOnce.call();
104-
}
105-
106-
private _shutdown() {
107-
return Promise.resolve()
108-
.then(() => {
109-
return this.onShutdown();
110-
})
111-
.then(() => {
112-
return this._flushAll();
113-
})
114-
.then(() => {
115-
return this._exporter.shutdown();
116-
});
117-
}
118-
119-
/** Add a span in the buffer. */
120-
private _addToBuffer(span: ReadableSpan) {
121-
if (this._finishedSpans.length >= this._maxQueueSize) {
122-
// limit reached, drop span
123-
124-
if (this._droppedSpansCount === 0) {
125-
diag.debug('maxQueueSize reached, dropping spans');
126-
}
127-
this._droppedSpansCount++;
128-
129-
return;
130-
}
131-
132-
if (this._droppedSpansCount > 0) {
133-
// some spans were dropped, log once with count of spans dropped
134-
diag.warn(`Dropped ${this._droppedSpansCount} spans because maxQueueSize reached`);
135-
this._droppedSpansCount = 0;
136-
}
137-
138-
this._finishedSpans.push(span);
139-
this._maybeStartTimer();
140-
}
141-
142-
/**
143-
* Send all spans to the exporter respecting the batch size limit
144-
* This function is used only on forceFlush or shutdown,
145-
* for all other cases _flush should be used
146-
* */
147-
private _flushAll(): Promise<void> {
148-
return new Promise((resolve, reject) => {
149-
const promises = [];
150-
// calculate number of batches
151-
const count = Math.ceil(this._finishedSpans.length / this._maxExportBatchSize);
152-
for (let i = 0, j = count; i < j; i++) {
153-
promises.push(this._flushOneBatch());
154-
}
155-
Promise.all(promises)
156-
.then(() => {
157-
resolve();
158-
})
159-
.catch(reject);
160-
});
161-
}
162-
163-
private _flushOneBatch(): Promise<void> {
164-
this._clearTimer();
165-
if (this._finishedSpans.length === 0) {
166-
return Promise.resolve();
167-
}
168-
return new Promise((resolve, reject) => {
169-
const timer = setTimeout(() => {
170-
// don't wait anymore for export, this way the next batch can start
171-
reject(new Error('Timeout'));
172-
}, this._exportTimeoutMillis);
173-
// prevent downstream exporter calls from generating spans
174-
context.with(suppressTracing(context.active()), () => {
175-
// Reset the finished spans buffer here because the next invocations of the _flush method
176-
// could pass the same finished spans to the exporter if the buffer is cleared
177-
// outside the execution of this callback.
178-
let spans: ReadableSpan[];
179-
if (this._finishedSpans.length <= this._maxExportBatchSize) {
180-
spans = this._finishedSpans;
181-
this._finishedSpans = [];
182-
} else {
183-
spans = this._finishedSpans.splice(0, this._maxExportBatchSize);
184-
}
185-
186-
const doExport = () =>
187-
this._exporter.export(spans, result => {
188-
clearTimeout(timer);
189-
if (result.code === ExportResultCode.SUCCESS) {
190-
resolve();
191-
} else {
192-
reject(result.error ?? new Error('BatchSpanProcessor: span export failed'));
193-
}
194-
});
195-
196-
let pendingResources: Array<Promise<void>> | null = null;
197-
for (let i = 0, len = spans.length; i < len; i++) {
198-
const span = spans[i];
199-
if (span.resource.asyncAttributesPending && span.resource.waitForAsyncAttributes) {
200-
pendingResources ??= [];
201-
pendingResources.push(span.resource.waitForAsyncAttributes());
202-
}
203-
}
204-
205-
// Avoid scheduling a promise to make the behavior more predictable and easier to test
206-
if (pendingResources === null) {
207-
doExport();
208-
} else {
209-
Promise.all(pendingResources).then(doExport, err => {
210-
globalErrorHandler(err);
211-
reject(err);
212-
});
213-
}
214-
});
215-
});
216-
}
217-
218-
private _maybeStartTimer() {
219-
if (this._isExporting) return;
220-
const flush = () => {
221-
this._isExporting = true;
222-
this._flushOneBatch()
223-
.finally(() => {
224-
this._isExporting = false;
225-
if (this._finishedSpans.length > 0) {
226-
this._clearTimer();
227-
this._maybeStartTimer();
228-
}
229-
})
230-
.catch(e => {
231-
this._isExporting = false;
232-
globalErrorHandler(e);
233-
});
234-
};
235-
// we only wait if the queue doesn't have enough elements yet
236-
if (this._finishedSpans.length >= this._maxExportBatchSize) {
237-
return flush();
238-
}
239-
if (this._timer !== undefined) return;
240-
this._timer = setTimeout(() => flush(), this._scheduledDelayMillis);
241-
unrefTimer(this._timer);
242-
}
243-
244-
private _clearTimer() {
245-
if (this._timer !== undefined) {
246-
clearTimeout(this._timer);
247-
this._timer = undefined;
248-
}
54+
(this as any)._addToBuffer(span);
24955
}
25056

25157
onShutdown(): void {}

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

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ const APPLICATION_SIGNALS_ENABLED_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS
6565
const APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT';
6666
const METRIC_EXPORT_INTERVAL_CONFIG: string = 'OTEL_METRIC_EXPORT_INTERVAL';
6767
const DEFAULT_METRIC_EXPORT_INTERVAL_MILLIS: number = 60000;
68-
const AWS_LAMBDA_FUNCTION_NAME_CONFIG: string = 'AWS_LAMBDA_FUNCTION_NAME';
68+
export const AWS_LAMBDA_FUNCTION_NAME_CONFIG: string = 'AWS_LAMBDA_FUNCTION_NAME';
6969
const AWS_XRAY_DAEMON_ADDRESS_CONFIG: string = 'AWS_XRAY_DAEMON_ADDRESS';
7070
const FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = 'T1S';
7171
const FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = 'T1U';
72+
// Follow Python SDK Impl to set the max span batch size
73+
// which will reduce the chance of UDP package size is larger than 64KB
74+
const LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10;
7275
/**
7376
* Aws Application Signals Config Provider creates a configuration object that can be provided to
7477
* the OTel NodeJS SDK for Auto Instrumentation with Application Signals Functionality.
@@ -128,6 +131,9 @@ export class AwsOpentelemetryConfigurator {
128131
if (!resourceDetectorsFromEnv.includes('env')) {
129132
defaultDetectors.push(envDetectorSync);
130133
}
134+
} else if (isLambdaEnvironment()) {
135+
// If in Lambda environment, only keep env detector as default
136+
defaultDetectors.push(envDetectorSync);
131137
} else {
132138
/*
133139
* envDetectorSync is used as opposed to envDetector (async), so it is guaranteed that the
@@ -229,17 +235,6 @@ export class AwsOpentelemetryConfigurator {
229235

230236
diag.info('AWS Application Signals enabled.');
231237

232-
// Register BatchUnsampledSpanProcessor to export unsampled traces in Lambda
233-
// when Application Signals enabled
234-
if (isLambdaEnvironment()) {
235-
spanProcessors.push(
236-
new AwsBatchUnsampledSpanProcessor(
237-
new OTLPUdpSpanExporter(getXrayDaemonEndpoint(), FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX)
238-
)
239-
);
240-
diag.info('Enabled batch unsampled span processor for Lambda environment.');
241-
}
242-
243238
let exportIntervalMillis: number = Number(process.env[METRIC_EXPORT_INTERVAL_CONFIG]);
244239
diag.debug(`AWS Application Signals Metrics export interval: ${exportIntervalMillis}`);
245240

@@ -256,12 +251,30 @@ export class AwsOpentelemetryConfigurator {
256251
exporter: applicationSignalsMetricExporter,
257252
exportIntervalMillis: exportIntervalMillis,
258253
});
259-
const meterProvider: MeterProvider = new MeterProvider({
260-
/** Resource associated with metric telemetry */
261-
resource: resource,
262-
readers: [periodicExportingMetricReader],
263-
});
264-
spanProcessors.push(AwsSpanMetricsProcessorBuilder.create(meterProvider, resource).build());
254+
255+
// Register BatchUnsampledSpanProcessor to export unsampled traces in Lambda
256+
// when Application Signals enabled
257+
if (isLambdaEnvironment()) {
258+
spanProcessors.push(
259+
new AwsBatchUnsampledSpanProcessor(
260+
new OTLPUdpSpanExporter(getXrayDaemonEndpoint(), FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX),
261+
{
262+
maxExportBatchSize: getSpanExportBatchSize(),
263+
}
264+
)
265+
);
266+
diag.info('Enabled batch unsampled span processor for Lambda environment.');
267+
}
268+
269+
// Disable Application Metrics for Lambda environment
270+
if (!isLambdaEnvironment()) {
271+
const meterProvider: MeterProvider = new MeterProvider({
272+
/** Resource associated with metric telemetry */
273+
resource: resource,
274+
readers: [periodicExportingMetricReader],
275+
});
276+
spanProcessors.push(AwsSpanMetricsProcessorBuilder.create(meterProvider, resource).build());
277+
}
265278
}
266279

267280
static customizeSampler(sampler: Sampler): Sampler {
@@ -415,7 +428,11 @@ export class AwsSpanProcessorProvider {
415428
// eslint-disable-next-line @typescript-eslint/typedef
416429
let protocol = this.getOtlpProtocol();
417430

418-
if (AwsOpentelemetryConfigurator.isApplicationSignalsEnabled() && isLambdaEnvironment()) {
431+
// If `isLambdaEnvironment` is true, we will default to exporting OTel spans via `udp_exporter` to Fluxpump,
432+
// regardless of whether `AppSignals` is true or false.
433+
// However, if the customer has explicitly set the `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
434+
// we will continue using the `otlp_exporter` to send OTel traces to the specified endpoint.
435+
if (!hasCustomOtlpTraceEndpoint() && isLambdaEnvironment()) {
419436
protocol = 'udp';
420437
}
421438
switch (protocol) {
@@ -521,7 +538,9 @@ export class AwsSpanProcessorProvider {
521538
if (exporter instanceof ConsoleSpanExporter) {
522539
return new SimpleSpanProcessor(configuredExporter);
523540
} else {
524-
return new BatchSpanProcessor(configuredExporter);
541+
return new BatchSpanProcessor(configuredExporter, {
542+
maxExportBatchSize: getSpanExportBatchSize(),
543+
});
525544
}
526545
});
527546
}
@@ -616,11 +635,22 @@ function getSamplerProbabilityFromEnv(environment: Required<ENVIRONMENT>): numbe
616635
return probability;
617636
}
618637

619-
function isLambdaEnvironment() {
638+
function getSpanExportBatchSize() {
639+
if (isLambdaEnvironment()) {
640+
return LAMBDA_SPAN_EXPORT_BATCH_SIZE;
641+
}
642+
return undefined;
643+
}
644+
645+
export function isLambdaEnvironment() {
620646
// detect if running in AWS Lambda environment
621647
return process.env[AWS_LAMBDA_FUNCTION_NAME_CONFIG] !== undefined;
622648
}
623649

650+
function hasCustomOtlpTraceEndpoint() {
651+
return process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] !== undefined;
652+
}
653+
624654
function getXrayDaemonEndpoint() {
625655
return process.env[AWS_XRAY_DAEMON_ADDRESS_CONFIG];
626656
}

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@opentelemetry/semantic-conventions';
1919
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
2020
import * as SQL_DIALECT_KEYWORDS_JSON from './configuration/sql_dialect_keywords.json';
21+
import { AWS_LAMBDA_FUNCTION_NAME_CONFIG, isLambdaEnvironment } from './aws-opentelemetry-configurator';
2122

2223
/** Utility class designed to support shared logic across AWS Span Processors. */
2324
export class AwsSpanProcessingUtil {
@@ -50,6 +51,9 @@ export class AwsSpanProcessingUtil {
5051
let operation: string = span.name;
5152
if (AwsSpanProcessingUtil.shouldUseInternalOperation(span)) {
5253
operation = AwsSpanProcessingUtil.INTERNAL_OPERATION;
54+
}
55+
if (isLambdaEnvironment()) {
56+
operation = process.env[AWS_LAMBDA_FUNCTION_NAME_CONFIG] + '/Handler';
5357
} else if (!AwsSpanProcessingUtil.isValidOperation(span, operation)) {
5458
operation = AwsSpanProcessingUtil.generateIngressOperation(span);
5559
}

0 commit comments

Comments
 (0)