Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void>;
_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
Expand Down Expand Up @@ -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);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}'`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
applyInstrumentationPatches,
AWSXRAY_TRACE_ID_HEADER_CAPITALIZED,
customExtractor,
ExtendedAwsLambdaInstrumentation,
headerGetter,
} from './../../src/patches/instrumentation-patch';
import * as sinon from 'sinon';
Expand All @@ -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:
Expand Down Expand Up @@ -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', () => {
Expand Down
5 changes: 5 additions & 0 deletions lambda-layer/packages/layer/scripts/otel-instrument
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading