Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SUP-1281] AWS Lambda example app #1517

Draft
wants to merge 7 commits into
base: next
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions examples/aws-lambda/simple-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.aws-sam
86 changes: 86 additions & 0 deletions examples/aws-lambda/simple-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# AWS Lambda

This is an example project showing how to use `@bugsnag/js` with AWS Lambda.

This project was initialized with [`sam init`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-init.html).

## Prerequisites

- [SAM CLI tools](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
- [Docker](https://docs.docker.com/get-docker/) is installed and running on your machine (only if testing locally)

## Usage

Clone the repo and `cd` into the directory of this example:

```
git clone [email protected]:bugsnag/bugsnag-js.git
cd bugsnag-js/examples/aws-lambda/simple-app
```

- Bugsnag is started for each Lambda function with `Bugsnag.start` and the `@bugsnag/plugin-aws-lambda` plugin
- Errors in your Lambda are handled by wrapping your Lambda handler inside Bugsnag's handler
- Both async and callback handlers are supported, with examples given for both inside this app

Replace `<YOUR_BUGSNAG_API_KEY>` in `template.yaml` with your own.

### Install dependencies

A Lambda layer is used to share dependencies (Bugsnag) across the functions. From the project root, install the dependencies before invoking the functions:

```
cd dependencies-layer && npm install && cd ..
```

### Build

From the root directory:

```
sam build
```

### Run all functions locally

To run all the functions on a local server:

```
sam local start-api --host '127.0.0.1' -p '3000'
```

To avoid building the dependency layer image on each invocation, you can use the `--warm-containers [EAGER | LAZY]` option. `EAGER` loads the layer containers for all functions at startup + persists them between invocations. `LAZY` only loads the layer containers when each function is first invoked, and then persists them for further invocations.

Hit a function endpoint:

```
curl -X GET 'http://127.0.0.1:3000/async/handled-exception'
```

### Testing a single function

To test a single function using a sample [Event](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-concepts.html#gettingstarted-concepts-event):

```
sam local invoke "<RESOURCE_NAME>" -e <path/to/event.json>
```

e.g. to run the `CallbackHandledException` function:

```
sam local invoke "CallbackHandledException" -e events/callback/handled-exception.json
```

### Available functions

Bugsnag's `createHandler` supports both [`async`](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html#nodejs-handler-async) and [`callback`](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html#nodejs-handler-sync) handlers. Examples are given for both types of handler.
Function Name | Expected Response Code & Message | Purpose
--- | --- | ---
`AsyncUnhandledException`| 502 - Internal server error | Error returned from the handler
`AsyncHandledException` | 200 - Did not crash! | Call to `Bugsnag.notify` inside the handler, successful response
`AsyncPromiseRejection` | 502 - Internal server error | Promise rejected inside handler
`AsyncTimeout` | 502 - Internal server error | Function times out - Bugsnag notifies [`lambdaTimeoutNotifyMs`](https://docs.bugsnag.com/platforms/javascript/aws-lambda/#lambdatimeoutnotifyms) before timeout
`CallbackUnhandledException` | 502 - Internal server error | Error returned using handler's callback function
`CallbackThrownUnhandledException` | 502 - Internal server error | Error thrown inside the handler
`CallbackHandledException` | 200 - Did not crash! | Call to `Bugsnag.notify` inside the handler, successful response
`CallbackPromiseRejection` | 502 - Internal server error | Promise rejected inside handler
`CallbackTimeout` | 502 - Internal server error | Function times out - Bugsnag notifies [`lambdaTimeoutNotifyMs`](https://docs.bugsnag.com/platforms/javascript/aws-lambda/#lambdatimeoutnotifyms) before timeout
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Copy link

@himanshufize himanshufize Jun 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luke-belton Just one request. Can you add an example in which we call multiple functions and in those function error could occur but they should not break the flow of execution?

This in regard use case when failure in some place is OK but still be need log regarding those failures - One API request

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler()

const handler = async (event, _context) => {
Bugsnag.notify(new Error('Bad thing!'), (e) => {
e.context = "Don't worry - I handled it"
})
console.log('a handled error was sent to our dashboard!')
return {
statusCode: 200,
body: JSON.stringify({
route: event.path,
message: 'Did not crash!'
})
}
}

module.exports.lambdaHandler = bugsnagHandler(handler)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "async-handled-exception",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler()

const handler = async (_event, _context) => {
Promise.reject(new Error('yikes - a rejected promise!'))

return {
statusCode: 200,
body: JSON.stringify({ message: 'Did not crash!' })
}
}

module.exports.lambdaHandler = bugsnagHandler(handler)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "async-promise-rejection",
"version": "1.0.0"
}
23 changes: 23 additions & 0 deletions examples/aws-lambda/simple-app/async/timeout/async-timeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler({
lambdaTimeoutNotifyMs: 2000 // set to notify 2000ms before timeout (default)
})

// due to a bug in AWS lambda this will not notify when running with SAM CLI: https://github.com/aws/aws-sam-cli/issues/2519
const handler = async (_event, _context) => {
await new Promise((resolve) => setTimeout(resolve, 4000)) // function timeout is 3000ms as per template.yaml

return {
statusCode: 200,
body: JSON.stringify({ message: 'Did not crash!' })
}
}

module.exports.lambdaHandler = bugsnagHandler(handler)
4 changes: 4 additions & 0 deletions examples/aws-lambda/simple-app/async/timeout/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "async-timeout",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler()

const handler = async (_event, _context) => {
throw new Error('Bad thing!')
}

module.exports.lambdaHandler = bugsnagHandler(handler)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "async-unhandled-exception",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler()

const handler = (event, _context, callback) => {
Bugsnag.notify(new Error('Bad thing!'), (e) => {
e.context = "Don't worry - I handled it"
})
console.log('a handled error was sent to our dashboard!')
callback(null, {
statusCode: 200,
body: JSON.stringify({
route: event.path,
message: 'Did not crash!'
})
})
}

module.exports.lambdaHandler = bugsnagHandler(handler)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "callback-handled-exception",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler()

const handler = (_event, _context, callback) => {
Promise.reject(new Error('yikes - a rejected promise!'))

callback(null, {
statusCode: 200,
body: JSON.stringify({ message: 'Did not crash!' })
})
}

module.exports.lambdaHandler = bugsnagHandler(handler)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "callback-promise-rejection",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler()

const handler = (_event, _context, _callback) => {
throw new Error('Oh no!')
}

module.exports.lambdaHandler = bugsnagHandler(handler)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "callback-thrown-unhandled-exception",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler({
lambdaTimeoutNotifyMs: 2000 // set to notify 2000ms before timeout
})

// due to a bug in AWS lambda this will not notify when running with SAM CLI: https://github.com/aws/aws-sam-cli/issues/2519
const handler = async (_event, _context, callback) => {
await new Promise((resolve) => setTimeout(resolve, 4000)) // function timeout is 3000ms as per template.yaml

callback(null, {
statusCode: 200,
body: JSON.stringify({ message: 'Did not crash!' })
})
}

module.exports.lambdaHandler = bugsnagHandler(handler)
4 changes: 4 additions & 0 deletions examples/aws-lambda/simple-app/callback/timeout/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "callback-timeout",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const Bugsnag = require('@bugsnag/js')
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')

Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
plugins: [BugsnagPluginAwsLambda]
})

const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler()

const handler = (event, context, callback) => {
callback(new Error('Oh no!'))
}

module.exports.lambdaHandler = bugsnagHandler(handler)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "callback-unhandled-exception",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "simple-app-dependencies",
"version": "1.0.0",
"dependencies": {
"@bugsnag/js": "^7.16.1",
"@bugsnag/plugin-aws-lambda": "^7.16.1"
}
}

46 changes: 46 additions & 0 deletions examples/aws-lambda/simple-app/events/async/handled-exception.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"body": "",
"resource": "/{proxy+}",
"path": "/async/handled-exception",
"httpMethod": "GET",
"isBase64Encoded": false,
"queryStringParameters": {},
"multiValueQueryStringParameters": {},
"pathParameters": {},
"stageVariables": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, sdch",
"Accept-Language": "en-US,en;q=0.8",
"Cache-Control": "max-age=0",
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Custom User Agent String"
},
"requestContext": {
"accountId": "123456789012",
"resourceId": "123456",
"stage": "prod",
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
"requestTime": "09/Apr/2015:12:34:56 +0000",
"requestTimeEpoch": 1428582896000,
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"accessKey": null,
"sourceIp": "127.0.0.1",
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "Custom User Agent String",
"user": null
},
"path": "/prod/async/handled-exception",
"resourcePath": "/{proxy+}",
"httpMethod": "GET",
"apiId": "1234567890",
"protocol": "HTTP/1.1"
}
}
Loading