Skip to content

Commit a74e9fb

Browse files
committed
OTel Logs Support in Lambda
1 parent cc55fa3 commit a74e9fb

File tree

4 files changed

+190
-7
lines changed

4 files changed

+190
-7
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ import {
3434
} from './aws/services/bedrock';
3535
import { SecretsManagerServiceExtension } from './aws/services/secretsmanager';
3636
import { StepFunctionsServiceExtension } from './aws/services/step-functions';
37-
import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
37+
import type { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
3838
import type { Command as AwsV3Command } from '@aws-sdk/types';
39+
import { LoggerProvider } from '@opentelemetry/api-logs';
3940

4041
export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID';
4142
export const AWSXRAY_TRACE_ID_HEADER_CAPITALIZED = 'X-Amzn-Trace-Id';
@@ -251,11 +252,49 @@ function patchLambdaServiceExtension(lambdaServiceExtension: any): void {
251252
}
252253
}
253254

254-
// Override the upstream private _endSpan method to remove the unnecessary metric force-flush error message
255-
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L358-L398
255+
export type ExtendedAwsLambdaInstrumentation = AwsLambdaInstrumentation & {
256+
_setLoggerProvider: (loggerProvider: LoggerProvider) => void;
257+
_logForceFlusher?: () => Promise<void>;
258+
_logForceFlush: (loggerProvider: LoggerProvider) => any;
259+
};
260+
261+
// Patch AWS Lambda Instrumentation
262+
// 1. Override the upstream private _endSpan method to remove the unnecessary metric force-flush error message
263+
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L358-L398
264+
// 2. Support setting logger provider and force flushing logs
256265
function patchAwsLambdaInstrumentation(instrumentation: Instrumentation): void {
257266
if (instrumentation) {
258-
(instrumentation as AwsLambdaInstrumentation)['_endSpan'] = function (
267+
const _setLoggerProvider = (instrumentation as ExtendedAwsLambdaInstrumentation)['setLoggerProvider'];
268+
(instrumentation as ExtendedAwsLambdaInstrumentation)['_setLoggerProvider'] = _setLoggerProvider;
269+
(instrumentation as ExtendedAwsLambdaInstrumentation)['_logForceFlusher'] = undefined;
270+
271+
instrumentation['setLoggerProvider'] = function (loggerProvider: LoggerProvider) {
272+
(this as ExtendedAwsLambdaInstrumentation)['_setLoggerProvider'](loggerProvider);
273+
(this as ExtendedAwsLambdaInstrumentation)['_logForceFlusher'] = (this as ExtendedAwsLambdaInstrumentation)[
274+
'_logForceFlush'
275+
](loggerProvider);
276+
};
277+
278+
(instrumentation as ExtendedAwsLambdaInstrumentation)['_logForceFlush'] = function (
279+
loggerProvider: LoggerProvider
280+
) {
281+
if (!loggerProvider) return undefined;
282+
283+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
284+
let currentProvider: any = loggerProvider;
285+
286+
if (typeof currentProvider.getDelegate === 'function') {
287+
currentProvider = currentProvider.getDelegate();
288+
}
289+
290+
if (typeof currentProvider.forceFlush === 'function') {
291+
return currentProvider.forceFlush.bind(currentProvider);
292+
}
293+
294+
return undefined;
295+
};
296+
297+
(instrumentation as ExtendedAwsLambdaInstrumentation)['_endSpan'] = function (
259298
span: Span,
260299
err: string | Error | null | undefined,
261300
callback: () => void
@@ -280,13 +319,27 @@ function patchAwsLambdaInstrumentation(instrumentation: Instrumentation): void {
280319
span.end();
281320

282321
const flushers = [];
283-
if ((this as any)._traceForceFlusher) {
284-
flushers.push((this as any)._traceForceFlusher());
322+
if (this['_traceForceFlusher']) {
323+
flushers.push(this['_traceForceFlusher']());
285324
} else {
286325
diag.error(
287326
'Spans may not be exported for the lambda function because we are not force flushing before callback.'
288327
);
289328
}
329+
if (this['_metricForceFlusher']) {
330+
flushers.push(this['_metricForceFlusher']());
331+
} else {
332+
diag.debug(
333+
'Metrics may not be exported for the lambda function because we are not force flushing before callback.'
334+
);
335+
}
336+
if (this['_logForceFlusher']) {
337+
flushers.push(this['_logForceFlusher']());
338+
} else {
339+
diag.debug(
340+
'Logs may not be exported for the lambda function because we are not force flushing before callback.'
341+
);
342+
}
290343

291344
Promise.all(flushers).then(callback, callback);
292345
};

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ if (process.env.OTEL_TRACES_SAMPLER === 'xray') {
1313
useXraySampler = true;
1414
}
1515

16-
import { diag, DiagConsoleLogger, trace } from '@opentelemetry/api';
16+
import { diag, DiagConsoleLogger, metrics, trace } from '@opentelemetry/api';
17+
import { logs } from '@opentelemetry/api-logs';
1718
import { getNodeAutoInstrumentations, InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node';
1819
import { Instrumentation } from '@opentelemetry/instrumentation';
1920
import * as opentelemetry from '@opentelemetry/sdk-node';
@@ -136,6 +137,10 @@ try {
136137
diag.info('Setting TraceProvider for instrumentations at the end of initialization');
137138
for (const instrumentation of instrumentations) {
138139
instrumentation.setTracerProvider(trace.getTracerProvider());
140+
instrumentation.setMeterProvider(metrics.getMeterProvider());
141+
if (instrumentation.setLoggerProvider) {
142+
instrumentation.setLoggerProvider(logs.getLoggerProvider());
143+
}
139144
}
140145

141146
diag.debug(`Environment variable OTEL_PROPAGATORS is set to '${process.env.OTEL_PROPAGATORS}'`);

aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
applyInstrumentationPatches,
2727
AWSXRAY_TRACE_ID_HEADER_CAPITALIZED,
2828
customExtractor,
29+
ExtendedAwsLambdaInstrumentation,
2930
headerGetter,
3031
} from './../../src/patches/instrumentation-patch';
3132
import * as sinon from 'sinon';
@@ -37,6 +38,7 @@ import * as nock from 'nock';
3738
import { ReadableSpan, Span as SDKSpan } from '@opentelemetry/sdk-trace-base';
3839
import { getTestSpans } from '@opentelemetry/contrib-test-utils';
3940
import { instrumentationConfigs } from '../../src/register';
41+
import { LoggerProvider } from '@opentelemetry/api-logs';
4042

4143
// It is assumed that bedrock.test.ts has already registered the
4244
// necessary instrumentations for testing by calling:
@@ -673,6 +675,124 @@ describe('InstrumentationPatchTest', () => {
673675
});
674676
});
675677
});
678+
679+
describe('AwsLambdaInstrumentationPatchTest', () => {
680+
let awsLambdaInstrumentation: ExtendedAwsLambdaInstrumentation;
681+
beforeEach(() => {
682+
const instrumentationsToTest = [
683+
new AwsLambdaInstrumentation(instrumentationConfigs['@opentelemetry/instrumentation-aws-lambda']),
684+
];
685+
applyInstrumentationPatches(instrumentationsToTest);
686+
awsLambdaInstrumentation = instrumentationsToTest[0] as ExtendedAwsLambdaInstrumentation;
687+
});
688+
689+
afterEach(() => {
690+
sinon.restore();
691+
});
692+
693+
it('Tests setLoggerProvider method', () => {
694+
const resolvedPromise = Promise.resolve();
695+
const mockLoggerProvider = {
696+
getDelegate: () => ({
697+
forceFlush: () => resolvedPromise,
698+
}),
699+
getLogger: () => {
700+
return {
701+
emit: () => {},
702+
};
703+
},
704+
};
705+
706+
awsLambdaInstrumentation.setLoggerProvider(mockLoggerProvider as LoggerProvider);
707+
expect(awsLambdaInstrumentation['_logForceFlusher']!()).toBe(resolvedPromise);
708+
});
709+
710+
it('Tests _logForceFlush with provider that has getDelegate', () => {
711+
const mockForceFlush = sinon.stub().resolves();
712+
const mockLoggerProvider = {
713+
getDelegate: () => ({
714+
forceFlush: mockForceFlush,
715+
}),
716+
getLogger: () => {
717+
return {
718+
emit: () => {},
719+
};
720+
},
721+
};
722+
723+
const flusher = awsLambdaInstrumentation['_logForceFlush'](mockLoggerProvider as LoggerProvider);
724+
expect(flusher).toBeDefined();
725+
flusher?.();
726+
expect(mockForceFlush.called).toBeTruthy();
727+
});
728+
729+
it('Tests _logForceFlush with provider that has direct forceFlush', () => {
730+
const mockForceFlush = sinon.stub().resolves();
731+
const mockLoggerProvider = {
732+
forceFlush: mockForceFlush,
733+
getLogger: () => {
734+
return {
735+
emit: () => {},
736+
};
737+
},
738+
};
739+
740+
const flusher = awsLambdaInstrumentation['_logForceFlush'](mockLoggerProvider as LoggerProvider);
741+
expect(flusher).toBeDefined();
742+
flusher?.();
743+
expect(mockForceFlush.called).toBeTruthy();
744+
});
745+
746+
it('Tests _logForceFlush with undefined provider', () => {
747+
const flusher = awsLambdaInstrumentation['_logForceFlush'](undefined as unknown as LoggerProvider);
748+
expect(flusher).toBeUndefined();
749+
});
750+
751+
it('Tests _endSpan with all flushers', done => {
752+
const mockSpan: Span = sinon.createStubInstance(SDKSpan);
753+
754+
// Setup mock flushers
755+
const mockTraceFlush = sinon.stub().resolves();
756+
const mockMetricFlush = sinon.stub().resolves();
757+
const mockLogFlush = sinon.stub().resolves();
758+
759+
awsLambdaInstrumentation['_traceForceFlusher'] = mockTraceFlush;
760+
awsLambdaInstrumentation['_metricForceFlusher'] = mockMetricFlush;
761+
awsLambdaInstrumentation['_logForceFlusher'] = mockLogFlush;
762+
763+
awsLambdaInstrumentation['_endSpan'](mockSpan, null, () => {
764+
expect(mockTraceFlush.called).toBeTruthy();
765+
expect(mockMetricFlush.called).toBeTruthy();
766+
expect(mockLogFlush.called).toBeTruthy();
767+
done();
768+
});
769+
});
770+
771+
it('Tests _endSpan handles missing flushers gracefully', done => {
772+
const mockSpan: Span = sinon.createStubInstance(SDKSpan);
773+
const mockDiag = sinon.spy(diag, 'debug');
774+
const mockDiagError = sinon.spy(diag, 'error');
775+
776+
awsLambdaInstrumentation['_endSpan'](mockSpan, null, () => {
777+
expect(
778+
mockDiagError.calledWith(
779+
'Spans may not be exported for the lambda function because we are not force flushing before callback.'
780+
)
781+
).toBeTruthy();
782+
expect(
783+
mockDiag.calledWith(
784+
'Metrics may not be exported for the lambda function because we are not force flushing before callback.'
785+
)
786+
).toBeTruthy();
787+
expect(
788+
mockDiag.calledWith(
789+
'Logs may not be exported for the lambda function because we are not force flushing before callback.'
790+
)
791+
).toBeTruthy();
792+
done();
793+
});
794+
});
795+
});
676796
});
677797

678798
describe('customExtractor', () => {

lambda-layer/packages/layer/scripts/otel-instrument

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ if [ -z "${OTEL_METRICS_EXPORTER}" ]; then
8080
export OTEL_METRICS_EXPORTER="none";
8181
fi
8282

83+
# - Disable logs exporter by default
84+
if [ -z "${OTEL_LOGS_EXPORTER}" ]; then
85+
export OTEL_LOGS_EXPORTER="none";
86+
fi
87+
8388
# - Append Lambda Resource Attributes to OTel Resource Attribute List
8489
if [ -z "${OTEL_RESOURCE_ATTRIBUTES}" ]; then
8590
export OTEL_RESOURCE_ATTRIBUTES=$LAMBDA_RESOURCE_ATTRIBUTES;

0 commit comments

Comments
 (0)