diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts index 9a336d74..bb5f1324 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts @@ -36,6 +36,7 @@ import { SecretsManagerServiceExtension } from './aws/services/secretsmanager'; import { StepFunctionsServiceExtension } from './aws/services/step-functions'; import type { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda'; import type { Command as AwsV3Command } from '@aws-sdk/types'; +import { LoggerProvider } from '@opentelemetry/api-logs'; export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID'; export const AWSXRAY_TRACE_ID_HEADER_CAPITALIZED = 'X-Amzn-Trace-Id'; @@ -251,11 +252,49 @@ function patchLambdaServiceExtension(lambdaServiceExtension: any): void { } } -// Override the upstream private _endSpan method to remove the unnecessary metric force-flush error message -// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L358-L398 +export type ExtendedAwsLambdaInstrumentation = AwsLambdaInstrumentation & { + _setLoggerProvider: (loggerProvider: LoggerProvider) => void; + _logForceFlusher?: () => Promise; + _logForceFlush: (loggerProvider: LoggerProvider) => any; +}; + +// Patch AWS Lambda Instrumentation +// 1. Override the upstream private _endSpan method to remove the unnecessary metric force-flush error message +// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L358-L398 +// 2. Support setting logger provider and force flushing logs function patchAwsLambdaInstrumentation(instrumentation: Instrumentation): void { if (instrumentation) { - (instrumentation as AwsLambdaInstrumentation)['_endSpan'] = function ( + const _setLoggerProvider = (instrumentation as ExtendedAwsLambdaInstrumentation)['setLoggerProvider']; + (instrumentation as ExtendedAwsLambdaInstrumentation)['_setLoggerProvider'] = _setLoggerProvider; + (instrumentation as ExtendedAwsLambdaInstrumentation)['_logForceFlusher'] = undefined; + + instrumentation['setLoggerProvider'] = function (loggerProvider: LoggerProvider) { + (this as ExtendedAwsLambdaInstrumentation)['_setLoggerProvider'](loggerProvider); + (this as ExtendedAwsLambdaInstrumentation)['_logForceFlusher'] = (this as ExtendedAwsLambdaInstrumentation)[ + '_logForceFlush' + ](loggerProvider); + }; + + (instrumentation as ExtendedAwsLambdaInstrumentation)['_logForceFlush'] = function ( + loggerProvider: LoggerProvider + ) { + if (!loggerProvider) return undefined; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let currentProvider: any = loggerProvider; + + if (typeof currentProvider.getDelegate === 'function') { + currentProvider = currentProvider.getDelegate(); + } + + if (typeof currentProvider.forceFlush === 'function') { + return currentProvider.forceFlush.bind(currentProvider); + } + + return undefined; + }; + + (instrumentation as ExtendedAwsLambdaInstrumentation)['_endSpan'] = function ( span: Span, err: string | Error | null | undefined, callback: () => void @@ -294,6 +333,13 @@ function patchAwsLambdaInstrumentation(instrumentation: Instrumentation): void { 'Metrics may not be exported for the lambda function because we are not force flushing before callback.' ); } + if (this['_logForceFlusher']) { + flushers.push(this['_logForceFlusher']()); + } else { + diag.debug( + 'Logs may not be exported for the lambda function because we are not force flushing before callback.' + ); + } Promise.all(flushers).then(callback, callback); }; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts index cb7e0bf9..07916bee 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts @@ -14,6 +14,7 @@ if (process.env.OTEL_TRACES_SAMPLER === 'xray') { } import { diag, DiagConsoleLogger, metrics, trace } from '@opentelemetry/api'; +import { logs } from '@opentelemetry/api-logs'; import { getNodeAutoInstrumentations, InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node'; import { Instrumentation } from '@opentelemetry/instrumentation'; import * as opentelemetry from '@opentelemetry/sdk-node'; @@ -137,6 +138,9 @@ try { for (const instrumentation of instrumentations) { instrumentation.setTracerProvider(trace.getTracerProvider()); instrumentation.setMeterProvider(metrics.getMeterProvider()); + if (instrumentation.setLoggerProvider) { + instrumentation.setLoggerProvider(logs.getLoggerProvider()); + } } diag.debug(`Environment variable OTEL_PROPAGATORS is set to '${process.env.OTEL_PROPAGATORS}'`); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts index 1f815c2a..1a256c7b 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts @@ -26,6 +26,7 @@ import { applyInstrumentationPatches, AWSXRAY_TRACE_ID_HEADER_CAPITALIZED, customExtractor, + ExtendedAwsLambdaInstrumentation, headerGetter, } from './../../src/patches/instrumentation-patch'; import * as sinon from 'sinon'; @@ -37,6 +38,7 @@ import * as nock from 'nock'; import { ReadableSpan, Span as SDKSpan } from '@opentelemetry/sdk-trace-base'; import { getTestSpans } from '@opentelemetry/contrib-test-utils'; import { instrumentationConfigs } from '../../src/register'; +import { LoggerProvider } from '@opentelemetry/api-logs'; // It is assumed that bedrock.test.ts has already registered the // necessary instrumentations for testing by calling: @@ -673,6 +675,124 @@ describe('InstrumentationPatchTest', () => { }); }); }); + + describe('AwsLambdaInstrumentationPatchTest', () => { + let awsLambdaInstrumentation: ExtendedAwsLambdaInstrumentation; + beforeEach(() => { + const instrumentationsToTest = [ + new AwsLambdaInstrumentation(instrumentationConfigs['@opentelemetry/instrumentation-aws-lambda']), + ]; + applyInstrumentationPatches(instrumentationsToTest); + awsLambdaInstrumentation = instrumentationsToTest[0] as ExtendedAwsLambdaInstrumentation; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('Tests setLoggerProvider method', () => { + const resolvedPromise = Promise.resolve(); + const mockLoggerProvider = { + getDelegate: () => ({ + forceFlush: () => resolvedPromise, + }), + getLogger: () => { + return { + emit: () => {}, + }; + }, + }; + + awsLambdaInstrumentation.setLoggerProvider(mockLoggerProvider as LoggerProvider); + expect(awsLambdaInstrumentation['_logForceFlusher']!()).toBe(resolvedPromise); + }); + + it('Tests _logForceFlush with provider that has getDelegate', () => { + const mockForceFlush = sinon.stub().resolves(); + const mockLoggerProvider = { + getDelegate: () => ({ + forceFlush: mockForceFlush, + }), + getLogger: () => { + return { + emit: () => {}, + }; + }, + }; + + const flusher = awsLambdaInstrumentation['_logForceFlush'](mockLoggerProvider as LoggerProvider); + expect(flusher).toBeDefined(); + flusher?.(); + expect(mockForceFlush.called).toBeTruthy(); + }); + + it('Tests _logForceFlush with provider that has direct forceFlush', () => { + const mockForceFlush = sinon.stub().resolves(); + const mockLoggerProvider = { + forceFlush: mockForceFlush, + getLogger: () => { + return { + emit: () => {}, + }; + }, + }; + + const flusher = awsLambdaInstrumentation['_logForceFlush'](mockLoggerProvider as LoggerProvider); + expect(flusher).toBeDefined(); + flusher?.(); + expect(mockForceFlush.called).toBeTruthy(); + }); + + it('Tests _logForceFlush with undefined provider', () => { + const flusher = awsLambdaInstrumentation['_logForceFlush'](undefined as unknown as LoggerProvider); + expect(flusher).toBeUndefined(); + }); + + it('Tests _endSpan with all flushers', done => { + const mockSpan: Span = sinon.createStubInstance(SDKSpan); + + // Setup mock flushers + const mockTraceFlush = sinon.stub().resolves(); + const mockMetricFlush = sinon.stub().resolves(); + const mockLogFlush = sinon.stub().resolves(); + + awsLambdaInstrumentation['_traceForceFlusher'] = mockTraceFlush; + awsLambdaInstrumentation['_metricForceFlusher'] = mockMetricFlush; + awsLambdaInstrumentation['_logForceFlusher'] = mockLogFlush; + + awsLambdaInstrumentation['_endSpan'](mockSpan, null, () => { + expect(mockTraceFlush.called).toBeTruthy(); + expect(mockMetricFlush.called).toBeTruthy(); + expect(mockLogFlush.called).toBeTruthy(); + done(); + }); + }); + + it('Tests _endSpan handles missing flushers gracefully', done => { + const mockSpan: Span = sinon.createStubInstance(SDKSpan); + const mockDiag = sinon.spy(diag, 'debug'); + const mockDiagError = sinon.spy(diag, 'error'); + + awsLambdaInstrumentation['_endSpan'](mockSpan, null, () => { + expect( + mockDiagError.calledWith( + 'Spans may not be exported for the lambda function because we are not force flushing before callback.' + ) + ).toBeTruthy(); + expect( + mockDiag.calledWith( + 'Metrics may not be exported for the lambda function because we are not force flushing before callback.' + ) + ).toBeTruthy(); + expect( + mockDiag.calledWith( + 'Logs may not be exported for the lambda function because we are not force flushing before callback.' + ) + ).toBeTruthy(); + done(); + }); + }); + }); }); describe('customExtractor', () => { diff --git a/lambda-layer/packages/layer/scripts/otel-instrument b/lambda-layer/packages/layer/scripts/otel-instrument index 1e3c54c9..af008e79 100644 --- a/lambda-layer/packages/layer/scripts/otel-instrument +++ b/lambda-layer/packages/layer/scripts/otel-instrument @@ -81,6 +81,11 @@ if [ -z "${OTEL_METRICS_EXPORTER}" ]; then export OTEL_METRICS_EXPORTER="awsemf"; fi +# - Disable logs exporter by default +if [ -z "${OTEL_LOGS_EXPORTER}" ]; then + export OTEL_LOGS_EXPORTER="none"; +fi + # - Append Lambda Resource Attributes to OTel Resource Attribute List if [ -z "${OTEL_RESOURCE_ATTRIBUTES}" ]; then export OTEL_RESOURCE_ATTRIBUTES=$LAMBDA_RESOURCE_ATTRIBUTES;