diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs new file mode 100644 index 000000000000..53785b6046f7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs @@ -0,0 +1,3 @@ +export const handler = async () => { + throw new Error('test esm'); +}; diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs index b13f30397b62..e51d323c1347 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/aws-serverless'; import * as http from 'node:http'; -export const handler = Sentry.wrapHandler(async () => { +export const handler = async () => { await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => { await new Promise(resolve => { http.get('http://example.com', res => { @@ -16,4 +16,4 @@ export const handler = Sentry.wrapHandler(async () => { }); }); }); -}); +}; diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js index 534909d6764e..e53b6670225d 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js @@ -1,7 +1,7 @@ const http = require('http'); const Sentry = require('@sentry/aws-serverless'); -exports.handler = Sentry.wrapHandler(async () => { +exports.handler = async () => { await new Promise(resolve => { const req = http.request( { @@ -21,4 +21,4 @@ exports.handler = Sentry.wrapHandler(async () => { }); Sentry.startSpan({ name: 'manual-span', op: 'manual' }, () => {}); -}); +}; diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs index 346613025497..e085a7cc2f8f 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs @@ -1,7 +1,7 @@ import * as http from 'node:http'; import * as Sentry from '@sentry/aws-serverless'; -export const handler = Sentry.wrapHandler(async () => { +export const handler = async () => { await new Promise(resolve => { const req = http.request( { @@ -21,4 +21,4 @@ export const handler = Sentry.wrapHandler(async () => { }); Sentry.startSpan({ name: 'manual-span', op: 'manual' }, () => {}); -}); +}; diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts index 79ad0fa31070..c20659835ee8 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts @@ -160,7 +160,35 @@ test.describe('Lambda layer', () => { type: 'Error', value: 'test', mechanism: { - type: 'auto.function.aws-serverless.handler', + type: 'auto.function.aws-serverless.otel', + handled: false, + }, + }), + ); + }); + + test('capturing errors works in ESM', async ({ lambdaClient }) => { + const errorEventPromise = waitForError('aws-serverless-lambda-sam', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'test esm'; + }); + + await lambdaClient.send( + new InvokeCommand({ + FunctionName: 'LayerErrorEsm', + Payload: JSON.stringify({}), + }), + ); + + const errorEvent = await errorEventPromise; + + // shows the SDK sent an error event + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]).toEqual( + expect.objectContaining({ + type: 'Error', + value: 'test esm', + mechanism: { + type: 'auto.function.aws-serverless.otel', handled: false, }, }), diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 708a9376ba3a..d24ff2560a05 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -67,8 +67,8 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.203.0", - "@opentelemetry/instrumentation-aws-lambda": "0.54.0", "@opentelemetry/instrumentation-aws-sdk": "0.56.0", + "@opentelemetry/semantic-conventions": "^1.36.0", "@sentry/core": "10.5.0", "@sentry/node": "10.5.0", "@types/aws-lambda": "^8.10.62" diff --git a/packages/aws-serverless/src/awslambda-auto.ts b/packages/aws-serverless/src/awslambda-auto.ts index 2f23fe652005..5848aa08e568 100644 --- a/packages/aws-serverless/src/awslambda-auto.ts +++ b/packages/aws-serverless/src/awslambda-auto.ts @@ -22,10 +22,6 @@ if (lambdaTaskRoot) { : {}, ), }); - - if (typeof require !== 'undefined') { - Sentry.tryPatchHandler(lambdaTaskRoot, handlerString); - } } else { throw Error('LAMBDA_TASK_ROOT environment variable is not set'); } diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 8cbcd31c50a5..87b9398fe960 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -147,5 +147,6 @@ export { export { awsIntegration } from './integration/aws'; export { awsLambdaIntegration } from './integration/awslambda'; -export { getDefaultIntegrations, init, tryPatchHandler, wrapHandler } from './sdk'; +export { getDefaultIntegrations, init } from './init'; +export { wrapHandler } from './sdk'; export type { WrapperOptions } from './sdk'; diff --git a/packages/aws-serverless/src/init.ts b/packages/aws-serverless/src/init.ts new file mode 100644 index 000000000000..269cc3fe27fb --- /dev/null +++ b/packages/aws-serverless/src/init.ts @@ -0,0 +1,31 @@ +import type { Integration, Options } from '@sentry/core'; +import { applySdkMetadata, getSDKSource } from '@sentry/core'; +import type { NodeClient, NodeOptions } from '@sentry/node'; +import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node'; +import { awsIntegration } from './integration/aws'; +import { awsLambdaIntegration } from './integration/awslambda'; + +/** + * Get the default integrations for the AWSLambda SDK. + */ +// NOTE: in awslambda-auto.ts, we also call the original `getDefaultIntegrations` from `@sentry/node` to load performance integrations. +// If at some point we need to filter a node integration out for good, we need to make sure to also filter it out there. +export function getDefaultIntegrations(_options: Options): Integration[] { + return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()]; +} + +/** + * Initializes the Sentry AWS Lambda SDK. + * + * @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}. + */ +export function init(options: NodeOptions = {}): NodeClient | undefined { + const opts = { + defaultIntegrations: getDefaultIntegrations(options), + ...options, + }; + + applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], getSDKSource()); + + return initWithoutDefaultIntegrations(opts); +} diff --git a/packages/aws-serverless/src/integration/awslambda.ts b/packages/aws-serverless/src/integration/awslambda.ts index 00bca1a9219c..c459fc8e25e8 100644 --- a/packages/aws-serverless/src/integration/awslambda.ts +++ b/packages/aws-serverless/src/integration/awslambda.ts @@ -1,8 +1,8 @@ -import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { generateInstrumentOnce } from '@sentry/node'; -import { eventContextExtractor } from '../utils'; +import { captureException, generateInstrumentOnce } from '@sentry/node'; +import { eventContextExtractor, markEventUnhandled } from '../utils'; +import { AwsLambdaInstrumentation } from './instrumentation-aws-lambda/instrumentation'; interface AwsLambdaOptions { /** @@ -27,6 +27,11 @@ export const instrumentAwsLambda = generateInstrumentOnce( span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda'); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda'); }, + responseHook(_span, { err }) { + if (err) { + captureException(err, scope => markEventUnhandled(scope, 'auto.function.aws-serverless.otel')); + } + }, }; }, ); diff --git a/packages/aws-serverless/src/integration/instrumentation-aws-lambda/instrumentation.ts b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/instrumentation.ts new file mode 100644 index 000000000000..9b37d66b866d --- /dev/null +++ b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/instrumentation.ts @@ -0,0 +1,524 @@ +// Vendored and modified from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/cc7eff47e2e7bad7678241b766753d5bd6dbc85f/packages/instrumentation-aws-lambda/src/instrumentation.ts +// Modifications: +// - Added Sentry `wrapHandler` around the OTel patch handler. +// - Cancel init when handler string is invalid (TS) +// - Hardcoded package version and name +/* eslint-disable */ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + Attributes, + Context as OtelContext, + MeterProvider, + Span, + TextMapGetter, + TracerProvider, +} from '@opentelemetry/api'; +import { + context as otelContext, + diag, + propagation, + ROOT_CONTEXT, + SpanKind, + SpanStatusCode, + trace, +} from '@opentelemetry/api'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; +import { + ATTR_URL_FULL, + SEMATTRS_FAAS_EXECUTION, + SEMRESATTRS_CLOUD_ACCOUNT_ID, + SEMRESATTRS_FAAS_ID, +} from '@opentelemetry/semantic-conventions'; +import type { APIGatewayProxyEventHeaders, Callback, Context, Handler } from 'aws-lambda'; +import * as fs from 'fs'; +import * as path from 'path'; +import type { LambdaModule } from './internal-types'; +import { ATTR_FAAS_COLDSTART } from './semconv'; +import type { AwsLambdaInstrumentationConfig, EventContextExtractor } from './types'; +import { wrapHandler } from '../../sdk'; + +const PACKAGE_VERSION = '0.54.0'; +const PACKAGE_NAME = '@opentelemetry/instrumentation-aws-lambda'; + +const headerGetter: TextMapGetter = { + keys(carrier): string[] { + return Object.keys(carrier); + }, + get(carrier, key: string) { + return carrier[key]; + }, +}; + +export const lambdaMaxInitInMilliseconds = 10_000; + +/** + * + */ +export class AwsLambdaInstrumentation extends InstrumentationBase { + private declare _traceForceFlusher?: () => Promise; + private declare _metricForceFlusher?: () => Promise; + + constructor(config: AwsLambdaInstrumentationConfig = {}) { + super(PACKAGE_NAME, PACKAGE_VERSION, config); + } + + /** + * + */ + init() { + const taskRoot = process.env.LAMBDA_TASK_ROOT; + const handlerDef = this.getConfig().lambdaHandler ?? process.env._HANDLER; + + // _HANDLER and LAMBDA_TASK_ROOT are always defined in Lambda but guard bail out if in the future this changes. + if (!taskRoot || !handlerDef) { + this._diag.debug('Skipping lambda instrumentation: no _HANDLER/lambdaHandler or LAMBDA_TASK_ROOT.', { + taskRoot, + handlerDef, + }); + return []; + } + + const handler = path.basename(handlerDef); + const moduleRoot = handlerDef.substring(0, handlerDef.length - handler.length); + + const [module, functionName] = handler.split('.', 2); + + if (!module || !functionName) { + this._diag.warn('Invalid handler definition', { + handler, + moduleRoot, + module, + }); + return []; + } + + // Lambda loads user function using an absolute path. + let filename = path.resolve(taskRoot, moduleRoot, module); + if (!filename.endsWith('.js')) { + // It's impossible to know in advance if the user has a js, mjs or cjs file. + // Check that the .js file exists otherwise fallback to the next known possibilities (.mjs, .cjs). + try { + fs.statSync(`${filename}.js`); + filename += '.js'; + } catch (e) { + try { + fs.statSync(`${filename}.mjs`); + // fallback to .mjs (ESM) + filename += '.mjs'; + } catch (e2) { + try { + fs.statSync(`${filename}.cjs`); + // fallback to .cjs (CommonJS) + filename += '.cjs'; + } catch (e3) { + this._diag.warn( + 'No handler file was able to resolved with one of the known extensions for the file', + filename, + ); + } + } + } + } + + diag.debug('Instrumenting lambda handler', { + taskRoot, + handlerDef, + handler, + moduleRoot, + module, + filename, + functionName, + }); + + const lambdaStartTime = this.getConfig().lambdaStartTime || Date.now() - Math.floor(1000 * process.uptime()); + + return [ + new InstrumentationNodeModuleDefinition( + // NB: The patching infrastructure seems to match names backwards, this must be the filename, while + // InstrumentationNodeModuleFile must be the module name. + filename, + ['*'], + undefined, + undefined, + [ + new InstrumentationNodeModuleFile( + module, + ['*'], + (moduleExports: LambdaModule) => { + if (isWrapped(moduleExports[functionName])) { + this._unwrap(moduleExports, functionName); + } + this._wrap(moduleExports, functionName, this._getHandler(lambdaStartTime)); + return moduleExports; + }, + (moduleExports?: LambdaModule) => { + if (moduleExports == null) return; + this._unwrap(moduleExports, functionName); + }, + ), + ], + ), + ]; + } + + /** + * + */ + private _getHandler(handlerLoadStartTime: number) { + return (original: Handler) => { + return wrapHandler(this._getPatchHandler(original, handlerLoadStartTime)); + }; + } + + /** + * + */ + private _getPatchHandler(original: Handler, lambdaStartTime: number) { + diag.debug('patch handler function'); + const plugin = this; + + let requestHandledBefore = false; + let requestIsColdStart = true; + + /** + * + */ + function _onRequest(): void { + if (requestHandledBefore) { + // Non-first requests cannot be coldstart. + requestIsColdStart = false; + } else { + if (process.env.AWS_LAMBDA_INITIALIZATION_TYPE === 'provisioned-concurrency') { + // If sandbox environment is initialized with provisioned concurrency, + // even the first requests should not be considered as coldstart. + requestIsColdStart = false; + } else { + // Check whether it is proactive initialization or not: + // https://aaronstuyvenberg.com/posts/understanding-proactive-initialization + const passedTimeSinceHandlerLoad: number = Date.now() - lambdaStartTime; + const proactiveInitialization: boolean = passedTimeSinceHandlerLoad > lambdaMaxInitInMilliseconds; + + // If sandbox has been initialized proactively before the actual request, + // even the first requests should not be considered as coldstart. + requestIsColdStart = !proactiveInitialization; + } + requestHandledBefore = true; + } + } + + return function patchedHandler( + this: never, + // The event can be a user type, it truly is any. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: any, + context: Context, + callback: Callback, + ) { + _onRequest(); + + const config = plugin.getConfig(); + const parent = AwsLambdaInstrumentation._determineParent( + event, + context, + config.eventContextExtractor || AwsLambdaInstrumentation._defaultEventContextExtractor, + ); + + const name = context.functionName; + const span = plugin.tracer.startSpan( + name, + { + kind: SpanKind.SERVER, + attributes: { + [SEMATTRS_FAAS_EXECUTION]: context.awsRequestId, + [SEMRESATTRS_FAAS_ID]: context.invokedFunctionArn, + [SEMRESATTRS_CLOUD_ACCOUNT_ID]: AwsLambdaInstrumentation._extractAccountId(context.invokedFunctionArn), + [ATTR_FAAS_COLDSTART]: requestIsColdStart, + ...AwsLambdaInstrumentation._extractOtherEventFields(event), + }, + }, + parent, + ); + + const { requestHook } = config; + if (requestHook) { + safeExecuteInTheMiddle( + () => requestHook(span, { event, context }), + e => { + if (e) diag.error('aws-lambda instrumentation: requestHook error', e); + }, + true, + ); + } + + return otelContext.with(trace.setSpan(parent, span), () => { + // Lambda seems to pass a callback even if handler is of Promise form, so we wrap all the time before calling + // the handler and see if the result is a Promise or not. In such a case, the callback is usually ignored. If + // the handler happened to both call the callback and complete a returned Promise, whichever happens first will + // win and the latter will be ignored. + const wrappedCallback = plugin._wrapCallback(callback, span); + const maybePromise = safeExecuteInTheMiddle( + () => original.apply(this, [event, context, wrappedCallback]), + error => { + if (error != null) { + // Exception thrown synchronously before resolving callback / promise. + plugin._applyResponseHook(span, error); + plugin._endSpan(span, error, () => {}); + } + }, + ) as Promise<{}> | undefined; + if (typeof maybePromise?.then === 'function') { + return maybePromise.then( + value => { + plugin._applyResponseHook(span, null, value); + return new Promise(resolve => plugin._endSpan(span, undefined, () => resolve(value))); + }, + (err: Error | string) => { + plugin._applyResponseHook(span, err); + return new Promise((resolve, reject) => plugin._endSpan(span, err, () => reject(err))); + }, + ); + } + return maybePromise; + }); + }; + } + + /** + * + */ + override setTracerProvider(tracerProvider: TracerProvider) { + super.setTracerProvider(tracerProvider); + this._traceForceFlusher = this._traceForceFlush(tracerProvider); + } + + /** + * + */ + private _traceForceFlush(tracerProvider: TracerProvider) { + if (!tracerProvider) return undefined; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let currentProvider: any = tracerProvider; + + if (typeof currentProvider.getDelegate === 'function') { + currentProvider = currentProvider.getDelegate(); + } + + if (typeof currentProvider.forceFlush === 'function') { + return currentProvider.forceFlush.bind(currentProvider); + } + + return undefined; + } + + /** + * + */ + override setMeterProvider(meterProvider: MeterProvider) { + super.setMeterProvider(meterProvider); + this._metricForceFlusher = this._metricForceFlush(meterProvider); + } + + /** + * + */ + private _metricForceFlush(meterProvider: MeterProvider) { + if (!meterProvider) return undefined; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const currentProvider: any = meterProvider; + + if (typeof currentProvider.forceFlush === 'function') { + return currentProvider.forceFlush.bind(currentProvider); + } + + return undefined; + } + + /** + * + */ + private _wrapCallback(original: Callback, span: Span): Callback { + const plugin = this; + return function wrappedCallback(this: never, err, res) { + diag.debug('executing wrapped lookup callback function'); + plugin._applyResponseHook(span, err, res); + + plugin._endSpan(span, err, () => { + diag.debug('executing original lookup callback function'); + return original.apply(this, [err, res]); + }); + }; + } + + /** + * + */ + private _endSpan(span: Span, err: string | Error | null | undefined, callback: () => void) { + if (err) { + span.recordException(err); + } + + let errMessage; + if (typeof err === 'string') { + errMessage = err; + } else if (err) { + errMessage = err.message; + } + if (errMessage) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: errMessage, + }); + } + + span.end(); + + const flushers = []; + if (this._traceForceFlusher) { + flushers.push(this._traceForceFlusher()); + } else { + diag.debug( + 'Spans may not be exported for the lambda function because we are not force flushing before callback.', + ); + } + if (this._metricForceFlusher) { + flushers.push(this._metricForceFlusher()); + } else { + diag.debug( + 'Metrics may not be exported for the lambda function because we are not force flushing before callback.', + ); + } + + Promise.all(flushers).then(callback, callback); + } + + /** + * + */ + private _applyResponseHook(span: Span, err?: Error | string | null, res?: any) { + const { responseHook } = this.getConfig(); + if (responseHook) { + safeExecuteInTheMiddle( + () => responseHook(span, { err, res }), + e => { + if (e) diag.error('aws-lambda instrumentation: responseHook error', e); + }, + true, + ); + } + } + + /** + * + */ + private static _extractAccountId(arn: string): string | undefined { + const parts = arn.split(':'); + if (parts.length >= 5) { + return parts[4]; + } + return undefined; + } + + /** + * + */ + private static _defaultEventContextExtractor(event: any): OtelContext { + // The default extractor tries to get sampled trace header from HTTP headers. + const httpHeaders = event.headers || {}; + return propagation.extract(otelContext.active(), httpHeaders, headerGetter); + } + + /** + * + */ + private static _extractOtherEventFields(event: any): Attributes { + const answer: Attributes = {}; + const fullUrl = this._extractFullUrl(event); + if (fullUrl) { + answer[ATTR_URL_FULL] = fullUrl; + } + return answer; + } + + /** + * + */ + private static _extractFullUrl(event: any): string | undefined { + // API gateway encodes a lot of url information in various places to recompute this + if (!event.headers) { + return undefined; + } + // Helper function to deal with case variations (instead of making a tolower() copy of the headers) + /** + * + */ + function findAny(event: any, key1: string, key2: string): string | undefined { + return event.headers[key1] ?? event.headers[key2]; + } + const host = findAny(event, 'host', 'Host'); + const proto = findAny(event, 'x-forwarded-proto', 'X-Forwarded-Proto'); + const port = findAny(event, 'x-forwarded-port', 'X-Forwarded-Port'); + if (!(proto && host && (event.path || event.rawPath))) { + return undefined; + } + let answer = `${proto}://${host}`; + if (port) { + answer += `:${port}`; + } + answer += event.path ?? event.rawPath; + if (event.queryStringParameters) { + let first = true; + for (const key in event.queryStringParameters) { + answer += first ? '?' : '&'; + answer += encodeURIComponent(key); + answer += '='; + answer += encodeURIComponent(event.queryStringParameters[key]); + first = false; + } + } + return answer; + } + + /** + * + */ + private static _determineParent( + event: any, + context: Context, + eventContextExtractor: EventContextExtractor, + ): OtelContext { + const extractedContext = safeExecuteInTheMiddle( + () => eventContextExtractor(event, context), + e => { + if (e) diag.error('aws-lambda instrumentation: eventContextExtractor error', e); + }, + true, + ); + if (trace.getSpan(extractedContext)?.spanContext()) { + return extractedContext; + } + return ROOT_CONTEXT; + } +} diff --git a/packages/aws-serverless/src/integration/instrumentation-aws-lambda/internal-types.ts b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/internal-types.ts new file mode 100644 index 000000000000..34894e010fa1 --- /dev/null +++ b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/internal-types.ts @@ -0,0 +1,19 @@ +// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/cc7eff47e2e7bad7678241b766753d5bd6dbc85f/packages/instrumentation-aws-lambda/src/internal-types.ts +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Handler } from 'aws-lambda'; + +export type LambdaModule = Record; diff --git a/packages/aws-serverless/src/integration/instrumentation-aws-lambda/semconv.ts b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/semconv.ts new file mode 100644 index 000000000000..a10eff490322 --- /dev/null +++ b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/semconv.ts @@ -0,0 +1,29 @@ +// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/cc7eff47e2e7bad7678241b766753d5bd6dbc85f/packages/instrumentation-aws-lambda/src/semconv.ts +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This file contains a copy of unstable semantic convention definitions + * used by this package. + * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv + */ + +/** + * A boolean that is true if the serverless function is executed for the first time (aka cold-start). + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_FAAS_COLDSTART = 'faas.coldstart'; diff --git a/packages/aws-serverless/src/integration/instrumentation-aws-lambda/types.ts b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/types.ts new file mode 100644 index 000000000000..1b7603281ba0 --- /dev/null +++ b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/types.ts @@ -0,0 +1,39 @@ +// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/cc7eff47e2e7bad7678241b766753d5bd6dbc85f/packages/instrumentation-aws-lambda/src/types.ts +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Context as OtelContext, Span } from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import type { Context } from 'aws-lambda'; + +export type RequestHook = (span: Span, hookInfo: { event: any; context: Context }) => void; + +export type ResponseHook = ( + span: Span, + hookInfo: { + err?: Error | string | null; + res?: any; + }, +) => void; + +export type EventContextExtractor = (event: any, context: Context) => OtelContext; +export interface AwsLambdaInstrumentationConfig extends InstrumentationConfig { + requestHook?: RequestHook; + responseHook?: ResponseHook; + eventContextExtractor?: EventContextExtractor; + lambdaHandler?: string; + lambdaStartTime?: number; +} diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index 9bad62f3a848..29ca27f3aa07 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -1,23 +1,10 @@ -import type { Integration, Options, Scope } from '@sentry/core'; -import { applySdkMetadata, consoleSandbox, debug, getSDKSource } from '@sentry/core'; -import type { NodeClient, NodeOptions } from '@sentry/node'; -import { - captureException, - captureMessage, - flush, - getCurrentScope, - getDefaultIntegrationsWithoutPerformance, - initWithoutDefaultIntegrations, - withScope, -} from '@sentry/node'; +import type { Scope } from '@sentry/core'; +import { consoleSandbox, debug } from '@sentry/core'; +import { captureException, captureMessage, flush, getCurrentScope, withScope } from '@sentry/node'; import type { Context, Handler } from 'aws-lambda'; -import { existsSync } from 'fs'; -import { basename, resolve } from 'path'; import { performance } from 'perf_hooks'; import { types } from 'util'; import { DEBUG_BUILD } from './debug-build'; -import { awsIntegration } from './integration/aws'; -import { awsLambdaIntegration } from './integration/awslambda'; import { markEventUnhandled } from './utils'; const { isPromise } = types; @@ -53,42 +40,6 @@ export interface WrapperOptions { startTrace: boolean; } -/** - * Get the default integrations for the AWSLambda SDK. - */ -// NOTE: in awslambda-auto.ts, we also call the original `getDefaultIntegrations` from `@sentry/node` to load performance integrations. -// If at some point we need to filter a node integration out for good, we need to make sure to also filter it out there. -export function getDefaultIntegrations(_options: Options): Integration[] { - return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()]; -} - -/** - * Initializes the Sentry AWS Lambda SDK. - * - * @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}. - */ -export function init(options: NodeOptions = {}): NodeClient | undefined { - const opts = { - defaultIntegrations: getDefaultIntegrations(options), - ...options, - }; - - applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], getSDKSource()); - - return initWithoutDefaultIntegrations(opts); -} - -/** */ -function tryRequire(taskRoot: string, subdir: string, mod: string): T { - const lambdaStylePath = resolve(taskRoot, subdir, mod); - if (existsSync(lambdaStylePath) || existsSync(`${lambdaStylePath}.js`)) { - // Lambda-style path - return require(lambdaStylePath); - } - // Node-style path - return require(require.resolve(mod, { paths: [taskRoot, subdir] })); -} - /** */ function isPromiseAllSettledResult(result: T[]): boolean { return result.every( @@ -108,58 +59,6 @@ function getRejectedReasons(results: PromiseSettledResult[]): T[] { }, []); } -/** */ -export function tryPatchHandler(taskRoot: string, handlerPath: string): void { - type HandlerBag = HandlerModule | Handler | null | undefined; - - interface HandlerModule { - [key: string]: HandlerBag; - } - - const handlerDesc = basename(handlerPath); - const match = handlerDesc.match(/^([^.]*)\.(.*)$/); - if (!match) { - DEBUG_BUILD && debug.error(`Bad handler ${handlerDesc}`); - return; - } - - const [, handlerMod = '', handlerName = ''] = match; - - let obj: HandlerBag; - try { - const handlerDir = handlerPath.substring(0, handlerPath.indexOf(handlerDesc)); - obj = tryRequire(taskRoot, handlerDir, handlerMod); - } catch (e) { - DEBUG_BUILD && debug.error(`Cannot require ${handlerPath} in ${taskRoot}`, e); - return; - } - - let mod: HandlerBag; - let functionName: string | undefined; - handlerName.split('.').forEach(name => { - mod = obj; - obj = obj && (obj as HandlerModule)[name]; - functionName = name; - }); - if (!obj) { - DEBUG_BUILD && debug.error(`${handlerPath} is undefined or not exported`); - return; - } - if (typeof obj !== 'function') { - DEBUG_BUILD && debug.error(`${handlerPath} is not a function`); - return; - } - - // Check for prototype pollution - if (functionName === '__proto__' || functionName === 'constructor' || functionName === 'prototype') { - DEBUG_BUILD && debug.error(`Invalid handler name: ${functionName}`); - return; - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (mod as HandlerModule)[functionName!] = wrapHandler(obj); -} - /** * Tries to invoke context.getRemainingTimeInMillis if not available returns 0 * Some environments use AWS lambda but don't support this function diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts index 648ef4caeaec..58bb04a234b9 100644 --- a/packages/aws-serverless/test/sdk.test.ts +++ b/packages/aws-serverless/test/sdk.test.ts @@ -1,7 +1,8 @@ import type { Event } from '@sentry/core'; import type { Callback, Handler } from 'aws-lambda'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { init, wrapHandler } from '../src/sdk'; +import { init } from '../src/init'; +import { wrapHandler } from '../src/sdk'; const mockFlush = vi.fn((...args) => Promise.resolve(args)); const mockWithScope = vi.fn(); diff --git a/yarn.lock b/yarn.lock index 4175e91b712c..16f91521a991 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5876,15 +5876,6 @@ "@opentelemetry/instrumentation" "^0.203.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-aws-lambda@0.54.0": - version "0.54.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.54.0.tgz#835263593aa988ec460e840d3d47110392aaf92e" - integrity sha512-uiYI+kcMUJ/H9cxAwB8c9CaG8behLRgcYSOEA8M/tMQ54Y1ZmzAuEE3QKOi21/s30x5Q+by9g7BwiVfDtqzeMA== - dependencies: - "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.27.0" - "@types/aws-lambda" "8.10.150" - "@opentelemetry/instrumentation-aws-sdk@0.56.0": version "0.56.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.56.0.tgz#a65cd88351b7bd8566413798764679295166754a" @@ -6140,10 +6131,10 @@ "@opentelemetry/resources" "2.0.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0": - version "1.34.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.34.0.tgz#8b6a46681b38a4d5947214033ac48128328c1738" - integrity sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA== +"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0": + version "1.36.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.36.0.tgz#149449bd4df4d0464220915ad4164121e0d75d4d" + integrity sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ== "@opentelemetry/sql-common@^0.41.0": version "0.41.0" @@ -7912,7 +7903,7 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== -"@types/aws-lambda@8.10.150", "@types/aws-lambda@^8.10.62": +"@types/aws-lambda@^8.10.62": version "8.10.150" resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.150.tgz#4998b238750ec389a326a7cdb625808834036bd3" integrity sha512-AX+AbjH/rH5ezX1fbK8onC/a+HyQHo7QGmvoxAE42n22OsciAxvZoZNEr22tbXs8WfP1nIsBjKDpgPm3HjOZbA==