-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement error handling and tracing for Google Cloud functions.
- Loading branch information
1 parent
9443027
commit b4a29be
Showing
11 changed files
with
734 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// '@google-cloud/functions-framework/build/src/functions' import is expected to be type-only so it's erased in the final .js file. | ||
// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. | ||
import { | ||
CloudEventFunction, | ||
CloudEventFunctionWithCallback, | ||
} from '@google-cloud/functions-framework/build/src/functions'; | ||
import { flush, getCurrentHub, startTransaction } from '@sentry/node'; | ||
import { logger } from '@sentry/utils'; | ||
|
||
import { captureEventError, getActiveDomain, WrapperOptions } from './general'; | ||
|
||
export type CloudEventFunctionWrapperOptions = WrapperOptions; | ||
|
||
/** | ||
* Wraps an event function handler adding it error capture and tracing capabilities. | ||
* | ||
* @param fn Event handler | ||
* @param options Options | ||
* @returns Event handler | ||
*/ | ||
export function wrapCloudEventFunction( | ||
fn: CloudEventFunction | CloudEventFunctionWithCallback, | ||
wrapOptions: Partial<CloudEventFunctionWrapperOptions> = {}, | ||
): CloudEventFunctionWithCallback { | ||
const options: CloudEventFunctionWrapperOptions = { | ||
flushTimeout: 2000, | ||
...wrapOptions, | ||
}; | ||
return (context, callback) => { | ||
const transaction = startTransaction({ | ||
name: context.type || '<unknown>', | ||
op: 'gcp.function.cloud_event', | ||
}); | ||
|
||
// We put the transaction on the scope so users can attach children to it | ||
getCurrentHub().configureScope(scope => { | ||
scope.setSpan(transaction); | ||
}); | ||
|
||
const activeDomain = getActiveDomain(); | ||
|
||
activeDomain.on('error', err => { | ||
captureEventError(err, context); | ||
}); | ||
|
||
const newCallback = activeDomain.bind((...args: unknown[]) => { | ||
if (args[0] !== null && args[0] !== undefined) { | ||
captureEventError(args[0], context); | ||
} | ||
transaction.finish(); | ||
|
||
flush(options.flushTimeout) | ||
.then(() => { | ||
callback(...args); | ||
}) | ||
.then(null, e => { | ||
logger.error(e); | ||
}); | ||
}); | ||
|
||
if (fn.length > 1) { | ||
return (fn as CloudEventFunctionWithCallback)(context, newCallback); | ||
} | ||
|
||
Promise.resolve() | ||
.then(() => (fn as CloudEventFunction)(context)) | ||
.then( | ||
result => { | ||
newCallback(null, result); | ||
}, | ||
err => { | ||
newCallback(err, undefined); | ||
}, | ||
); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// '@google-cloud/functions-framework/build/src/functions' import is expected to be type-only so it's erased in the final .js file. | ||
// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. | ||
import { EventFunction, EventFunctionWithCallback } from '@google-cloud/functions-framework/build/src/functions'; | ||
import { flush, getCurrentHub, startTransaction } from '@sentry/node'; | ||
import { logger } from '@sentry/utils'; | ||
|
||
import { captureEventError, getActiveDomain, WrapperOptions } from './general'; | ||
|
||
export type EventFunctionWrapperOptions = WrapperOptions; | ||
|
||
/** | ||
* Wraps an event function handler adding it error capture and tracing capabilities. | ||
* | ||
* @param fn Event handler | ||
* @param options Options | ||
* @returns Event handler | ||
*/ | ||
export function wrapEventFunction( | ||
fn: EventFunction | EventFunctionWithCallback, | ||
wrapOptions: Partial<EventFunctionWrapperOptions> = {}, | ||
): EventFunctionWithCallback { | ||
const options: EventFunctionWrapperOptions = { | ||
flushTimeout: 2000, | ||
...wrapOptions, | ||
}; | ||
return (data, context, callback) => { | ||
const transaction = startTransaction({ | ||
name: context.eventType, | ||
op: 'gcp.function.event', | ||
}); | ||
|
||
// We put the transaction on the scope so users can attach children to it | ||
getCurrentHub().configureScope(scope => { | ||
scope.setSpan(transaction); | ||
}); | ||
|
||
const activeDomain = getActiveDomain(); | ||
|
||
activeDomain.on('error', err => { | ||
captureEventError(err, context); | ||
}); | ||
|
||
const newCallback = activeDomain.bind((...args: unknown[]) => { | ||
if (args[0] !== null && args[0] !== undefined) { | ||
captureEventError(args[0], context); | ||
} | ||
transaction.finish(); | ||
|
||
flush(options.flushTimeout) | ||
.then(() => { | ||
callback(...args); | ||
}) | ||
.then(null, e => { | ||
logger.error(e); | ||
}); | ||
}); | ||
|
||
if (fn.length > 2) { | ||
return (fn as EventFunctionWithCallback)(data, context, newCallback); | ||
} | ||
|
||
Promise.resolve() | ||
.then(() => (fn as EventFunction)(data, context)) | ||
.then( | ||
result => { | ||
newCallback(null, result); | ||
}, | ||
err => { | ||
newCallback(err, undefined); | ||
}, | ||
); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// '@google-cloud/functions-framework/build/src/functions' import is expected to be type-only so it's erased in the final .js file. | ||
// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. | ||
import { Context } from '@google-cloud/functions-framework/build/src/functions'; | ||
import { captureException, Scope, SDK_VERSION, withScope } from '@sentry/node'; | ||
import { Context as SentryContext } from '@sentry/types'; | ||
import { addExceptionMechanism } from '@sentry/utils'; | ||
import * as domain from 'domain'; | ||
import { hostname } from 'os'; | ||
|
||
export interface WrapperOptions { | ||
flushTimeout: number; | ||
} | ||
|
||
/** | ||
* Capture exception with additional event information. | ||
* | ||
* @param e exception to be captured | ||
* @param context event context | ||
*/ | ||
export function captureEventError(e: unknown, context: Context): void { | ||
withScope(scope => { | ||
addServerlessEventProcessor(scope); | ||
scope.setContext('runtime', { | ||
name: 'node', | ||
version: global.process.version, | ||
}); | ||
scope.setTag('server_name', process.env.SENTRY_NAME || hostname()); | ||
scope.setContext('gcp.function.context', { ...context } as SentryContext); | ||
captureException(e); | ||
}); | ||
} | ||
|
||
/** | ||
* Add event processor that will override SDK details to point to the serverless SDK instead of Node, | ||
* as well as set correct mechanism type, which should be set to `handled: false`. | ||
* We do it like this, so that we don't introduce any side-effects in this module, which makes it tree-shakeable. | ||
* @param scope Scope that processor should be added to | ||
*/ | ||
export function addServerlessEventProcessor(scope: Scope): void { | ||
scope.addEventProcessor(event => { | ||
event.sdk = { | ||
...event.sdk, | ||
name: 'sentry.javascript.serverless', | ||
integrations: [...((event.sdk && event.sdk.integrations) || []), 'GCPFunction'], | ||
packages: [ | ||
...((event.sdk && event.sdk.packages) || []), | ||
{ | ||
name: 'npm:@sentry/serverless', | ||
version: SDK_VERSION, | ||
}, | ||
], | ||
version: SDK_VERSION, | ||
}; | ||
|
||
addExceptionMechanism(event, { | ||
handled: false, | ||
}); | ||
|
||
return event; | ||
}); | ||
} | ||
|
||
/** | ||
* @returns Current active domain with a correct type. | ||
*/ | ||
export function getActiveDomain(): domain.Domain { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access | ||
return (domain as any).active as domain.Domain; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// '@google-cloud/functions-framework/build/src/functions' import is expected to be type-only so it's erased in the final .js file. | ||
// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. | ||
import { HttpFunction } from '@google-cloud/functions-framework/build/src/functions'; | ||
import { captureException, flush, getCurrentHub, Handlers, startTransaction, withScope } from '@sentry/node'; | ||
import { logger, stripUrlQueryAndFragment } from '@sentry/utils'; | ||
|
||
import { addServerlessEventProcessor, getActiveDomain, WrapperOptions } from './general'; | ||
|
||
type Request = Parameters<HttpFunction>[0]; | ||
type Response = Parameters<HttpFunction>[1]; | ||
type ParseRequestOptions = Handlers.ParseRequestOptions; | ||
|
||
export interface HttpFunctionWrapperOptions extends WrapperOptions { | ||
parseRequestOptions: ParseRequestOptions; | ||
} | ||
|
||
export { Request, Response }; | ||
|
||
const { parseRequest } = Handlers; | ||
|
||
/** | ||
* Capture exception with additional request information. | ||
* | ||
* @param e exception to be captured | ||
* @param req incoming request | ||
* @param options request capture options | ||
*/ | ||
function captureRequestError(e: unknown, req: Request, options: ParseRequestOptions): void { | ||
withScope(scope => { | ||
addServerlessEventProcessor(scope); | ||
scope.addEventProcessor(event => parseRequest(event, req, options)); | ||
captureException(e); | ||
}); | ||
} | ||
|
||
/** | ||
* Wraps an HTTP function handler adding it error capture and tracing capabilities. | ||
* | ||
* @param fn HTTP Handler | ||
* @param options Options | ||
* @returns HTTP handler | ||
*/ | ||
export function wrapHttpFunction( | ||
fn: HttpFunction, | ||
wrapOptions: Partial<HttpFunctionWrapperOptions> = {}, | ||
): HttpFunction { | ||
const options: HttpFunctionWrapperOptions = { | ||
flushTimeout: 2000, | ||
parseRequestOptions: {}, | ||
...wrapOptions, | ||
}; | ||
return (req, res) => { | ||
const reqMethod = (req.method || '').toUpperCase(); | ||
const reqUrl = req.url && stripUrlQueryAndFragment(req.url); | ||
|
||
const transaction = startTransaction({ | ||
name: `${reqMethod} ${reqUrl}`, | ||
op: 'gcp.function.http', | ||
}); | ||
|
||
// We put the transaction on the scope so users can attach children to it | ||
getCurrentHub().configureScope(scope => { | ||
scope.setSpan(transaction); | ||
}); | ||
|
||
// We also set __sentry_transaction on the response so people can grab the transaction there to add | ||
// spans to it later. | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access | ||
(res as any).__sentry_transaction = transaction; | ||
|
||
// functions-framework creates a domain for each incoming request so we take advantage of this fact and add an error handler. | ||
// BTW this is the only way to catch any exception occured during request lifecycle. | ||
getActiveDomain().on('error', err => { | ||
captureRequestError(err, req, options.parseRequestOptions); | ||
}); | ||
|
||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
const _end = res.end; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
res.end = function(chunk?: any | (() => void), encoding?: string | (() => void), cb?: () => void): void { | ||
transaction.setHttpStatus(res.statusCode); | ||
transaction.finish(); | ||
|
||
flush(options.flushTimeout) | ||
.then(() => { | ||
_end.call(this, chunk, encoding, cb); | ||
}) | ||
.then(null, e => { | ||
logger.error(e); | ||
}); | ||
}; | ||
|
||
return fn(req, res); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from './http'; | ||
export * from './events'; | ||
export * from './cloud_events'; | ||
export { init } from '@sentry/node'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
// https://medium.com/unsplash/named-namespace-imports-7345212bbffb | ||
import * as AWSLambda from './awslambda'; | ||
export { AWSLambda }; | ||
import * as GCPFunction from './gcpfunction'; | ||
export { AWSLambda, GCPFunction }; | ||
|
||
export * from '@sentry/node'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.