Skip to content

Commit

Permalink
ref(serverless): Use startSpanManual() instead of `startTransaction…
Browse files Browse the repository at this point in the history
…()` (#9970)

Also refactor it to use `continueTrace`.

Note that this already uses the "new" way of `startSpanManual` which
ignores the `finish` function.

---------

Co-authored-by: Lukas Stracke <[email protected]>
  • Loading branch information
mydea and Lms24 authored Dec 22, 2023
1 parent 2c0b6d3 commit eadfd61
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 414 deletions.
107 changes: 61 additions & 46 deletions packages/serverless/src/awslambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ import { hostname } from 'os';
import { basename, resolve } from 'path';
import { types } from 'util';
/* eslint-disable max-lines */
import type { Scope } from '@sentry/node';
import * as Sentry from '@sentry/node';
import { captureException, captureMessage, flush, getCurrentHub, withScope } from '@sentry/node';
import type { Integration, SdkMetadata } from '@sentry/types';
import { isString, logger, tracingContextFromHeaders } from '@sentry/utils';
import type { NodeOptions, Scope } from '@sentry/node';
import { SDK_VERSION } from '@sentry/node';
import {
captureException,
captureMessage,
continueTrace,
defaultIntegrations as nodeDefaultIntegrations,
flush,
getCurrentScope,
init as initNode,
startSpanManual,
withScope,
} from '@sentry/node';
import type { Integration, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';
Expand Down Expand Up @@ -55,9 +65,9 @@ export interface WrapperOptions {
startTrace: boolean;
}

export const defaultIntegrations: Integration[] = [...Sentry.defaultIntegrations, new AWSServices({ optional: true })];
export const defaultIntegrations: Integration[] = [...nodeDefaultIntegrations, new AWSServices({ optional: true })];

interface AWSLambdaOptions extends Sentry.NodeOptions {
interface AWSLambdaOptions extends NodeOptions {
/**
* Internal field that is set to `true` when init() is called by the Sentry AWS Lambda layer.
*
Expand All @@ -66,7 +76,9 @@ interface AWSLambdaOptions extends Sentry.NodeOptions {
}

/**
* @see {@link Sentry.init}
* Initializes the Sentry AWS Lambda SDK.
*
* @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}.
*/
export function init(options: AWSLambdaOptions = {}): void {
const opts = {
Expand All @@ -81,13 +93,13 @@ export function init(options: AWSLambdaOptions = {}): void {
packages: [
{
name: 'npm:@sentry/serverless',
version: Sentry.SDK_VERSION,
version: SDK_VERSION,
},
],
version: Sentry.SDK_VERSION,
version: SDK_VERSION,
};

Sentry.init(opts);
initNode(opts);
}

/** */
Expand Down Expand Up @@ -290,44 +302,13 @@ export function wrapHandler<TEvent, TResult>(
}, timeoutWarningDelay) as unknown as NodeJS.Timeout;
}

const hub = getCurrentHub();

let transaction: Sentry.Transaction | undefined;
if (options.startTrace) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;
const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
sentryTrace,
baggage,
);
Sentry.getCurrentScope().setPropagationContext(propagationContext);

transaction = hub.startTransaction({
name: context.functionName,
op: 'function.aws.lambda',
origin: 'auto.function.serverless',
...traceparentData,
metadata: {
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
source: 'component',
},
});
}
async function processResult(span?: Span): Promise<TResult> {
const scope = getCurrentScope();

return withScope(async scope => {
let rv: TResult;
try {
enhanceScopeWithEnvironmentData(scope, context, START_TIME);
if (options.startTrace) {
enhanceScopeWithTransactionData(scope, context);
// We put the transaction on the scope so users can attach children to it
scope.setSpan(transaction);
}

rv = await asyncHandler(event, context);

// We manage lambdas that use Promise.allSettled by capturing the errors of failed promises
Expand All @@ -342,12 +323,46 @@ export function wrapHandler<TEvent, TResult>(
throw e;
} finally {
clearTimeout(timeoutWarningTimer);
transaction?.end();
span?.end();
await flush(options.flushTimeout).catch(e => {
DEBUG_BUILD && logger.error(e);
});
}
return rv;
}

if (options.startTrace) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

const continueTraceContext = continueTrace({ sentryTrace, baggage });

return startSpanManual(
{
name: context.functionName,
op: 'function.aws.lambda',
origin: 'auto.function.serverless',
...continueTraceContext,
metadata: {
...continueTraceContext.metadata,
source: 'component',
},
},
span => {
enhanceScopeWithTransactionData(getCurrentScope(), context);

return processResult(span);
},
);
}

return withScope(async () => {
return processResult(undefined);
});
};
}
101 changes: 48 additions & 53 deletions packages/serverless/src/gcpfunction/cloud_events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { captureException, flush, getCurrentHub, getCurrentScope } from '@sentry/node';
import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node';
import { isThenable, logger } from '@sentry/utils';

import { DEBUG_BUILD } from '../debug-build';
Expand All @@ -21,7 +21,6 @@ export function wrapCloudEventFunction(
return proxyFunction(fn, f => domainify(_wrapCloudEventFunction(f, wrapOptions)));
}

/** */
function _wrapCloudEventFunction(
fn: CloudEventFunction | CloudEventFunctionWithCallback,
wrapOptions: Partial<CloudEventFunctionWrapperOptions> = {},
Expand All @@ -31,63 +30,59 @@ function _wrapCloudEventFunction(
...wrapOptions,
};
return (context, callback) => {
const hub = getCurrentHub();
return startSpanManual(
{
name: context.type || '<unknown>',
op: 'function.gcp.cloud_event',
origin: 'auto.function.serverless.gcp_cloud_event',
metadata: { source: 'component' },
},
span => {
const scope = getCurrentScope();
scope.setContext('gcp.function.context', { ...context });

const transaction = hub.startTransaction({
name: context.type || '<unknown>',
op: 'function.gcp.cloud_event',
origin: 'auto.function.serverless.gcp_cloud_event',
metadata: { source: 'component' },
}) as ReturnType<typeof hub.startTransaction> | undefined;
const newCallback = domainify((...args: unknown[]) => {
if (args[0] !== null && args[0] !== undefined) {
captureException(args[0], scope => markEventUnhandled(scope));
}
span?.end();

// getCurrentHub() is expected to use current active domain as a carrier
// since functions-framework creates a domain for each incoming request.
// So adding of event processors every time should not lead to memory bloat.
const scope = getCurrentScope();
scope.setContext('gcp.function.context', { ...context });
// We put the transaction on the scope so users can attach children to it
scope.setSpan(transaction);

const newCallback = domainify((...args: unknown[]) => {
if (args[0] !== null && args[0] !== undefined) {
captureException(args[0], scope => markEventUnhandled(scope));
}
transaction?.end();

// eslint-disable-next-line @typescript-eslint/no-floating-promises
flush(options.flushTimeout)
.then(null, e => {
DEBUG_BUILD && logger.error(e);
})
.then(() => {
callback(...args);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
flush(options.flushTimeout)
.then(null, e => {
DEBUG_BUILD && logger.error(e);
})
.then(() => {
callback(...args);
});
});
});

if (fn.length > 1) {
let fnResult;
try {
fnResult = (fn as CloudEventFunctionWithCallback)(context, newCallback);
} catch (err) {
captureException(err, scope => markEventUnhandled(scope));
throw err;
}
if (fn.length > 1) {
let fnResult;
try {
fnResult = (fn as CloudEventFunctionWithCallback)(context, newCallback);
} catch (err) {
captureException(err, scope => markEventUnhandled(scope));
throw err;
}

if (isThenable(fnResult)) {
fnResult.then(null, err => {
captureException(err, scope => markEventUnhandled(scope));
throw err;
});
}
if (isThenable(fnResult)) {
fnResult.then(null, err => {
captureException(err, scope => markEventUnhandled(scope));
throw err;
});
}

return fnResult;
}
return fnResult;
}

return Promise.resolve()
.then(() => (fn as CloudEventFunction)(context))
.then(
result => newCallback(null, result),
err => newCallback(err, undefined),
);
return Promise.resolve()
.then(() => (fn as CloudEventFunction)(context))
.then(
result => newCallback(null, result),
err => newCallback(err, undefined),
);
},
);
};
}
Loading

0 comments on commit eadfd61

Please sign in to comment.