Skip to content

Commit 9cc654c

Browse files
committed
commit project
1 parent d551d93 commit 9cc654c

11 files changed

+2074
-2
lines changed

.eslintrc.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = {
2+
"extends": "airbnb-base",
3+
"plugins": [
4+
"import"
5+
],
6+
"rules": {
7+
"no-console": "off",
8+
"no-debugger": "off",
9+
"comma-dangle": ["error", {
10+
"arrays": "only-multiline",
11+
"objects": "only-multiline",
12+
"imports": "only-multiline",
13+
"exports": "only-multiline",
14+
"functions": "never"
15+
}]
16+
}
17+
};

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# macOS
2+
.DS_Store
3+
14
# Logs
25
logs
36
*.log

.vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"eslint.enable": true
3+
}

Dockerfile

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM node:8-alpine
2+
3+
WORKDIR /workdir
4+
ADD . /workdir
5+
6+
EXPOSE 8080
7+
EXPOSE 9229
8+
RUN npm install
9+
10+
WORKDIR /workdir/broker
11+
CMD ["node", "index.js"]

README.md

+163-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,163 @@
1-
# aws-lambda-debugger
2-
Debugging support for AWS Lambda Node 6.10+ (no joke)
1+
# AWS Lambda Debugger
2+
3+
Do you want to step through code running live in Lambda? Do you want to fix bugs faster?
4+
Do you want free pizza?
5+
6+
This project will help you with the first 2 questions. When you show it to a friend,
7+
you might get that 3rd one too :)
8+
9+
*This is only for the AWS Node 6.10 runtime*
10+
11+
## Isn't this impossible?
12+
13+
No. Well, not anymore.
14+
15+
## How?
16+
17+
Normally, debugging is a one hop process: developer's debugger connect directly to the process. This *is* impossible with Lambda.
18+
19+
However, we fork your code to a separate child process that is
20+
running in debug mode and connected to the original via AN interprocess communication
21+
channel. The parent process opens 2 WebSockets as well: one to the child process'
22+
V8 Inspector and the other to a broker server, becoming a proxy between the 2
23+
connections. Next, the developer connects a debugger to the broker server, which
24+
connects them to the proxy, which is connected to the child's debugger port.
25+
Now, you have a 3 hop chain like this:
26+
27+
```
28+
Debugger <=> Broker <=> Proxy <=> Child
29+
```
30+
31+
Once the developer's debugger connects to the broker server, the debug
32+
version of the handler is executed. The proxy and the child coordinate to
33+
shim the event, context, and callback. *Result:* the developer is connected
34+
to a live running Lambda function with full control.
35+
36+
Oh, and you only have to add one line of code *at the end* of your handler file(s)...
37+
38+
## I want one!
39+
40+
Good. There are 5 steps:
41+
42+
1. Deploy the broker server
43+
2. Add the proxy to your code
44+
3. Configure the proxy via environment variables
45+
4. Increase your Lambda timeout
46+
5. Use it!
47+
48+
### Deploy the broker server
49+
50+
- Kick off an EC2 Amazon Linux Instance
51+
- Attach Security Group
52+
- exposing ports `8080` and `9229` to `0.0.0.0/0`
53+
- expose port 22 to [YOUR IP](https://www.google.com/search?q=whats+my+ip)
54+
- SSH in to the box
55+
56+
```bash
57+
# Install Docker
58+
sudo yum update -y
59+
sudo yum install -y docker
60+
sudo service docker start
61+
62+
# Run the Broker
63+
docker run --name debug-broker \
64+
-d -p 8080:8080 -p 9229:9229 \
65+
trek10/aws-lambda-debugger-broker --restart always
66+
67+
# To view logs
68+
docker logs debug-broker
69+
```
70+
71+
### Add the proxy to your code
72+
73+
Add the package to your repo:
74+
75+
```bash
76+
npm install aws-lambda-debugger --save
77+
```
78+
79+
Require the package at the very end of each file that contains a Lambda handler
80+
that you want to debug. Example:
81+
82+
```javascript
83+
module.exports.myHandler = (event, context, callback) => {
84+
// put some code that you want to debug here
85+
}
86+
87+
require('aws-lambda-debugger');
88+
```
89+
90+
That's it!!!
91+
92+
### Configure the proxy via environment variables
93+
94+
There are 3 magic environment variables that need to be set:
95+
96+
- `DEBUGGER_ACTIVE`: As long as this value is present and it is not 'false'
97+
or empty string, the proxy will do its job.
98+
- `DEBUGGER_BROKER_ADDRESS`: This is the IP address or domain for the broker server.
99+
- `DEBUGGER_FUNCTION_ID`: This is a unique ID (per function!) that is used to route the
100+
debugger to the appropriate function. It is used as part of the URL to connect.
101+
102+
### Increase your Lambda timeout
103+
104+
This is pretty straight forward. Alter the timeout to 300 seconds to allow
105+
maximum debug time.
106+
107+
### Use it!
108+
109+
1. Launch your Lambda function (from console, via CLI, etc)
110+
2. Replace the `DEBUGGER_BROKER_ADDRESS` and `DEBUGGER_FUNCTION_ID` in the following URL
111+
and paste it into Chrome.
112+
```chrome-devtools://devtools/remote/serve_file/@60cd6e859b9f557d2312f5bf532f6aec5f284980/inspector.html?experiments=true&v8only=true&ws=[DEBUGGER_BROKER_ADDRESS]:9229/[DEBUGGER_FUNCTION_ID]```
113+
3. DEBUG!!!
114+
115+
## What's the catch?
116+
117+
There are a few catches/known issues:
118+
119+
- Multiple `console.log` calls close together sometimes causes them to be
120+
aggregated in a single CloudWatch Log entry
121+
- This only works with the AWS Node 6.10 runtime. We expect it to work with
122+
Node 8 whenever AWS offers support for it too.
123+
- Chrome DevTools is the only debugger that we have proven to work. YMMV with
124+
your IDE.
125+
- You pay for your debug time. No way around this. It is a running Lambda
126+
function after all.
127+
- `context.callbackWaitsForEmptyEventLoop` is defaulted to `false`. You can
128+
change it back to `true` (we even shimmed that!), but your function *will*
129+
run until it times out if you do.
130+
- `context.getRemainingTimeInMillis` is technically an approximation. We
131+
grab the remaining time and the current timestamp when the debugger connects
132+
and ship both to the child. The time delta is then subtracted from the original
133+
time. Since all times are retrieved inside of the Lambda, this should be a
134+
*very close* approximation.
135+
136+
## Anything else I should know?
137+
138+
Functionally, this thing is complete. However it is still very new,
139+
so don't be surprised if something goes wrong. Feel free to file an issue.
140+
We're happy to take PRs too.
141+
142+
## Items that need work still
143+
144+
- Tests: There are no tests. Because of all of the dark arts, eventing stuff,
145+
and the sockets, we didn't want to delay release because the tests are going
146+
to be hard to write. If you're a ninja at this, feel free to reach out.
147+
- Improving logging in the broker server
148+
- Bulletproof socket cleanup: We're still trying to make sure that all sockets
149+
are cleaned up as fast as possible. If you find any leaks, please let us know.
150+
- Study memory usage in the broker.
151+
152+
## Future ideas:
153+
154+
- Make things more configurable
155+
- Web UI for broker server
156+
- Get direct link as soon as Lambda connects to broker
157+
- Track remaining time
158+
159+
**Made with :gift_heart: and :sparkles:magic:sparkles: by [Rob Ribeiro](https://github.com/azurelogic) + [Trek10](https://www.trek10.com/)**
160+
161+
P.S.: We do AWS consulting and monitoring. Come talk to us.
162+
163+
Twitter: [Rob](https://twitter.com/azurelogic) + [Trek10](https://twitter.com/trek10inc)

broker/index.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const WebSocket = require('ws');
2+
const types = require('../lib/messageTypes');
3+
const util = require('util');
4+
5+
const serialize = x => util.inspect(x, { depth: null });
6+
const log = (message) => {
7+
console.log(`${new Date().toISOString()}: ${message}`);
8+
};
9+
10+
const lambdaServer = new WebSocket.Server({ port: 8080 });
11+
const userServer = new WebSocket.Server({ port: 9229 });
12+
13+
let socketCache = [];
14+
15+
// todo: study memory usage under heavy debugging for docs
16+
// todo: keep improving connection cleanup, e.g.: abandoned connections
17+
// todo: structured logging
18+
19+
// configure lambda facing socket server
20+
lambdaServer.on('connection', (proxySocket, request) => {
21+
log(`Lambda connection incoming with function ID: ${serialize(request.url)}`);
22+
const foundCacheRecord = socketCache.find(cacheRecord => cacheRecord.key === request.url);
23+
if (foundCacheRecord) {
24+
log('Found conflicting key in cache. Terminating connection.');
25+
proxySocket.close();
26+
} else {
27+
log(`Registering proxy in cache under key: ${request.url}`);
28+
socketCache.push({
29+
key: request.url,
30+
proxySocket,
31+
});
32+
}
33+
34+
proxySocket.on('error', (error) => {
35+
log(`Proxy socket error: ${serialize(error)}`);
36+
});
37+
38+
proxySocket.on('close', () => {
39+
log(`Proxy socket initiated closure. Closing connections associated with key: ${serialize(request.url)}`);
40+
const cacheRecord = socketCache.find(record => record.key === request.url);
41+
if (cacheRecord && cacheRecord.userSocket) {
42+
if (cacheRecord.userSocket.readyState === WebSocket.OPEN) cacheRecord.userSocket.close();
43+
socketCache = socketCache.filter(record => record.key !== request.url);
44+
}
45+
});
46+
});
47+
48+
// configure user facing socket server
49+
userServer.on('connection', (userSocket, request) => {
50+
log(`User connection incoming with function ID: ${serialize(request.url)}`);
51+
let proxySocket;
52+
const foundCacheRecord = socketCache.find(record => record.key === request.url);
53+
if (foundCacheRecord) {
54+
// kick anything after the first debugger or if the proxy socket isn't open
55+
log('Found associated proxy in cache.');
56+
if ((foundCacheRecord.userSocket) ||
57+
foundCacheRecord.proxySocket.readyState !== WebSocket.OPEN) {
58+
log('Associated proxy already has a user connection. Terminating connection.');
59+
userSocket.close();
60+
return;
61+
}
62+
proxySocket = foundCacheRecord.proxySocket; // eslint-disable-line
63+
foundCacheRecord.userSocket = userSocket;
64+
log('Notifying associated proxy of user connection.');
65+
proxySocket.send(JSON.stringify({ type: types.USER_CONNECTED }));
66+
} else {
67+
// kick when lambda isn't connected
68+
log('No associated proxy found in cache. Terminating connection.');
69+
userSocket.close();
70+
return;
71+
}
72+
73+
// pass along V8 inspector messages
74+
userSocket.on('message', (message) => {
75+
if (proxySocket.readyState === WebSocket.OPEN) {
76+
proxySocket.send(JSON.stringify({ type: types.V8_INSPECTOR_MESSAGE, payload: message }));
77+
}
78+
});
79+
80+
userSocket.on('error', (error) => {
81+
log(`User socket error: ${serialize(error)}`);
82+
});
83+
84+
userSocket.on('close', () => {
85+
log(`User socket initiated closure. Closing connections associated with request: ${serialize(request.url)}`);
86+
const cacheRecord = socketCache.find(record => record.key === request.url);
87+
if (cacheRecord && cacheRecord.proxySocket) {
88+
if (cacheRecord.proxySocket.readyState === WebSocket.OPEN) {
89+
cacheRecord.proxySocket.close();
90+
}
91+
socketCache = socketCache.filter(record => record.key !== request.url);
92+
}
93+
});
94+
95+
proxySocket.on('message', (messageString) => {
96+
const message = JSON.parse(messageString);
97+
switch (message.type) {
98+
case types.V8_INSPECTOR_MESSAGE: {
99+
if (userSocket.readyState === WebSocket.OPEN) {
100+
userSocket.send(message.payload);
101+
}
102+
break;
103+
}
104+
default: {
105+
break;
106+
}
107+
}
108+
});
109+
});
110+
111+
log('Broker started...');

lib/messageTypes.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const CHILD_READY = 'CHILD_READY';
2+
const USER_CONNECTED = 'USER_CONNECTED';
3+
const INVOKE_HANDLER = 'INVOKE_HANDLER';
4+
const V8_INSPECTOR_MESSAGE = 'V8_INSPECTOR_MESSAGE';
5+
const SET_CALLBACKWAITSFOREMPTYEVENTLOOP = 'SET_CALLBACKWAITSFOREMPTYEVENTLOOP';
6+
const LAMBDA_CALLBACK = 'LAMBDA_CALLBACK';
7+
8+
module.exports = {
9+
CHILD_READY,
10+
USER_CONNECTED,
11+
INVOKE_HANDLER,
12+
V8_INSPECTOR_MESSAGE,
13+
SET_CALLBACKWAITSFOREMPTYEVENTLOOP,
14+
LAMBDA_CALLBACK,
15+
};

0 commit comments

Comments
 (0)