Laravel Vapor cloud dashboard using only Cloudwatch cover image

Laravel Vapor cloud dashboard using only Cloudwatch

George Boot

tech

In a recent blogpost, Michael Dyrynda showed a dashboard he has build for monitoring thenping.me. As Laravel Vapor only includes a very basic dashboard, he created a custom one. He used Grafana Cloud to build the dashboard.

When I read the article, I wondered why he used Grafana as AWS itself also offers a similar service. In fact, the data that is used on the dashboard, comes from AWS Cloudwatch, and Cloudwatch itself has very powerful dashboard functionalities built in. Unlike Grafana, it can't (easily at least) read data from a wide range of external sources, but since that is not really the case for this project, I don't think it matters.

Because I believe this dash could just as well be build using native Cloudwatch, I challenged myself to do so. When I shared this idea in the Serverless Laravel Slack, people seemed very interested and asked me if I was willing to share the dashboard. I am willing to do just that.

Lambda Insights

By default, Lambda does not export memory statistics to Cloudwatch. The only way to get information about memory and cold starts, is by parsing the raw logs. This can be done using Cloudwatch Logs Insights, but these values can't be nicely shown on your dashboard.

A while ago, AWS announced Lambda Insights. This is basically an opt-in extension to your functions, that will export statistics like actual memory usage, network throughput and cpu cycles to Cloudwatch.

Enabling this extension can simply be done by adding a layer in your vapor.yml. Look up the layer for your region on this page.

Example vapor.yml:

1id: 123456
2name: my-awesome-app
3 
4environments:
5 production:
6 runtime: al2
7 layers:
8 - vapor:php-8.0:al2
9 - arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension:14

Unfortunately, I haven't found a way to enable Lambda Insights for docker-based deployments. No worries, the rest of the dash will still work.

Copy, Search, Replace, Paste, Enjoy

All developers want in the end, is a simple copy/paste solution. I am pleased to inform you that you are about to get that! In Cloudwatch, it is possible to export and import complete dashboards as json. Practically this means, that once you copy the below json, you will have to search and replace the following values:

  • Replace PROJECT_NAME with the name of your Vapor project, eg. awesome-site
  • Replace PROJECT_ENV with the environment of your deployment, eg. production. If you are using Docker deployments, add -d to the end of your env (production-d, staging-d, etc.)
  • Replace PROJECT_REGION with the AWS region your project lives in, eg. eu-west-1
  • Replace DATABASE_NAME with the name of your database as configured in Vapor, eg. my-app-db
  • Replace API_GATEWAY_ID with the ID of your projects API gateway. You can find this using the AWS Console. Example: yuttpf0t41

Note that the dashboard is made for API Gateway users. If you use a Application Elastic Load Balancer, an example is provided below the main json.

Once you have prepared the json in your editor of choice, head to the AWS Console to import the dashboard.

  • After logging in, go to Cloudwatch, and in the menu of the left, select Dashboards
  • Click Create dashboard, enter a name and once created, dismiss the popup to add your first graph as you won't need it
  • In the top bar, select Actions and click View/edit source.
  • Paste your prepared json, and hit Update
  • Hit Save Dashboard
  • Congrats, you now have an awesome dashboard!

Pro tip: view dashboard without loggin in

If you don't want to log in every time you want to check the dashboard (or want to permanently show it on a wall-mounted tv etc.), you can share your dashboard using a public url. Everyone with that url, will be able to view the dashboard and underlying metrics.

You can obtain such a public url, by clicking Actions in the top menu, and selecting Share dashboard. Pick Share your dashboard publicly, read the warning message and confirm.

Wanna express your gratitude with a donation? Become a (one-time) sponsor on GitHub Sponsor.

Have any questions? At the bottom of the page, you can comment on this post.

The dashboard json:

1{
2 "widgets": [
3 {
4 "height": 4,
5 "width": 8,
6 "y": 0,
7 "x": 8,
8 "type": "metric",
9 "properties": {
10 "metrics": [
11 [ "AWS/Lambda", "Invocations", "FunctionName", "vapor-PROJECT_NAME-PROJECT_ENV", { "color": "#2ca02c" } ],
12 [ ".", "Errors", ".", ".", { "color": "#ff7f0e" } ],
13 [ ".", "Duration", ".", ".", { "yAxis": "right", "color": "#1f77b4", "stat": "Average" } ]
14 ],
15 "view": "timeSeries",
16 "stacked": false,
17 "region": "PROJECT_REGION",
18 "stat": "Sum",
19 "period": 300,
20 "title": "Lambda Invocations - HTTP",
21 "yAxis": {
22 "right": {
23 "showUnits": true
24 },
25 "left": {
26 "showUnits": true
27 }
28 },
29 "liveData": false,
30 "legend": {
31 "position": "hidden"
32 }
33 }
34 },
35 {
36 "height": 4,
37 "width": 4,
38 "y": 12,
39 "x": 0,
40 "type": "metric",
41 "properties": {
42 "metrics": [
43 [ "AWS/RDS", "DatabaseConnections", "DBInstanceIdentifier", "DATABASE_NAME", { "color": "#2ca02c" } ]
44 ],
45 "view": "timeSeries",
46 "stacked": false,
47 "region": "PROJECT_REGION",
48 "stat": "Average",
49 "period": 300,
50 "annotations": {
51 "horizontal": [
52 {
53 "color": "#ff7f0e",
54 "label": "Warning",
55 "value": 60,
56 "fill": "above"
57 },
58 {
59 "color": "#d62728",
60 "label": "Max",
61 "value": 85,
62 "fill": "above"
63 }
64 ]
65 },
66 "yAxis": {
67 "left": {
68 "min": 0,
69 "showUnits": false
70 }
71 },
72 "title": "RDS Connections",
73 "legend": {
74 "position": "hidden"
75 }
76 }
77 },
78 {
79 "height": 4,
80 "width": 4,
81 "y": 12,
82 "x": 4,
83 "type": "metric",
84 "properties": {
85 "metrics": [
86 [ "AWS/RDS", "DiskQueueDepth", "DBInstanceIdentifier", "DATABASE_NAME" ]
87 ],
88 "view": "timeSeries",
89 "stacked": false,
90 "region": "PROJECT_REGION",
91 "stat": "Average",
92 "period": 300,
93 "yAxis": {
94 "left": {
95 "showUnits": false,
96 "min": 0
97 }
98 },
99 "title": "RDS Queue Depth",
100 "legend": {
101 "position": "hidden"
102 },
103 "annotations": {
104 "horizontal": [
105 {
106 "color": "#ff7f0e",
107 "label": "Warning",
108 "value": 2,
109 "fill": "above"
110 }
111 ]
112 }
113 }
114 },
115 {
116 "height": 4,
117 "width": 4,
118 "y": 12,
119 "x": 8,
120 "type": "metric",
121 "properties": {
122 "metrics": [
123 [ "AWS/RDS", "WriteIOPS", "DBInstanceIdentifier", "DATABASE_NAME", { "color": "#2ca02c" } ],
124 [ ".", "ReadIOPS", ".", ".", { "color": "#1f77b4" } ]
125 ],
126 "view": "timeSeries",
127 "stacked": false,
128 "region": "PROJECT_REGION",
129 "stat": "Average",
130 "period": 300,
131 "title": "RDS IOPs",
132 "legend": {
133 "position": "hidden"
134 },
135 "yAxis": {
136 "left": {
137 "min": 0
138 }
139 }
140 }
141 },
142 {
143 "height": 4,
144 "width": 8,
145 "y": 0,
146 "x": 16,
147 "type": "metric",
148 "properties": {
149 "metrics": [
150 [ "AWS/Lambda", "ConcurrentExecutions", "FunctionName", "vapor-PROJECT_NAME-PROJECT_ENV" ],
151 [ "...", "vapor-PROJECT_NAME-PROJECT_ENV-cli" ],
152 [ "...", "vapor-PROJECT_NAME-PROJECT_ENV-queue" ]
153 ],
154 "view": "timeSeries",
155 "stacked": false,
156 "region": "PROJECT_REGION",
157 "stat": "Maximum",
158 "period": 300,
159 "title": "Lambda Concurrent Invocations",
160 "yAxis": {
161 "right": {
162 "showUnits": true
163 },
164 "left": {
165 "min": 0
166 }
167 },
168 "liveData": false,
169 "legend": {
170 "position": "hidden"
171 }
172 }
173 },
174 {
175 "height": 4,
176 "width": 8,
177 "y": 4,
178 "x": 16,
179 "type": "metric",
180 "properties": {
181 "metrics": [
182 [ "AWS/Lambda", "Throttles", "FunctionName", "vapor-PROJECT_NAME-PROJECT_ENV" ],
183 [ "...", "vapor-PROJECT_NAME-PROJECT_ENV-cli" ],
184 [ "...", "vapor-PROJECT_NAME-PROJECT_ENV-queue" ]
185 ],
186 "view": "timeSeries",
187 "stacked": false,
188 "region": "PROJECT_REGION",
189 "stat": "Maximum",
190 "period": 300,
191 "title": "Lambda Throttles",
192 "yAxis": {
193 "right": {
194 "showUnits": true
195 }
196 },
197 "liveData": false,
198 "legend": {
199 "position": "hidden"
200 }
201 }
202 },
203 {
204 "height": 4,
205 "width": 8,
206 "y": 4,
207 "x": 8,
208 "type": "metric",
209 "properties": {
210 "metrics": [
211 [ "AWS/Lambda", "Invocations", "FunctionName", "vapor-PROJECT_NAME-PROJECT_ENV-cli", { "color": "#2ca02c" } ],
212 [ ".", "Errors", ".", ".", { "color": "#ff7f0e" } ],
213 [ ".", "Duration", ".", ".", { "yAxis": "right", "color": "#1f77b4", "stat": "Average" } ]
214 ],
215 "view": "timeSeries",
216 "stacked": false,
217 "region": "PROJECT_REGION",
218 "stat": "Sum",
219 "period": 300,
220 "title": "Lambda Invocations - CLI",
221 "yAxis": {
222 "right": {
223 "showUnits": true
224 },
225 "left": {
226 "showUnits": true
227 }
228 },
229 "liveData": false,
230 "legend": {
231 "position": "hidden"
232 }
233 }
234 },
235 {
236 "height": 4,
237 "width": 8,
238 "y": 8,
239 "x": 8,
240 "type": "metric",
241 "properties": {
242 "metrics": [
243 [ "AWS/Lambda", "Invocations", "FunctionName", "vapor-PROJECT_NAME-PROJECT_ENV-queue", { "color": "#2ca02c" } ],
244 [ ".", "Errors", ".", ".", { "color": "#ff7f0e" } ],
245 [ ".", "Duration", ".", ".", { "yAxis": "right", "color": "#1f77b4", "stat": "Average" } ]
246 ],
247 "view": "timeSeries",
248 "stacked": false,
249 "region": "PROJECT_REGION",
250 "stat": "Sum",
251 "period": 300,
252 "title": "Lambda Invocations - Queue",
253 "yAxis": {
254 "right": {
255 "showUnits": true
256 },
257 "left": {
258 "showUnits": true
259 }
260 },
261 "liveData": false,
262 "legend": {
263 "position": "hidden"
264 }
265 }
266 },
267 {
268 "height": 4,
269 "width": 8,
270 "y": 0,
271 "x": 0,
272 "type": "metric",
273 "properties": {
274 "metrics": [
275 [ "LambdaInsights", "total_memory", "function_name", "vapor-PROJECT_NAME-PROJECT_ENV", { "label": "Provisioned", "id": "m1" } ],
276 [ ".", "memory_utilization", ".", ".", { "label": "Avg. utilisation", "id": "m2" } ],
277 [ ".", "used_memory_max", ".", ".", { "label": "Max", "id": "m3", "stat": "Maximum" } ],
278 [ "AWS/Lambda", "Duration", "FunctionName", ".", { "label": "Avg. duration", "visible": false } ]
279 ],
280 "view": "singleValue",
281 "title": "Lambda Memory - HTTP",
282 "region": "PROJECT_REGION",
283 "stat": "Average",
284 "period": 300,
285 "setPeriodToTimeRange": true
286 }
287 },
288 {
289 "height": 4,
290 "width": 8,
291 "y": 4,
292 "x": 0,
293 "type": "metric",
294 "properties": {
295 "metrics": [
296 [ "LambdaInsights", "total_memory", "function_name", "vapor-PROJECT_NAME-PROJECT_ENV-cli", { "label": "Provisioned", "id": "m1" } ],
297 [ ".", "memory_utilization", ".", ".", { "label": "Avg. utilisation", "id": "m2" } ],
298 [ ".", "used_memory_max", ".", ".", { "label": "Max", "id": "m3", "stat": "Maximum" } ],
299 [ "AWS/Lambda", "Duration", "FunctionName", ".", { "label": "Avg. duration", "visible": false } ]
300 ],
301 "view": "singleValue",
302 "title": "Lambda Memory - CLI",
303 "region": "PROJECT_REGION",
304 "stat": "Average",
305 "period": 300,
306 "setPeriodToTimeRange": true
307 }
308 },
309 {
310 "height": 4,
311 "width": 8,
312 "y": 8,
313 "x": 0,
314 "type": "metric",
315 "properties": {
316 "metrics": [
317 [ "LambdaInsights", "total_memory", "function_name", "vapor-PROJECT_NAME-PROJECT_ENV-queue", { "label": "Provisioned", "id": "m1" } ],
318 [ ".", "memory_utilization", ".", ".", { "label": "Avg. utilisation", "id": "m2" } ],
319 [ ".", "used_memory_max", ".", ".", { "label": "Max", "id": "m3", "stat": "Maximum" } ]
320 ],
321 "view": "singleValue",
322 "title": "Lambda Memory - Queue",
323 "region": "PROJECT_REGION",
324 "stat": "Average",
325 "period": 300,
326 "setPeriodToTimeRange": true
327 }
328 },
329 {
330 "height": 4,
331 "width": 8,
332 "y": 8,
333 "x": 16,
334 "type": "metric",
335 "properties": {
336 "metrics": [
337 [ "AWS/ApiGateway", "IntegrationLatency", "Stage", "PROJECT_ENV", "ApiId", "API_GATEWAY_ID", { "color": "#2ca02c" } ],
338 [ ".", "DataProcessed", ".", ".", ".", ".", { "yAxis": "right", "color": "#1f77b4" } ]
339 ],
340 "view": "timeSeries",
341 "stacked": false,
342 "region": "PROJECT_REGION",
343 "stat": "Average",
344 "period": 300,
345 "title": "API Gateway",
346 "yAxis": {
347 "right": {
348 "showUnits": true
349 }
350 },
351 "liveData": false,
352 "legend": {
353 "position": "hidden"
354 }
355 }
356 },
357 {
358 "height": 4,
359 "width": 4,
360 "y": 16,
361 "x": 8,
362 "type": "metric",
363 "properties": {
364 "metrics": [
365 [ "AWS/RDS", "BinLogDiskUsage", "DBInstanceIdentifier", "DATABASE_NAME", { "color": "#2ca02c", "id": "m1" } ]
366 ],
367 "view": "timeSeries",
368 "stacked": false,
369 "region": "PROJECT_REGION",
370 "stat": "Average",
371 "period": 300,
372 "title": "RDS Binlog Usage",
373 "legend": {
374 "position": "hidden"
375 }
376 }
377 },
378 {
379 "height": 4,
380 "width": 4,
381 "y": 12,
382 "x": 12,
383 "type": "metric",
384 "properties": {
385 "metrics": [
386 [ "AWS/RDS", "SwapUsage", "DBInstanceIdentifier", "DATABASE_NAME" ],
387 [ ".", "FreeableMemory", ".", "." ]
388 ],
389 "view": "timeSeries",
390 "stacked": false,
391 "region": "PROJECT_REGION",
392 "stat": "Average",
393 "period": 300,
394 "title": "RDS Swap Usage",
395 "legend": {
396 "position": "hidden"
397 },
398 "yAxis": {
399 "left": {
400 "min": 0
401 }
402 }
403 }
404 },
405 {
406 "height": 4,
407 "width": 4,
408 "y": 12,
409 "x": 16,
410 "type": "metric",
411 "properties": {
412 "metrics": [
413 [ "AWS/SQS", "NumberOfMessagesReceived", "QueueName", "PROJECT_NAME-PROJECT_ENV", { "color": "#2ca02c" } ]
414 ],
415 "view": "timeSeries",
416 "stacked": false,
417 "region": "PROJECT_REGION",
418 "stat": "Sum",
419 "period": 300,
420 "title": "SQS Messages",
421 "legend": {
422 "position": "hidden"
423 }
424 }
425 },
426 {
427 "height": 4,
428 "width": 4,
429 "y": 12,
430 "x": 20,
431 "type": "metric",
432 "properties": {
433 "metrics": [
434 [ "AWS/SQS", "ApproximateAgeOfOldestMessage", "QueueName", "PROJECT_NAME-PROJECT_ENV", { "stat": "Maximum" } ],
435 [ ".", "SentMessageSize", ".", ".", { "color": "#2ca02c", "yAxis": "right" } ]
436 ],
437 "view": "timeSeries",
438 "stacked": false,
439 "region": "PROJECT_REGION",
440 "stat": "Average",
441 "period": 300,
442 "title": "SQS Message Size and Age",
443 "legend": {
444 "position": "hidden"
445 }
446 }
447 },
448 {
449 "height": 4,
450 "width": 4,
451 "y": 16,
452 "x": 20,
453 "type": "metric",
454 "properties": {
455 "metrics": [
456 [ "AWS/SQS", "NumberOfMessagesReceived", "QueueName", "PROJECT_NAME-PROJECT_ENV", { "color": "#2ca02c" } ],
457 [ ".", "NumberOfMessagesDeleted", ".", "." ]
458 ],
459 "view": "timeSeries",
460 "stacked": false,
461 "region": "PROJECT_REGION",
462 "stat": "Sum",
463 "period": 300,
464 "title": "SQS Message Receives",
465 "legend": {
466 "position": "hidden"
467 }
468 }
469 },
470 {
471 "height": 4,
472 "width": 4,
473 "y": 16,
474 "x": 16,
475 "type": "metric",
476 "properties": {
477 "metrics": [
478 [ "AWS/DynamoDB", "ConsumedWriteCapacityUnits", "TableName", "vapor_cache", { "color": "#2ca02c" } ],
479 [ ".", "ConsumedReadCapacityUnits", ".", "." ]
480 ],
481 "view": "timeSeries",
482 "stacked": false,
483 "region": "PROJECT_REGION",
484 "stat": "Average",
485 "period": 300,
486 "title": "DynamoDB - Consumed Capacity Units",
487 "legend": {
488 "position": "hidden"
489 },
490 "yAxis": {
491 "left": {
492 "min": 0
493 }
494 }
495 }
496 },
497 {
498 "height": 4,
499 "width": 4,
500 "y": 16,
501 "x": 12,
502 "type": "metric",
503 "properties": {
504 "metrics": [
505 [ "AWS/DynamoDB", "ConditionalCheckFailedRequests", "TableName", "vapor_cache", { "color": "#2ca02c" } ]
506 ],
507 "view": "timeSeries",
508 "stacked": false,
509 "region": "PROJECT_REGION",
510 "stat": "Sum",
511 "period": 300,
512 "title": "DynamoDB - Conditional Check Failures",
513 "legend": {
514 "position": "hidden"
515 }
516 }
517 },
518 {
519 "height": 4,
520 "width": 4,
521 "y": 16,
522 "x": 4,
523 "type": "metric",
524 "properties": {
525 "metrics": [
526 [ "AWS/RDS", "BurstBalance", "DBInstanceIdentifier", "DATABASE_NAME", { "color": "#2ca02c" } ]
527 ],
528 "view": "timeSeries",
529 "stacked": false,
530 "region": "PROJECT_REGION",
531 "stat": "Average",
532 "period": 300,
533 "annotations": {
534 "horizontal": [
535 {
536 "label": "Limit",
537 "value": 25,
538 "fill": "below"
539 }
540 ]
541 },
542 "yAxis": {
543 "left": {
544 "min": 0,
545 "max": 100,
546 "showUnits": false
547 }
548 },
549 "title": "RDS Burst Balance",
550 "legend": {
551 "position": "hidden"
552 }
553 }
554 },
555 {
556 "height": 4,
557 "width": 4,
558 "y": 16,
559 "x": 0,
560 "type": "metric",
561 "properties": {
562 "metrics": [
563 [ "AWS/RDS", "CPUUtilization", "DBInstanceIdentifier", "DATABASE_NAME", { "id": "m2", "color": "#2ca02c" } ]
564 ],
565 "view": "timeSeries",
566 "stacked": false,
567 "region": "PROJECT_REGION",
568 "stat": "Average",
569 "period": 300,
570 "annotations": {
571 "horizontal": [
572 {
573 "label": "Crirical",
574 "value": 75,
575 "fill": "above"
576 },
577 {
578 "label": "Warning",
579 "value": 50
580 }
581 ]
582 },
583 "yAxis": {
584 "left": {
585 "min": 0,
586 "max": 100,
587 "showUnits": false
588 }
589 },
590 "title": "RDS CPU",
591 "legend": {
592 "position": "hidden"
593 }
594 }
595 }
596 ]
597}

Application Elastic Load Balancer example snippet:

1{
2 "view": "timeSeries",
3 "stacked": false,
4 "metrics": [
5 [ "AWS/ApplicationELB", "ProcessedBytes", "LoadBalancer", "app/AELB_NAME" ],
6 [ ".", "LambdaTargetProcessedBytes", ".", "." ]
7 ],
8 "region": "PROJECT_REGION",
9 "title": "Application ELB",
10 "period": 300,
11 "legend": {
12 "position": "hidden"
13 }
14}

Make sure to replace AELB_NAME with the instance name of your AELB.