├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images ├── create_a_custom_widget_1.png └── create_a_custom_widget_2.png ├── samples ├── athenaQuery │ ├── athenaQuery.js │ ├── dashboard.json │ ├── permissions.yaml │ └── tags ├── awsCall │ ├── awsCall.js │ ├── awsCall.py │ ├── dashboard.json │ ├── permissions.yaml │ └── tags ├── cloudWatchBitmapGraph │ ├── cloudWatchBitmapGraph.js │ ├── cloudWatchBitmapGraph.py │ ├── dashboard.json │ ├── permissions.yaml │ └── tags ├── cloudWatchMetricDataTable │ ├── cloudWatchMetricDataTable.js │ ├── dashboard.json │ ├── permissions.yaml │ └── tags ├── codeDeploy │ ├── codeDeploy.js │ ├── dashboard.json │ ├── permissions.yaml │ └── tags ├── costExplorerReport │ ├── costExplorerReport.js │ ├── dashboard.json │ ├── permissions.yaml │ └── tags ├── debugger │ ├── dashboard.json │ ├── debugger.js │ ├── debugger.py │ ├── permissions.yaml │ └── tags ├── ec2Table │ ├── dashboard.json │ ├── ec2Table.js │ ├── permissions.yaml │ └── tags ├── echo │ ├── dashboard.json │ ├── echo.js │ ├── echo.py │ ├── permissions.yaml │ └── tags ├── emailDashboardSnapshot │ ├── dashboard.json │ ├── emailDashboardSnapshot.py │ ├── permissions.yaml │ └── tags ├── fetchURL │ ├── dashboard.json │ ├── fetchURL.js │ ├── fetchURL.py │ ├── permissions.yaml │ └── tags ├── helloWorld │ ├── dashboard.json │ ├── helloWorld.js │ ├── helloWorld.py │ ├── permissions.yaml │ └── tags ├── includeTextWidget │ ├── dashboard.json │ ├── includeTextWidget.js │ ├── includeTextWidget.py │ ├── permissions.yaml │ └── tags ├── logsInsightsQuery │ ├── dashboard.json │ ├── logsInsightsQuery.js │ ├── permissions.yaml │ └── tags ├── rssReader │ ├── dashboard.json │ ├── permissions.yaml │ ├── rssReader.py │ └── tags ├── s3GetObject │ ├── dashboard.json │ ├── permissions.yaml │ ├── s3GetObject.js │ ├── s3GetObject.py │ └── tags ├── simplePie │ ├── dashboard.json │ ├── permissions.yaml │ ├── simplePie.js │ ├── simplePie.py │ └── tags └── snapshotDashboardToS3 │ ├── dashboard.json │ ├── permissions.yaml │ ├── snapshotDashboardToS3.py │ └── tags └── scripts ├── build-assets └── lib └── bashFunctions /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | build 3 | 4 | # macOS 5 | .DS_Store 6 | 7 | # IDE 8 | .idea 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudWatch Custom Widgets Samples 2 | 3 | [![License](https://img.shields.io/badge/license-MIT--0-green)](LICENSE) 4 | [![AWS Provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900)](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html) 5 | 6 | The samples in this project demonstrate several uses of Custom Widgets within a CloudWatch Dashboard. 7 | 8 |

Table of Contents

9 | 10 |
11 | Table of Contents 12 |
    13 |
  1. ➤ What are Custom Widgets?
  2. 14 |
  3. ➤ Why would I use Custom Widgets?
  4. 15 |
  5. 16 | ➤ Sounds great, how do I sign up? 17 | 22 |
  6. 23 |
  7. 24 | ➤ Even quicker - 2-click samples 25 | 29 |
  8. 30 |
  9. ➤ How does a widget work?
  10. 31 |
  11. ➤ Can the returned HTML contain any HTML tags?
  12. 32 |
  13. ➤ Is interactivity possible in the returned HTML?
  14. 33 |
  15. 34 | ➤ What can I do with the cwdb-action tag? 35 | 39 |
  16. 40 |
  17. ➤ Can I call Custom Widget functions in other accounts?
  18. 41 |
  19. ➤ Can a Lambda function call a customer's internal services?
  20. 42 |
  21. ➤ Can access to Custom Widgets be restricted?
  22. 43 |
  23. ➤ What is the 'Hello world' Custom Widget example?
  24. 44 |
  25. ➤ Example of returning data from a call to an AWS function?
  26. 45 |
  27. ➤ Can the user of a Custom Widget customize its behavior?
  28. 46 |
  29. ➤ Does Custom Widgets refresh and auto-refresh?
  30. 47 |
  31. ➤ Can Custom Widgets be resized and moved around the dashboard?
  32. 48 |
  33. ➤ Can Custom Widgets react to the time range of the dashboard?
  34. 49 |
  35. ➤ Is Lambda the only API that these widgets can call?
  36. 50 |
  37. ➤ What is the default style of Custom Widget HTML elements?
  38. 51 |
  39. ➤ Can I customize the style of the HTML elements?
  40. 52 |
  41. ➤ Can the default styles be disabled?
  42. 53 |
  43. ➤ Can I use Custom Widgets in my own website?
  44. 54 |
  45. ➤ Contributing
  46. 55 |
  47. ➤ License
  48. 56 |
57 |
58 | 59 |

What are Custom Widgets?

60 | 61 | A **Custom Widget** is a [CloudWatch Dashboards](https://aws.amazon.com/blogs/aws/cloudwatch-dashboards-create-use-customized-metrics-views/) widget that can display virtually anything you want. Custom Widgets enables you to add custom visualizations, display information from multiple sources or add custom controls like buttons to take actions directly in a CloudWatch Dashboard. Custom Widgets are powered by custom Lambda functions, enabling complete control over the content, layout, and interactions. You can add custom widgets programmatically using the AWS SDK, CLI and CloudFormation. 62 | 63 |

Why would I use Custom Widgets?

64 | 65 | Custom Widgets is a simple way to build a custom data view or tool on a dashboard. There's no complicated web framework to learn, it's completely serverless. If you can write code in Lambda and create HTML then you can create a useful custom widget. 66 | 67 |

Sounds great, how do I sign up?

68 | 69 |

Option A: Using the CloudWatch Console

70 | 71 | The samples are already available in the CloudWatch Console, from a CloudWatch dashboard you can click the **Add widget** button and then select **Custom widget**. The samples found within this repository are available for a 1-click quick creation: 72 | 73 |
74 | 75 | ![create_a_custom_widget_1](images/create_a_custom_widget_1.png) 76 | 77 | ![create_a_custom_widget_2](images/create_a_custom_widget_2.png) 78 | 79 |
80 | 81 |

Option B: Using this repository directly

82 | 83 | 1. Clone the repo 84 | 2. Run the build script: `scripts/build-assets` 85 | 3. YAML templates will be generated inside the `build` folder e.g.: 86 | ``` 87 | build 88 | └── cfn 89 | ├── customWidgetSample1-js.yaml 90 | ├── customWidgetSample2-py.yaml 91 | ├── customWidgetSample3-js.yaml 92 | ├── customWidgetSample4-py.yaml 93 | ├── ... 94 | └── customWidgetSampleN-py.yaml 95 | ``` 96 | 4. The templates can be deployed with the [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) web console (or the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)) 97 | 5. Modify them or adjust them to suit your own use case and needs! 98 | 99 |

Option C: Manually from a CloudWatch Dashboard

100 | 101 | Alternatively, add this entry into the **widgets** array of any CloudWatch Dashboard - go to **Actions -> View/edit source:** 102 | 103 | ``` json 104 | { 105 | "type": "custom" 106 | } 107 | ``` 108 | 109 |

Even quicker - 2-click samples

110 | 111 | The following links launch a sample Custom Widget into your AWS account, along with a sample dashboard showing how the widget works. To launch: 112 | * if you're not happy with default region of **us-east-1 (N. Virginia)** switch region at top right of AWS Console 113 | * change the function name if you want to use it as a starter widget of your own 114 | * tick the **I acknowledge that AWS CloudFormation might create IAM resources.** checkbox 115 | * click orange **Create stack** button 116 | 117 | CloudFormation will create the custom widget Lambda function and sample CloudWatch Dashboard that uses it for you within a minute. Once the stack is created check the **Outputs** tab for links to the Lambda function code and sample dashboard. Edit the widget code directly in Lambda console and test changes directly in CloudWatch dashboard. 118 | 119 |

JavaScript samples

120 | 121 | * [Hello world](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetHelloWorld-js&template=customWidgets/customWidgetHelloWorld-js.yaml¶m_DoCreateExampleDashboard=Yes): a very simple starter widget 122 | * [Custom widget debugger](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetDebugger-js&template=customWidgets/customWidgetDebugger-js.yaml¶m_DoCreateExampleDashboard=Yes): a helpful debugger widget that displays useful information about the Lambda runtime environment 123 | * [Echo](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetEcho-js&template=customWidgets/customWidgetEcho-js.yaml¶m_DoCreateExampleDashboard=Yes): a simple echoer. Test how HTML will appear in a widget without writing a widget 124 | * [Run Athena queries](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetAthenaQuery-js&template=customWidgets/customWidgetAthenaQuery-js.yaml¶m_DoCreateExampleDashboard=Yes): run and edit queries against [Amazon Athena](https://aws.amazon.com/athena/) 125 | * [Call AWS API](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetAwsCall-js&template=customWidgets/customWidgetAwsCall-js.yaml¶m_DoCreateExampleDashboard=Yes): call any read-only AWS API and display results as JSON 126 | * [EC2 Table](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetEc2Table-js&template=customWidgets/customWidgetEc2Table-js.yaml¶m_DoCreateExampleDashboard=Yes): display top EC2 instances by CPU, plus a Reboot button (disabled by default as IAM role created only has read-only permissions needed to read EC2 instances and metric data) 127 | * [Fast CloudWatch bitmap graphs](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetCloudWatchBitmapGraph-js&template=customWidgets/customWidgetCloudWatchBitmapGraph-js.yaml¶m_DoCreateExampleDashboard=Yes): render CloudWatch graphs server-side, for faster display 128 | * [CloudWatch metric data as table](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetCloudWatchMetricDataTable-js&template=customWidgets/customWidgetCloudWatchMetricDataTable-js.yaml¶m_DoCreateExampleDashboard=Yes): display raw CloudWatch metric data in table 129 | * [Code Deployments](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetCodeDeploy-js&template=customWidgets/customWidgetCodeDeploy-js.yaml¶m_DoCreateExampleDashboard=Yes): display [CodeDeploy](https://aws.amazon.com/codedeploy/) deployments 130 | * [Cost explorer report](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetCostExplorerReport-js&template=customWidgets/customWidgetCostExplorerReport-js.yaml¶m_DoCreateExampleDashboard=Yes): displays a report on cost of each AWS service for a selected time range 131 | * [Display content of external URL](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetFetchURL-js&template=customWidgets/customWidgetFetchURL-js.yaml¶m_DoCreateExampleDashboard=Yes): display externally accessible HTML content 132 | * [Include text widget from dashboard](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetIncludeTextWidget-js&template=customWidgets/customWidgetIncludeTextWidget-js.yaml¶m_DoCreateExampleDashboard=Yes): displays the first text widget from a specified CloudWatch Dashboard 133 | * [Run Logs Insights queries](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetLogsInsightsQuery-js&template=customWidgets/customWidgetLogsInsightsQuery-js.yaml¶m_DoCreateExampleDashboard=Yes): run and edit queries against [CloudWatch Logs Insights](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html) 134 | * [Display S3 object](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetS3GetObject-js&template=customWidgets/customWidgetS3GetObject-js.yaml¶m_DoCreateExampleDashboard=Yes): display object from S3 bucket in your account 135 | * [Simple SVG pie chart](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetSimplePie-js&template=customWidgets/customWidgetSimplePie-js.yaml¶m_DoCreateExampleDashboard=Yes): simple example of graphical SVG-based widget 136 | 137 |

Python samples

138 | 139 | * [Hello world](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetHelloWorld-py&template=customWidgets/customWidgetHelloWorld-py.yaml¶m_DoCreateExampleDashboard=Yes): a very simple starter widget 140 | * [Custom widget debugger](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetDebugger-py&template=customWidgets/customWidgetDebugger-py.yaml¶m_DoCreateExampleDashboard=Yes): a helpful debugger widget that displays useful information about the Lambda runtime environment 141 | * [Echo](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetEcho-py&template=customWidgets/customWidgetEcho-py.yaml¶m_DoCreateExampleDashboard=Yes): a simple echoer. Test how HTML will appear in a widget without writing a widget 142 | * [Call AWS API](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetAwsCall-py&template=customWidgets/customWidgetAwsCall-py.yaml¶m_DoCreateExampleDashboard=Yes): call any read-only AWS API and display results as JSON 143 | * [Fast CloudWatch bitmap graphs](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetCloudWatchBitmapGraph-py&template=customWidgets/customWidgetCloudWatchBitmapGraph-py.yaml¶m_DoCreateExampleDashboard=Yes): render CloudWatch graphs server-side, for faster display 144 | * [Send dashboard snapshot by email](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetEmailDashboardSnapshot-py&template=customWidgets/customWidgetEmailDashboardSnapshot-py.yaml¶m_DoCreateExampleDashboard=Yes): take a snapshot of the current dashboard and send it to email recipient 145 | * [Display content of an external URL](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetFetchURL-py&template=customWidgets/customWidgetFetchURL-py.yaml¶m_DoCreateExampleDashboard=Yes): display externally accessible HTML content 146 | * [Include text widget from dashboard](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetIncludeTextWidget-py&template=customWidgets/customWidgetIncludeTextWidget-py.yaml¶m_DoCreateExampleDashboard=Yes): displays the first text widget from a specified CloudWatch Dashboard 147 | * [RSS Reader](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetRssReader-py&template=customWidgets/customWidgetRssReader-py.yaml¶m_DoCreateExampleDashboard=Yes): display RSS feeds 148 | * [Display S3 object](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetS3GetObject-py&template=customWidgets/customWidgetS3GetObject-py.yaml¶m_DoCreateExampleDashboard=Yes): display object from S3 bucket in your account 149 | * [Simple SVG pie chart](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetSimplePie-py&template=customWidgets/customWidgetSimplePie-py.yaml¶m_DoCreateExampleDashboard=Yes): simple example of graphical SVG-based widget 150 | * [Snapshot dashboard to S3](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetSnapshotDashboardToS3-py&template=customWidgets/customWidgetSnapshotDashboardToS3-py.yaml¶m_DoCreateExampleDashboard=Yes): take a snapshot of the current dashboard and store it in S3 151 | 152 |

How does a widget work?

153 | 154 | Your CloudWatch Dashboard: 155 | 1. calls the Lambda function containing the widget code. The function is passed any custom parameters defined in the widget 156 | 2. the Lambda function is expected to return a string of HTML 157 | 3. the CloudWatch Dashboard displays the HTML 158 | 4. if the response is JSON it is displayed as formatted JSON 159 | 160 |

Can the returned HTML contain any HTML tags?

161 | 162 | Almost all HTML tags are supported. CSS styles and SVGs can be used for building sophisticated and graphically rich views. 163 | 164 | However, for security reasons _JavaScript_ is not allowed in the returned HTML. If the HTML string from the Lambda function contains any Javascript it will be "cleaned" from the HTML before rendering. Also, the **<iframe>** tag is forbidden. 165 | 166 | Removing Javascript is done to prevent privilege escalation issues, where the writer of the Lambda function can inject code that would run with the possibly higher privileges of the user viewing the widget in the CloudWatch Console. 167 | 168 |

Is interactivity possible in the returned HTML?

169 | 170 | Yes. Even though JavaScript is not allowed, there are a number of avenues that allow interactivity with the returned HTML: 171 | 172 | * Any element in the returned HTML can be tagged with special configuration in a **<cwdb-action>** tag that trigger display information in popups, ask for confirmation on clicks can call any Lambda function when that element is clicked. This allows for example the definition of buttons that will call any AWS API via a Lambda function. The returned HTML can be set to either replace the existing Lambda widget's content, or display inside a modal. 173 | * HTML can include links that open new consoles, other customer pages or load other dashboards 174 | * HTML can include the 'title' attribute for an element, that gives additional information if the user hovers over that element 175 | * HTML can include CSS selectors such as :hover which can trigger animations or different CSS effects 176 | 177 |

What can I do with the cwdb-action tag?

178 | 179 | Example of how to create a button that reboots an EC2 instance via a Lambda function call, displaying the success or failure of the call in a popup: 180 | 181 | ``` html 182 | Reboot Instance 183 | 184 | { "instanceId": "i-342389adbfef" } 185 | 186 | ``` 187 | 188 |

cwdb-action: Definition and usage

189 | 190 | The **<cwdb-action>** element defines a behavior on the previous element. The content of the **<cwdb-action>** is either HTML to display or JSON of parameters to pass to the call to a Lambda function. 191 | 192 | **Attributes** 193 | General definition: 194 | ``` html 195 | 201 | 202 | html | params in JSON 203 | 204 | 205 | ``` 206 | 207 | | Attribute | Value | Description | 208 | | --- | --- | --- | 209 | | **action** | call | html | Two actions are supported, _call_ a Lambda function or (default) display _html_ contained within **<cwdb-action>** | 210 | | **confirmation** | _message_ | Displays a confirmation message that needs to be acknowledged before the action is taken (allowing customer to cancel) | 211 | | **display** | popup | widget | Where should action result be displayed. Can be either in a _popup_ or (default) replace the content of the _widget_ itself | 212 | | **endpoint** | _arn of lambda function_ | The ARN of the Lambda function to call. Required if **action** is set to **call** | 213 | | **event** | click | dblclick | mouseenter | The event on the previous element which triggers the action. The _mouseenter_ event can be used only in combination with the _html_ action. The default is _click_ | 214 | 215 |

cwdb-action: Examples

216 | 217 | A link which displays more information in a popup: 218 | ``` html 219 | Click me for more info in popup 220 | 221 |

Big title

222 | More info about something important. 223 |
224 | ``` 225 | A Next button (primary) which replaces content of widget with call to a Lambda: 226 | ``` html 227 | Next 228 | 229 | { "pageNum": 2 } 230 | 231 | ``` 232 | 233 |

Can I call Custom Widget functions in other accounts?

234 | 235 | Yes. In order to help with sharing of functionality between multiple accounts owned by a customer, a Custom Widget function can be defined in one account and called from the dashboards of other accounts, as long as the correct permissions have been defined to allow access from other accounts. Follow [CloudWatch Cross-Account Cross-Region setup](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Cross-Account-Cross-Region.html) and make sure you allow the current account to invoke the custom widget Lambda function in the shared account(s). 236 | 237 | The CloudWatch Dashboard facilitates this by allowing the customer to pick a Lambda function by pasting a raw ARN to it into the Dashboard definition. 238 | 239 |

Can a Lambda function call a customer's internal services?

240 | 241 | If those services are accessible within an AWS VPC then the Lambda function can run within that VPC, thus allowing access to the customer's internal services/data. 242 | 243 |

Can access to Custom Widgets be restricted?

244 | 245 | Yes. Normal IAM policies can be applied so that IAM users can be allowlisted or blocklisted for Lambda execute permissions on all or particular Lambda functions. 246 | 247 | This allows customers to share a single dashboard with multiple users but lock down the view of particular Lambda widgets to users with higher privileges. 248 | 249 |

What is the 'Hello world' Custom Widget example?

250 | 251 | Below is the Javascript code for the [Hello world](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetHelloWorld-js&template=customWidgets/customWidgetHelloWorld-js.yaml¶m_DoCreateExampleDashboard=Yes) example: 252 | 253 | ``` javascript 254 | exports.handler = async (event) => { 255 | const name = event.name || 'friend'; 256 | return `

Hello ${name}

`; 257 | }; 258 | ``` 259 | 260 | And here is the Python version of [Hello world](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetHelloWorld-py&template=customWidgets/customWidgetHelloWorld-py.yaml¶m_DoCreateExampleDashboard=Yes): 261 | 262 | ``` python 263 | def lambda_handler(event, context): 264 | name = event.get('name', 'friend') 265 | return f'

Hello {name}

' 266 | ``` 267 | 268 |

Example of returning data from a call to an AWS function?

269 | 270 | Below is the [JavaScript code](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetS3GetObject-js&template=customWidgets/customWidgetS3GetObject-js.yaml¶m_DoCreateExampleDashboard=Yes) for displaying the HTML content of any file in an S3 bucket: 271 | 272 | ``` javascript 273 | const aws = require('aws-sdk'); 274 | 275 | exports.handler = async (event) => { 276 | const region = event.region || process.env.AWS_REGION; 277 | const params = { 278 | Bucket: event.bucket, 279 | Key: event.key 280 | }; 281 | const s3 = new aws.S3({ region }); 282 | const result = await s3.getObject(params).promise(); 283 | 284 | return result.Body.toString(); 285 | }; 286 | 287 | ``` 288 | 289 | And the Python equivalent: 290 | 291 | ``` python 292 | def lambda_handler(event, context): 293 | region = event.get('region', os.environ['AWS_REGION']) 294 | s3 = boto3.client('s3', region_name=region) 295 | result = s3.get_object(Bucket=event['bucket'], Key=event['key']) 296 | 297 | return result['Body'].read().decode('utf-8') 298 | ``` 299 | 300 | Another one, that can [call any AWS API](https://console.aws.amazon.com/cloudwatch/cfn.js?region=us-east-1&action=create&stackName=customWidgetAwsCall-js&template=customWidgets/customWidgetAwsCall-js.yaml¶m_DoCreateExampleDashboard=Yes) and display the results in pretty-fied JSON: 301 | 302 | ``` javascript 303 | const aws = require('aws-sdk'); 304 | 305 | exports.handler = async (event) => { 306 | const service = event.service || 'CloudWatch'; 307 | const api = event.api || 'listDashboards'; 308 | const region = event.region || process.env.AWS_REGION; 309 | const params = event.params || {}; 310 | 311 | if (!aws[service]) { 312 | throw `Unknown AWS service ${service}`; 313 | } 314 | 315 | const client = new aws[service]({ region }); 316 | 317 | if (!client[api]) { 318 | throw `Unknown API ${api} for AWS service ${service}`; 319 | } 320 | 321 | return await client[api](params).promise(); 322 | }; 323 | ``` 324 | 325 |

Can the user of a Custom Widget customize its behavior?

326 | 327 | Yes. All Lambda functions for HTML widgets can receive custom parameters from the dashboard, defined by the user on a per-widget basis. It is up to the Lambda function writer to decide what parameters will be accepted. 328 | 329 | When creating/modifying a custom widget a customer can: 330 | 331 | - select the Lambda function to call from any region in the account via a dropdown 332 | - enter a [Version or Alias](http://docs.aws.amazon.com/lambda/latest/dg/versioning-aliases.html) of the Lambda function to run a specific version of the function 333 | - enter a specific ARN for a Lambda function, which could be in a different account 334 | - list the custom parameters to be sent to the Lambda function, in either JSON or YAML form 335 | - set the title for the widget 336 | - choose when the widget should be updated (i.e. when the Lambda function should be called again). This can be on **refresh** when the dashboard auto-refreshes and/or when the widget is **resized** and/or when the dashboard **time range** is adjusted (including when graphs are zoomed into) 337 | 338 |

Does Custom Widgets refresh and auto-refresh?

339 | 340 | Yes. The refresh button will call all Lambda functions and re-render all Lambda widgets along with the CloudWatch metric graphs. The auto-refresh however will re-render as long as the **Refresh** option is selected (see previous question). 341 | 342 |

Can Custom Widgets be resized and moved around the dashboard?

343 | 344 | Yes. The Custom Widgets can be resized and moved around the dashboards as easily as the existing Text and Metric Graph widgets. 345 | 346 |

Can Custom Widgets react to the time range of the dashboard?

347 | 348 | Yes. The parameters passed to every call to a Custom Widget function will include the time range of the dashboard. 349 | 350 | They will also include the dimensions of the viewable area of the widget box, helping writers of the function design their HTML to fit the size of the Lambda widget's area as they see fit. 351 | 352 |

Is the Custom Widget passed any information by default?

353 | 354 | Yes. Every call to Lambda includes a parameter called **widgetContext** which has the following structure/contents: 355 | 356 | ``` json 357 | { 358 | "widgetContext": { 359 | "dashboardName": "Name-of-current-dashboard", 360 | "widgetId": "widget-16", 361 | "accountId": "XXXXXXXXXXXX", 362 | "locale": "en", 363 | "timezone": { 364 | "label": "UTC", 365 | "offsetISO": "+00:00", 366 | "offsetInMinutes": 0 367 | }, 368 | "period": 300, 369 | "isAutoPeriod": true, 370 | "timeRange": { 371 | "mode": "relative", 372 | "start": 1512556923228, 373 | "end": 1512600123228, 374 | "relativeStart": 43200000, 375 | "zoom": { 376 | "start": 1627276030434, 377 | "end": 1627282956521 378 | } 379 | }, 380 | "theme": "light", 381 | "linkCharts": true, 382 | "title": "Tweets for Amazon website problem", 383 | "forms": { 384 | "all": {} 385 | }, 386 | "params": { 387 | "original": "param-to-widget" 388 | }, 389 | "width": 588, 390 | "height": 369 391 | } 392 | } 393 | ``` 394 | 395 | This gives the Lambda developer important widget context information such as: 396 | 397 | - current dashboard settings such as time range, zoom range, timezone, period settings, if charts are "linked" together and language setting of the console 398 | - width and height of visible area of the widget 399 | - widget title and unique id 400 | - content of all form fields within the widget – allowing widget to take input and send it to another Lambda call via **<cwdb-action>** tag 401 | - the calling account id 402 | - the original parameters configured for the widget, so that a button that calls the Lambda function again does not have to copy/paste original parameters 403 | 404 |

Is Lambda the only API that these widgets can call?

405 | 406 | Yes. 407 | 408 |

What is the default style of Custom Widget HTML elements?

409 | 410 | The default style of HTML elements such as links and tables will follow the styling of the CloudWatch Console and Dashboard. 411 | 412 |

Can I customize the style of the HTML elements?

413 | 414 | Yes. Styles of HTML elements can be set either via inline styles or including a stylesheet within the returned HTML (include **<style></style>** anywhere within the HTML). 415 | 416 |

Can the default styles be disabled?

417 | 418 | Yes. In order not to force customers to override and fight with the default CSS we also allow the default CSS to be disabled, under control of the returned HTML. Simply include a single HTML element that has a class of **cwdb-no-default-styles** , e.g. 419 | 420 | ```html 421 | I have default styling 422 | ``` 423 | 424 |

Can I use Custom Widgets in my own website?

425 | 426 | Yes. [CloudWatch Dashboarding Sharing](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-dashboard-sharing.html) allows you to display a CloudWatch Dashboard outside the AWS console and embed it into other websites via the [iframe](https://www.w3schools.com/html/html_iframe.asp) tag. 427 | 428 | [CloudWatch Dashboarding Sharing](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-dashboard-sharing.html) supports sharing dashboards by: 429 | * share single dashboard with group of email addresses, using login with passwords 430 | * share single dashboard via a public (obscure) URL, login not required 431 | * Share all dashboards in an account and via third-party single sign-on (SSO) provider 432 | 433 | So add your Custom Widgets to a dashboard and share it. Once it is shared you will have a URL for the dashboard, in this form: 434 | ``` 435 | https://cloudwatch.amazonaws.com/dashboard.html?dashboard=&context= 436 | ``` 437 | 438 | This can then be embedded into a website with HTML similar to this: 439 | 440 | ``` 441 | 442 | ``` 443 | 444 |

Contributing

445 | 446 | Contributions are very welcome please checkout out the [contributing guidelines](CONTRIBUTING.md). 447 | 448 |

License

449 | 450 | Licensed under the [MIT-0 license](LICENSE). 451 | -------------------------------------------------------------------------------- /images/create_a_custom_widget_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cloudwatch-custom-widgets-samples/e337b8de4d0f5edf906d7ac2b17d609248301cc8/images/create_a_custom_widget_1.png -------------------------------------------------------------------------------- /images/create_a_custom_widget_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cloudwatch-custom-widgets-samples/e337b8de4d0f5edf906d7ac2b17d609248301cc8/images/create_a_custom_widget_2.png -------------------------------------------------------------------------------- /samples/athenaQuery/athenaQuery.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: display results of Athena queries 5 | const { AthenaClient, StartQueryExecutionCommand, GetQueryExecutionCommand, GetQueryResultsCommand } = require("@aws-sdk/client-athena"); 6 | 7 | const DOCS = ` 8 | ## Run Athena Query 9 | Runs an Athena query and displays results in a table. 10 | 11 | ### Widget parameters 12 | Param | Description 13 | ---|--- 14 | **region** | The region to run the Athena query in 15 | **database** | Name of the Athena database 16 | **sql** | The SQL query to run 17 | 18 | ### Example parameters 19 | \`\`\` yaml 20 | region: ${process.env.AWS_REGION} 21 | database: default 22 | sql: select eventtime, eventsource, eventname from sampledb.cloudtrail_logs limit 10 23 | \`\`\` 24 | `; 25 | 26 | const CHECK_QUERY_STATUS_DELAY_MS = 250; 27 | const CSS = '' 28 | 29 | const executeQuery = async (athena, accountId, region, querySQL, database) => { 30 | const params = { 31 | QueryString: querySQL, 32 | ResultConfiguration: { 33 | OutputLocation: `s3://aws-cw-widget-athena-query-results-${accountId}-${region}` 34 | }, 35 | QueryExecutionContext: { 36 | Database: database 37 | } 38 | }; 39 | 40 | const startQueryCommand = new StartQueryExecutionCommand(params); 41 | const query = await athena.send(startQueryCommand); 42 | 43 | // Wait until query is finished execution. 44 | await checkQueryStatus(athena, query); 45 | const getQueryResultsCommand = new GetQueryResultsCommand({ QueryExecutionId: query.QueryExecutionId }); 46 | return await athena.send(getQueryResultsCommand); 47 | } 48 | 49 | const checkQueryStatus = async (athena, query) => { 50 | let finished = false; 51 | while (!finished) { 52 | await sleep(CHECK_QUERY_STATUS_DELAY_MS); 53 | 54 | const getQueryExecutionCommand = new GetQueryExecutionCommand(query); 55 | const response = await athena.send(getQueryExecutionCommand); 56 | const queryStatus = response.QueryExecution.Status.State; 57 | switch (queryStatus) { 58 | case 'SUCCEEDED': 59 | finished = true; 60 | case 'RUNNING': 61 | case 'QUEUED': 62 | continue; 63 | default: 64 | console.error('Query Error: ', response); 65 | throw new Error(`Status of Query ${query.QueryExecutionId} is ${queryStatus}.`); 66 | } 67 | } 68 | } 69 | 70 | const sleep = async (delay) => { 71 | return new Promise((resolve) => setTimeout(resolve, delay)); 72 | } 73 | 74 | const displayResults = async (database, sql, results, region, context) => { 75 | let html = ` 76 |
77 | 78 | 79 | 80 | 81 | 82 |
Database
SQL
83 | Run query 84 | { "region": "${region}" } 85 |

86 |

Results

87 | `; 88 | 89 | if (results && results.ResultSet && results.ResultSet.ResultSetMetadata) { 90 | const cols = results.ResultSet.ResultSetMetadata.ColumnInfo; 91 | const rows = results.ResultSet.Rows.slice(1); 92 | 93 | html += ` 94 | `; 95 | 96 | rows.forEach(row => { 97 | html += ``; 98 | }); 99 | 100 | html += `
${cols.map(col => col.Label).join('')}
${row.Data.map(cell => cell.VarCharValue || '').join('')}
` 101 | } else if (results) { 102 | html += `
${results}
`; 103 | } 104 | 105 | return html; 106 | }; 107 | 108 | exports.handler = async (event, context) => { 109 | if (event.describe) { 110 | return DOCS; 111 | } 112 | 113 | const form = event.widgetContext.forms.all; 114 | const database = form.database || event.database || 'default'; 115 | const sql = form.sql || event.sql; 116 | const region = event.region || process.env.AWS_REGION; 117 | const accountId = context.invokedFunctionArn.split(":")[4]; 118 | const athena = new AthenaClient({ region }); 119 | 120 | let results; 121 | 122 | if (database && sql && sql.trim() !== '') { 123 | try { 124 | results = await executeQuery(athena, accountId, region, sql, database); 125 | } catch (e) { 126 | results = e; 127 | } 128 | } 129 | 130 | return CSS + await displayResults(database, sql, results, region, context); 131 | }; 132 | -------------------------------------------------------------------------------- /samples/athenaQuery/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 24, 6 | "height": 18, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "params": { 10 | "region": "${AWS::Region}", 11 | "database": "default", 12 | "sql": "select eventtime, eventsource, eventname, awsregion, sourceipaddress, errorcode, errormessage from sampledb.cloudtrail_logs limit 10" 13 | }, 14 | "updateOn": { 15 | "refresh": true 16 | }, 17 | "title": "Athena Query, ${AWS::Region}" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /samples/athenaQuery/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Effect: Allow 5 | Action: 6 | - s3:GetObject 7 | - s3:PutObject 8 | - s3:AbortMultipartUpload 9 | - s3:ListMultipartUploadParts 10 | - s3:DeleteObject 11 | Resource: 12 | - !Sub arn:${AWS::Partition}:s3:::aws-cw-widget-athena-query-results-${AWS::AccountId}-${AWS::Region}/* 13 | - Effect: Allow 14 | Action: 15 | - s3:GetBucketLocation 16 | - s3:CreateBucket 17 | - s3:ListBucket 18 | - s3:ListBucketMultipartUploads 19 | - s3:DeleteObject 20 | Resource: 21 | - !Sub arn:${AWS::Partition}:s3:::aws-cw-widget-athena-query-results-${AWS::AccountId}-${AWS::Region} 22 | - Effect: Allow 23 | Action: 24 | - athena:* 25 | Resource: 26 | - "*" 27 | - Effect: Allow 28 | Action: 29 | - s3:Get* 30 | - s3:List* 31 | Resource: 32 | - "*" 33 | - Effect: Allow 34 | Action: 35 | - glue:CreateDatabase 36 | - glue:DeleteDatabase 37 | - glue:GetDatabase 38 | - glue:GetDatabases 39 | - glue:UpdateDatabase 40 | - glue:CreateTable 41 | - glue:DeleteTable 42 | - glue:BatchDeleteTable 43 | - glue:UpdateTable 44 | - glue:GetTable 45 | - glue:GetTables 46 | - glue:BatchCreatePartition 47 | - glue:CreatePartition 48 | - glue:DeletePartition 49 | - glue:BatchDeletePartition 50 | - glue:UpdatePartition 51 | - glue:GetPartition 52 | - glue:GetPartitions 53 | - glue:BatchGetPartition 54 | Resource: 55 | - "*" 56 | PolicyName: athenaAccess 57 | -------------------------------------------------------------------------------- /samples/athenaQuery/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readWrite 3 | -------------------------------------------------------------------------------- /samples/awsCall/awsCall.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: call any read-only AWS API and return raw results in JSON 5 | const DOCS = ` 6 | ## Make an AWS Call 7 | Calls any (read-only) AWS API and displays the result as JSON. 8 | 9 | ### Widget parameters 10 | Param | Description 11 | ---|--- 12 | **service** | The name of the AWS service to call, e.g. **EC2** or **CloudWatch** 13 | **api** | The API name to call, e.g. **DescribeInstances** or **ListDashboards** 14 | **params** | The parameters to pass to the API 15 | 16 | ### Example parameters 17 | \`\`\` yaml 18 | service: EC2 19 | api: describeInstances 20 | params: 21 | Filters: 22 | - Name: instance-state-name 23 | Values: 24 | - running 25 | \`\`\` 26 | `; 27 | 28 | exports.handler = async (event) => { 29 | if (event.describe) { 30 | return DOCS; 31 | } 32 | 33 | const service = event.service || 'CloudWatch'; 34 | const api = event.api || 'ListDashboards'; 35 | const region = event.region || process.env.AWS_REGION; 36 | const params = event.params || {}; 37 | 38 | try { 39 | const { [service + 'Client']: ServiceClient } = await import(`@aws-sdk/client-${service.toLowerCase()}`); 40 | 41 | if (!ServiceClient) { 42 | throw new Error(`Unknown AWS service ${service}`); 43 | } 44 | 45 | const client = new ServiceClient({ region }); 46 | 47 | const { [api + 'Command']: Command } = await import(`@aws-sdk/client-${service.toLowerCase()}`); 48 | const command = new Command(params); 49 | 50 | const response = await client.send(command); 51 | 52 | return response; 53 | } catch (error) { 54 | console.error(`Error calling AWS API: ${error.message}`); 55 | return { error: error.message }; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /samples/awsCall/awsCall.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: call any read-only AWS API and return raw results in JSON 5 | import boto3 6 | import json 7 | import os 8 | import re 9 | 10 | DOCS = """ 11 | ## Make an AWS Call 12 | Calls any (read-only) AWS API and displays the result as JSON. 13 | 14 | ### Widget parameters 15 | Param | Description 16 | ---|--- 17 | **service** | The name of the AWS service to call, e.g. **EC2** or **CloudWatch** 18 | **api** | The API name to call 19 | **params** | The parameters to pass to the API 20 | 21 | ### Example parameters 22 | ``` yaml 23 | service: EC2 24 | api: describeInstances 25 | params: 26 | Filters: 27 | - Name: instance-state-name 28 | Values: 29 | - running 30 | ```""" 31 | 32 | def lambda_handler(event, context): 33 | if 'describe' in event: 34 | return DOCS 35 | 36 | service = event.get('service', 'cloudwatch').lower() 37 | apiRaw = event.get('api', 'list_dashboards') 38 | api = re.sub(r'(? { 27 | if (event.describe) { 28 | return DOCS; 29 | } 30 | 31 | const widgetContext = event.widgetContext; 32 | const timeRange = widgetContext.timeRange.zoom || widgetContext.timeRange; 33 | const start = new Date(timeRange.start).toISOString(); 34 | const end = new Date(timeRange.end).toISOString(); 35 | const width = widgetContext.width; 36 | const height = widgetContext.height; 37 | const graph = Object.assign(event.graph, { start, end, width, height}); 38 | if (!graph.theme) { 39 | graph.theme = widgetContext.theme; 40 | } 41 | const params = { 42 | MetricWidget: JSON.stringify(graph) 43 | }; 44 | const region = event.graph.region; 45 | 46 | const cloudwatch = new CloudWatchClient({ region }); 47 | const command = new GetMetricWidgetImageCommand(params); 48 | 49 | try { 50 | const response = await cloudwatch.send(command); 51 | const base64Image = Buffer.from(response.MetricWidgetImage).toString('base64'); 52 | return ``; 53 | } catch (error) { 54 | console.error("Error fetching metric widget image:", error); 55 | return `

Error fetching metric widget image: ${error.message}

`; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /samples/cloudWatchBitmapGraph/cloudWatchBitmapGraph.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: display a CloudWatch metric graph as bitmap 5 | import base64 6 | import boto3 7 | import datetime 8 | import json 9 | import os 10 | 11 | DOCS = """ 12 | ## Display a CloudWatch bitmap graph 13 | Displays CloudWatch metrics as a bitmap, for faster display of metrics. 14 | 15 | ### Widget parameters 16 | Param | Description 17 | ---|--- 18 | **graph** | The graph definition. Use the parameters from the **Source** tab in CloudWatch Console's **Metrics** page 19 | 20 | ### Example parameters 21 | ``` yaml 22 | graph: 23 | view: timeSeries 24 | metrics: 25 | - [ AWS/Lambda, Invocations ] 26 | region: {os.environ("AWS_REGION")} 27 | ```""" 28 | 29 | def lambda_handler(event, context): 30 | if 'describe' in event: 31 | return DOCS 32 | 33 | widgetContext = event['widgetContext'] 34 | timeRange = widgetContext['timeRange']['zoom'] if 'zoom' in widgetContext['timeRange'] else widgetContext['timeRange'] 35 | start = datetime.datetime.utcfromtimestamp(timeRange['start']/1000).isoformat() 36 | end = datetime.datetime.utcfromtimestamp(timeRange['end']/1000).isoformat() 37 | width = widgetContext['width'] 38 | height = widgetContext['height'] 39 | graph = { **event['graph'], 'start': start, 'end': end, 'width': width, 'height': height } 40 | if 'theme' not in graph: 41 | graph['theme'] = widgetContext['theme'] 42 | params = { 'MetricWidget': json.dumps(graph) } 43 | region = graph['region'] 44 | 45 | cloudwatch = boto3.client('cloudwatch', region_name=region) 46 | image = cloudwatch.get_metric_widget_image(**params) 47 | base64_image = base64.b64encode(image['MetricWidgetImage']).decode('UTF-8') 48 | return f"""""" 49 | -------------------------------------------------------------------------------- /samples/cloudWatchBitmapGraph/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "metric", 5 | "width": 24, 6 | "height": 3, 7 | "properties": { 8 | "metrics": [ 9 | [ "AWS/Lambda", "Invocations" ] 10 | ], 11 | "view": "timeSeries", 12 | "stacked": false, 13 | "region": "us-east-1", 14 | "stat": "Sum", 15 | "period": 300, 16 | "title": "Total Lambda invocations (interactive) - zoom in on me to see bitmaps zoom" 17 | } 18 | }, 19 | { 20 | "type": "custom", 21 | "width": 12, 22 | "height": 6, 23 | "properties": { 24 | "endpoint": "${lambdaFunction.Arn}", 25 | "updateOn": { 26 | "refresh": true, 27 | "resize": true, 28 | "timeRange": true 29 | }, 30 | "params": { 31 | "graph": { 32 | "metrics": [ 33 | [ 34 | { 35 | "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Invocations\"', 'Sum', 300)", 36 | "id": "lambda", 37 | "period": 300, 38 | "visible": false 39 | } 40 | ], 41 | [ 42 | { 43 | "expression": "SORT(lambda, SUM, DESC)", 44 | "label": "[sum: ${!SUM}]", 45 | "id": "sort", 46 | "stat": "Sum" 47 | } 48 | ] 49 | ], 50 | "view": "timeSeries", 51 | "stacked": false, 52 | "region": "us-east-1", 53 | "stat": "Average", 54 | "period": 300, 55 | "yAxis": { 56 | "left": { 57 | "label": "Count", 58 | "showUnits": false 59 | } 60 | } 61 | } 62 | }, 63 | "title": "Lambda functions: by invocations (bitmap)" 64 | } 65 | }, 66 | { 67 | "type": "custom", 68 | "width": 12, 69 | "height": 6, 70 | "properties": { 71 | "endpoint": "${lambdaFunction.Arn}", 72 | "updateOn": { 73 | "refresh": true, 74 | "resize": true, 75 | "timeRange": true 76 | }, 77 | "params": { 78 | "graph": { 79 | "metrics": [ 80 | [ 81 | { 82 | "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Duration\"', 'p90', 300)", 83 | "id": "lambda", 84 | "visible": false 85 | } 86 | ], 87 | [ 88 | { 89 | "expression": "SORT(lambda, SUM, DESC)", 90 | "label": "[avg: ${!AVG}ms]", 91 | "id": "sort" 92 | } 93 | ] 94 | ], 95 | "view": "timeSeries", 96 | "stacked": false, 97 | "region": "us-east-1", 98 | "stat": "p90", 99 | "period": 300, 100 | "yAxis": { 101 | "left": { 102 | "label": "Milliseconds", 103 | "showUnits": false 104 | } 105 | } 106 | } 107 | }, 108 | "title": "Lambda functions: by duration, p90 (bitmap)" 109 | } 110 | }, 111 | { 112 | "type": "custom", 113 | "width": 12, 114 | "height": 6, 115 | "properties": { 116 | "endpoint": "${lambdaFunction.Arn}", 117 | "updateOn": { 118 | "refresh": true, 119 | "resize": true, 120 | "timeRange": true 121 | }, 122 | "params": { 123 | "graph": { 124 | "metrics": [ 125 | [ 126 | { 127 | "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Errors\"', 'Sum', 300)", 128 | "id": "lambda", 129 | "period": 300, 130 | "visible": false 131 | } 132 | ], 133 | [ 134 | { 135 | "expression": "SORT(lambda, SUM, DESC)", 136 | "label": "[sum: ${!SUM}]", 137 | "id": "e2", 138 | "stat": "Sum" 139 | } 140 | ] 141 | ], 142 | "view": "timeSeries", 143 | "stacked": false, 144 | "region": "us-east-1", 145 | "stat": "Average", 146 | "period": 300, 147 | "yAxis": { 148 | "left": { 149 | "label": "Count", 150 | "showUnits": false 151 | } 152 | } 153 | } 154 | }, 155 | "title": "Lambda functions: by errors (bitmap)" 156 | } 157 | }, 158 | { 159 | "type": "custom", 160 | "width": 12, 161 | "height": 6, 162 | "properties": { 163 | "endpoint": "${lambdaFunction.Arn}", 164 | "updateOn": { 165 | "refresh": true, 166 | "resize": true, 167 | "timeRange": true 168 | }, 169 | "params": { 170 | "graph": { 171 | "metrics": [ 172 | [ 173 | { 174 | "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Throttles\"', 'Sum', 300)", 175 | "id": "lambda", 176 | "visible": false 177 | } 178 | ], 179 | [ 180 | { 181 | "expression": "SORT(lambda, SUM, DESC)", 182 | "label": "[sum: ${!SUM}]", 183 | "id": "sort" 184 | } 185 | ] 186 | ], 187 | "view": "timeSeries", 188 | "stacked": false, 189 | "region": "us-east-1", 190 | "stat": "Average", 191 | "period": 300, 192 | "yAxis": { 193 | "left": { 194 | "label": "Count", 195 | "showUnits": false 196 | } 197 | } 198 | } 199 | }, 200 | "title": "Lambda functions: by throttles (bitmap)" 201 | } 202 | } 203 | ] 204 | } -------------------------------------------------------------------------------- /samples/cloudWatchBitmapGraph/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - cloudwatch:GetMetricWidgetImage 6 | Effect: Allow 7 | Resource: 8 | - "*" 9 | PolicyName: cloudwatchGetMetricWidgetImage 10 | -------------------------------------------------------------------------------- /samples/cloudWatchBitmapGraph/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/cloudWatchMetricDataTable/cloudWatchMetricDataTable.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: display CloudWatch metric data as a table 5 | const { CloudWatchClient, GetMetricDataCommand } = require("@aws-sdk/client-cloudwatch"); 6 | 7 | const DOCS = ` 8 | ## Display CloudWatch metric data in a table 9 | Retrieves data from CloudWatch metric API and displays the datapoint values in a table. 10 | 11 | ### Widget parameters 12 | Param | Description 13 | ---|--- 14 | **MetricDataQueries** | An array of [MetricDataQuery](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDataQuery.html) definitions, same parameter that [GetMetricData API](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_GetMetricData.html) expects 15 | **region** | The region to call to get the data 16 | 17 | ### Example parameters 18 | \`\`\` yaml 19 | MetricDataQueries: 20 | - Expression: SEARCH('{AWS/Lambda,FunctionName} MetricName="Invocations"', 'Sum', 21 | 300) 22 | Id: lambda 23 | ReturnData: false 24 | - Expression: SORT(lambda, SUM, DESC) 25 | Label: '' 26 | Id: sort 27 | region: ${process.env.AWS_REGION} 28 | \`\`\` 29 | `; 30 | 31 | const CSS = ``; 32 | 33 | const orderData = data => { 34 | const allTimestamps = {}; 35 | const allTimestampsToValues = []; 36 | data.forEach(metricResult => { 37 | const timestampsToValues = {}; 38 | metricResult.Timestamps.forEach((timestamp, i) => { 39 | allTimestamps[timestamp] = [] 40 | timestampsToValues[timestamp] = metricResult.Values[i]; 41 | }); 42 | allTimestampsToValues.push(timestampsToValues); 43 | }); 44 | 45 | return { allTimestamps: Object.keys(allTimestamps).sort(), allTimestampsToValues }; 46 | }; 47 | 48 | const tableStart = allTimestamps => { 49 | const timestamps = allTimestamps.map(timestamp => { 50 | const d = new Date(timestamp); 51 | const mo = new Intl.DateTimeFormat('en', { month: 'short' }).format(d); 52 | const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(d); 53 | const h = d.getHours(); 54 | const m = '0' + new Intl.DateTimeFormat('en', { minute: '2-digit' }).format(d); 55 | return `${mo} ${da}
${h}:${m.slice(-2)}`; 56 | }).join(''); 57 | return ``; 58 | }; 59 | 60 | exports.handler = async (event) => { 61 | if (event.describe) { 62 | return DOCS; 63 | } 64 | 65 | const widgetContext = event.widgetContext; 66 | const timeRange = widgetContext.timeRange.zoom || widgetContext.timeRange; 67 | const start = new Date(timeRange.start); 68 | const end = new Date(timeRange.end); 69 | const params = { 70 | MetricDataQueries: event.MetricDataQueries, 71 | StartTime: start, 72 | EndTime: end 73 | }; 74 | const region = event.region; 75 | 76 | const cloudwatch = new CloudWatchClient({ region }); 77 | const command = new GetMetricDataCommand(params); 78 | 79 | const gmdResponse = await cloudwatch.send(command); 80 | const data = gmdResponse.MetricDataResults; 81 | const { allTimestamps, allTimestampsToValues } = orderData(data); 82 | 83 | const metricRows = data.map((metricResult, i) => { 84 | let html = ``; 85 | let orderedMetricData = allTimestampsToValues[i]; 86 | const values = allTimestamps.map(timestamp => { 87 | const value = orderedMetricData[timestamp]; 88 | return value === undefined ? '' : '' + value; 89 | }); 90 | html += ``; 91 | return html; 92 | }); 93 | 94 | return CSS + tableStart(allTimestamps) + `${metricRows.join('')}
${timestamps}
${metricResult.Label}${values.join('')}
`; 95 | }; 96 | -------------------------------------------------------------------------------- /samples/cloudWatchMetricDataTable/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "metric", 5 | "width": 24, 6 | "height": 6, 7 | "properties": { 8 | "metrics": [ 9 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Invocations\"', 'Sum', 300)", "id": "lambda", "visible": false } ], 10 | [ { "expression": "SORT(lambda, SUM, DESC)", "label": "", "id": "sort" } ] 11 | ], 12 | "region": "us-east-1", 13 | "stat": "Sum", 14 | "period": 300, 15 | "title": "Lambda invocations: graph - zoom in on me to see data table 'zoom'" 16 | } 17 | }, 18 | { 19 | "type": "custom", 20 | "width": 24, 21 | "height": 12, 22 | "properties": { 23 | "endpoint": "${lambdaFunction.Arn}", 24 | "updateOn": { 25 | "refresh": true, 26 | "timeRange": true 27 | }, 28 | "params": { 29 | "MetricDataQueries": [ 30 | { 31 | "Expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Invocations\"', 'Sum', 300)", 32 | "Id": "lambda", 33 | "ReturnData": false 34 | }, 35 | { 36 | "Expression": "SORT(lambda, SUM, DESC)", 37 | "Label": "", 38 | "Id": "sort" 39 | } 40 | ], 41 | "region": "us-east-1" 42 | }, 43 | "title": "Lambda invocations: data table" 44 | } 45 | } ] 46 | } -------------------------------------------------------------------------------- /samples/cloudWatchMetricDataTable/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - cloudwatch:GetMetricData 6 | Effect: Allow 7 | Resource: 8 | - "*" 9 | PolicyName: cloudwatchGetMetricData 10 | -------------------------------------------------------------------------------- /samples/cloudWatchMetricDataTable/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/codeDeploy/codeDeploy.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: display CodeDeploy deployments 5 | const { CodeDeployClient, ListDeploymentsCommand, BatchGetDeploymentsCommand } = require("@aws-sdk/client-codedeploy"); 6 | 7 | const CSS = ''; 8 | 9 | const DOCS = `## Display Code Deployments 10 | Displays all deployments from [CodeDeploy](https://console.aws.amazon.com/codesuite/codedeploy/deployments?region=us-east-1) 11 | 12 | ### Example parameters 13 | \`\`\` yaml 14 | region: ${process.env.AWS_REGION} 15 | \`\`\` 16 | `; 17 | 18 | const localReadableDate = (date, event) => { 19 | const offsetInMinutes = event.widgetContext.timezone.offsetInMinutes; 20 | const localDate = new Date(date.getTime() - offsetInMinutes * 60 * 1000); 21 | const readableDate = localDate.toISOString().replace(/T/, ' ').replace(/[.].*/, ''); 22 | return readableDate; 23 | }; 24 | 25 | const roundDate = (date, roundUp, minutes) => { 26 | const millis = minutes * 60 * 1000; 27 | const dateToRoundInMillis = date.getTime() - (roundUp ? 0 : millis); 28 | const roundedDate = new Date(Math.round(dateToRoundInMillis / millis) * millis); 29 | return roundedDate; 30 | }; 31 | 32 | async function getDeploymentIds(codeDeployClient, start, end) { 33 | let nextToken = null; 34 | let results = []; 35 | 36 | do { 37 | const command = new ListDeploymentsCommand({ createTimeRange: { start, end }, nextToken }); 38 | const result = await codeDeployClient.send(command); 39 | nextToken = result.nextToken; 40 | results = results.concat(result.deployments); 41 | } while (nextToken); 42 | return results; 43 | } 44 | 45 | async function getDeployments(codeDeployClient, start, end) { 46 | const deploymentIds = await getDeploymentIds(codeDeployClient, start, end); 47 | let results = []; 48 | while (deploymentIds.length > 0) { 49 | const deploymentsToQuery = deploymentIds.splice(0, Math.min(deploymentIds.length, 25)); 50 | const command = new BatchGetDeploymentsCommand({ deploymentIds: deploymentsToQuery }); 51 | const response = await codeDeployClient.send(command); 52 | results = results.concat(response.deploymentsInfo); 53 | } 54 | 55 | return results; 56 | } 57 | 58 | function getHtmlOutput(deployments, event) { 59 | let result = CSS; 60 | if (deployments.length == 0) { 61 | return `${result}

No deployments in the selected time range

`; 62 | } 63 | 64 | const applicationDeployments = deployments.reduce((accumulator, currentValue) => { 65 | if (!accumulator.hasOwnProperty(currentValue.applicationName)) { 66 | accumulator[currentValue.applicationName] = []; 67 | } 68 | accumulator[currentValue.applicationName].push(currentValue); 69 | return accumulator; 70 | }, {}); 71 | 72 | Object.keys(applicationDeployments).sort().forEach(x => { 73 | let deployments = applicationDeployments[x]; 74 | deployments = deployments.sort((a, b) => a.createTime.getTime() - b.createTime.getTime()); 75 | result = `${result}

${x}

`; 76 | result = `${result}`; 77 | deployments.forEach(deployment => { 78 | result = `${result} 79 | 80 | `; 81 | }); 82 | result += "
Deployment IdDeployment Group NameCreate TimeComplete TimeStatusCreator
${deployment.deploymentId}${deployment.deploymentGroupName}${localReadableDate(deployment.createTime, event)}${localReadableDate(deployment.completeTime, event)} | apply${deployment.status}${deployment.creator}
"; 83 | }); 84 | 85 | return result; 86 | } 87 | 88 | exports.handler = async (event) => { 89 | if (event.describe) { 90 | return DOCS; 91 | } 92 | const codeDeployClient = new CodeDeployClient({ region: event.region || process.env.AWS_REGION }); 93 | const timeRange = event.widgetContext.timeRange.zoom || event.widgetContext.timeRange; 94 | const deployments = await getDeployments(codeDeployClient, timeRange.start / 1000, timeRange.end / 1000); 95 | return getHtmlOutput(deployments, event); 96 | }; 97 | -------------------------------------------------------------------------------- /samples/codeDeploy/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "-P7D", 3 | "widgets": [ 4 | { 5 | "type": "metric", 6 | "width": 24, 7 | "height": 3, 8 | "properties": { 9 | "metrics": [ 10 | [ "AWS/Lambda", "Invocations" ] 11 | ], 12 | "view": "timeSeries", 13 | "stacked": false, 14 | "region": "${AWS::Region}", 15 | "stat": "Sum", 16 | "period": 300, 17 | "title": "Lambda invocations - zoom in on me to see deployments change" 18 | } 19 | }, 20 | { 21 | "type": "custom", 22 | "width": 24, 23 | "height": 8, 24 | "properties": { 25 | "endpoint": "${lambdaFunction.Arn}", 26 | "params": { 27 | "region": "${AWS::Region}" 28 | }, 29 | "updateOn": { 30 | "refresh": true, 31 | "timeRange": true 32 | }, 33 | "title": "Deployments" 34 | } 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /samples/codeDeploy/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - codedeploy:BatchGetDeployments 6 | - codedeploy:ListDeployments 7 | Effect: Allow 8 | Resource: 9 | - "*" 10 | PolicyName: viewCodeDeployments 11 | -------------------------------------------------------------------------------- /samples/codeDeploy/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/costExplorerReport/costExplorerReport.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: display sample Cost Explorer report 5 | const { CostExplorerClient, GetCostAndUsageCommand } = require("@aws-sdk/client-cost-explorer"); 6 | 7 | const DOCS = `## Display Cost Explorer report 8 | Displays a report on cost of each AWS service for the selected time range.`; 9 | 10 | const PALETTE = ['#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#7f7f7f','#bcbd22','#17becf','#aec7e8','#ffbb78','#98df8a','#ff9896','#c5b0d5','#c49c94','#f7b6d2','#c7c7c7','#dbdb8d','#9edae5' ]; 11 | const CSS = ``; 12 | 13 | const getCostResults = async (start, end) => { 14 | const ce = new CostExplorerClient({ region: 'us-east-1' }); 15 | let NextPageToken = null; 16 | let costs = []; 17 | do { // paginate until no next page token 18 | const params = { 19 | TimePeriod: { Start: start, End: end }, 20 | Granularity: 'DAILY', 21 | GroupBy: [ { Type: 'DIMENSION', Key: 'SERVICE' } ], 22 | Metrics: [ 'UnblendedCost' ], 23 | NextPageToken 24 | }; 25 | const command = new GetCostAndUsageCommand(params); 26 | const response = await ce.send(command); 27 | costs = costs.concat(response.ResultsByTime); 28 | NextPageToken = response.NextPageToken; 29 | } while (NextPageToken); 30 | return costs; 31 | }; 32 | 33 | const tableStart = (totalCost, start, end) => { 34 | return ``; 35 | }; 36 | 37 | const collate = costResults => { 38 | const scs = {}; 39 | let totalCost = 0; 40 | costResults.forEach(result => { 41 | result.Groups.forEach(group => { 42 | const serviceName = group.Keys[0]; 43 | let serviceCost = scs[serviceName] || 0; 44 | let currentDayCost = parseFloat(group.Metrics.UnblendedCost.Amount) || 0; 45 | totalCost += currentDayCost; 46 | serviceCost += parseFloat(group.Metrics.UnblendedCost.Amount); 47 | scs[serviceName] = serviceCost; 48 | }); 49 | }); 50 | const sortedScs = Object.entries(scs).sort(([,a],[,b]) => b-a); 51 | const maxServiceCost = Math.max(...sortedScs.map(cost => cost[1])); 52 | return { totalCost, serviceCosts: sortedScs, maxServiceCost }; 53 | }; 54 | 55 | const getCostResultsHtml = (costResults, start, end) => { 56 | costResults.sort((a, b) => a.TimePeriod.Start.localeCompare(b.TimePeriod.Start)); 57 | const { totalCost, serviceCosts, maxServiceCost } = collate(costResults); 58 | let html = tableStart(totalCost, start, end); 59 | 60 | serviceCosts.forEach((serviceEntry, i) => { 61 | const [ serviceName, serviceCost ] = serviceEntry; 62 | const percent = (serviceCost / totalCost * 100).toFixed(2); 63 | const maxPercent = (serviceCost / maxServiceCost * 100).toFixed(2); 64 | const color = PALETTE[i % PALETTE.length]; 65 | html += ``; 66 | }); 67 | return `${html}
ServiceTotal Cost: $${totalCost.toLocaleString(undefined, {maximumFractionDigits: 0 })} (${start} - ${end})
${serviceName}$${serviceCost.toLocaleString(undefined, {maximumFractionDigits: 2 })}
`; 68 | }; 69 | 70 | exports.handler = async (event) => { 71 | if (event.describe) { 72 | return DOCS; 73 | } 74 | const widgetContext = event.widgetContext; 75 | const timeRange = widgetContext.timeRange.zoom || widgetContext.timeRange; 76 | const start = timeRange.start; 77 | const end = timeRange.end; 78 | const minTimeDiff = Math.max(end - start, 24 * 60 * 60 * 1000); 79 | const newStart = end - minTimeDiff; 80 | const startStr = new Date(newStart).toISOString().split('T')[0]; 81 | const endStr = new Date(end).toISOString().split('T')[0]; 82 | const dailyCosts = await getCostResults(startStr, endStr); 83 | return CSS + getCostResultsHtml(dailyCosts, startStr, endStr); 84 | }; 85 | -------------------------------------------------------------------------------- /samples/costExplorerReport/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "-P30D", 3 | "widgets": [ 4 | { 5 | "type": "custom", 6 | "width": 24, 7 | "height": 8, 8 | "properties": { 9 | "endpoint": "${lambdaFunction.Arn}", 10 | "updateOn": { 11 | "refresh": true, 12 | "timeRange": true 13 | }, 14 | "title": "Cost Explorer Report - change time range to see report change" 15 | } 16 | }, 17 | { 18 | "type": "metric", 19 | "width": 24, 20 | "height": 6, 21 | "properties": { 22 | "metrics": [ 23 | [ { "expression": "SEARCH('{AWS/Billing,Currency,ServiceName} MetricName=\"EstimatedCharges\"', 'Average', 86400)", "id": "e1", "region": "us-east-1", "visible": false } ], 24 | [ { "expression": "SORT(e1, MAX, DESC)", "label": "[${!MAX}]", "id": "e2", "region": "us-east-1" } ] 25 | ], 26 | "view": "timeSeries", 27 | "stacked": false, 28 | "region": "us-east-1", 29 | "stat": "Average", 30 | "period": 300, 31 | "title": "Max daily cost - over time", 32 | "yAxis": { 33 | "left": { 34 | "label": "$", 35 | "showUnits": false 36 | } 37 | } 38 | } 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /samples/costExplorerReport/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - ce:GetCostAndUsage 6 | Effect: Allow 7 | Resource: 8 | - "*" 9 | PolicyName: costExplorerGetCostAndUsage 10 | -------------------------------------------------------------------------------- /samples/costExplorerReport/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/debugger/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "text", 5 | "width": 24, 6 | "height": 1, 7 | "properties": { 8 | "markdown": "Zoom in on the graph below to see how it affects the zoom parameters sent to the debugger Lamnda function" 9 | } 10 | }, 11 | { 12 | "type": "metric", 13 | "width": 24, 14 | "height": 3, 15 | "properties": { 16 | "metrics": [ 17 | [ "AWS/Lambda", "Invocations" ] 18 | ], 19 | "view": "timeSeries", 20 | "stacked": false, 21 | "region": "${AWS::Region}", 22 | "stat": "Sum", 23 | "period": 300, 24 | "title": "Lambda invocations - zoom in on me" 25 | } 26 | }, 27 | { 28 | "type": "custom", 29 | "width": 24, 30 | "height": 40, 31 | "properties": { 32 | "endpoint": "${lambdaFunction.Arn}", 33 | "params": { 34 | "param1": "value", 35 | "param2": [ 36 | "entry1", 37 | "entry2" 38 | ], 39 | "param3": { 40 | "sub": 7 41 | } 42 | }, 43 | "updateOn": { 44 | "refresh": true, 45 | "resize": true, 46 | "timeRange": true 47 | }, 48 | "title": "Debugger Lambda function " 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /samples/debugger/debugger.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: simple debugger, displays form and parameters passed to widget 5 | const DOCS = ` 6 | ## Custom Widget Debugger 7 | A simpler "debugger" custom widget that prints out: 8 | * the Lambda **event** oject including the **widgetContext** parameter passed to the widget by CloudWatch Dashboards 9 | * the Lambda **context** object 10 | * the Lambda enivronment variables 11 | 12 | ### Widget parameters 13 | Just pass in any parameters you want to see how they are sent to the Lambda script. 14 | 15 | ### Example parameters 16 | \`\`\` yaml 17 | --- 18 | param1: value 19 | param2: 20 | - entry1 21 | - entry2 22 | param3: 23 | sub: 7 24 | \`\`\` 25 | `; 26 | 27 | exports.handler = async (event, context) => { 28 | if (event.describe) { 29 | return DOCS; 30 | } 31 | 32 | const form = event.widgetContext.forms.all; 33 | const input = form.input || ''; 34 | const stage = form.stage || 'prod'; 35 | 36 | return ` 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 59 | 60 |
InputStage 46 | Popup 47 | 48 |

Form values:

49 | 50 | 51 | 52 |
Input:${input}
Stage:${stage}
53 |
54 |
56 | Submit 57 | 58 |
61 |
62 |

63 |

event

64 |
${JSON.stringify(event, null, 4)}
65 |

context

66 |
${JSON.stringify(context, null, 4)}
67 |

environment variables

68 |
${JSON.stringify(process.env, null, 4)}
69 | `; 70 | }; 71 | -------------------------------------------------------------------------------- /samples/debugger/debugger.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: simple debugger, displays form and parameters passed to widget 5 | import json 6 | import os 7 | 8 | DOCS = """ 9 | ## Custom Widget Debugger 10 | A simpler "debugger" custom widget that prints out: 11 | * the Lambda **event** oject including the **widgetContext** parameter passed to the widget by CloudWatch Dashboards 12 | * the Lambda **context** object 13 | * the Lambda enivronment variables 14 | 15 | ### Widget parameters 16 | Just pass in any parameters you want to see how they are sent to the Lambda script. 17 | 18 | ### Example parameters 19 | ``` yaml 20 | --- 21 | param1: value 22 | param2: 23 | - entry1 24 | - entry2 25 | param3: 26 | sub: 7 27 | ```""" 28 | 29 | def lambda_handler(event, context): 30 | if 'describe' in event: 31 | return DOCS 32 | 33 | form = event['widgetContext']['forms']['all'] 34 | input = form.get('input', '') 35 | stage = form.get('stage', 'prod') 36 | prettyEvent = json.dumps(event, indent=4, sort_keys=True) 37 | prettyContext = json.dumps(context.__dict__, indent=4, sort_keys=True, default=str) 38 | prettyEnv = "" 39 | for key, val in os.environ.items(): 40 | prettyEnv += f"{key}={val}\n" 41 | 42 | return f""" 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 61 | 65 | 66 |
InputStage 52 | Popup 53 | 54 |

Form values:

55 | 56 | 57 | 58 |
Input:{input}
Stage:{stage}
59 |
60 |
62 | Submit 63 | 64 |
67 |
68 |

69 |

event

70 |
{prettyEvent}
71 |

context

72 |
{prettyContext}
73 |

Lambda environment variables

74 |
{prettyEnv}
75 | """ 76 | -------------------------------------------------------------------------------- /samples/debugger/permissions.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cloudwatch-custom-widgets-samples/e337b8de4d0f5edf906d7ac2b17d609248301cc8/samples/debugger/permissions.yaml -------------------------------------------------------------------------------- /samples/debugger/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/ec2Table/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "-PT1H", 3 | "widgets": [ 4 | { 5 | "type": "custom", 6 | "width": 24, 7 | "height": 18, 8 | "properties": { 9 | "endpoint": "${lambdaFunction.Arn}", 10 | "params": { 11 | "maxInstances": 10 12 | }, 13 | "updateOn": { 14 | "refresh": true, 15 | "timeRange": true 16 | }, 17 | "title": "Top EC2 Instances by CPU" 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /samples/ec2Table/ec2Table.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: display EC2 Instances by CPU Utilization 5 | const { EC2Client, DescribeInstancesCommand, RebootInstancesCommand } = require("@aws-sdk/client-ec2"); 6 | const { CloudWatchClient, GetMetricDataCommand } = require("@aws-sdk/client-cloudwatch"); 7 | 8 | const DOCS = `## EC2 Table 9 | Displays top running EC2 instances by CPU Utilization. 10 | 11 | ### Example parameters 12 | \`\`\` yaml 13 | maxInstances: 10 14 | \`\`\` 15 | `; 16 | 17 | async function getInstances(ec2) { 18 | let NextToken = null; 19 | let instances = []; 20 | 21 | do { 22 | const args = { Filters: [ { Name: 'instance-state-name', Values: [ 'running' ]} ], NextToken }; 23 | const command = new DescribeInstancesCommand(args); 24 | const result = await ec2.send(command); 25 | NextToken = result.NextToken; 26 | result.Reservations.forEach(res => { 27 | instances = instances.concat(res.Instances); 28 | }); 29 | } while (NextToken); 30 | 31 | return instances; 32 | } 33 | 34 | async function reboot(ec2, instanceId) { 35 | try { 36 | const command = new RebootInstancesCommand({ InstanceIds: [ instanceId ] }); 37 | await ec2.send(command); 38 | return `Reboot was successfully trigger on the following instances: ${instanceId}.`; 39 | } catch (e) { 40 | return `Error rebooting instance. Note that the custom widget Lambda function's IAM role requires ec2:rebootInstances permission`; 41 | } 42 | } 43 | 44 | async function addCpu(cw, instances, StartTime, EndTime) { 45 | const Period = Math.floor((EndTime.getTime() - StartTime.getTime()) / 1000) 46 | const metrics = instances.map((instance, i) => { 47 | return { 48 | Id: `i${i}`, 49 | MetricStat: { Metric: { 50 | Namespace: 'AWS/EC2', MetricName: 'CPUUtilization', 51 | Dimensions: [ { Name: 'InstanceId', Value: instance.InstanceId } ] 52 | }, 53 | Stat: 'Average', Period 54 | } 55 | }; 56 | }); 57 | for (let i = 0; i < metrics.length; i += 500) { 58 | const command = new GetMetricDataCommand({ MetricDataQueries: metrics.slice(i, i + 500), StartTime, EndTime}); 59 | const result = await cw.send(command); 60 | result.MetricDataResults.forEach(metric => { 61 | const index = parseInt(metric.Id.slice(1)); 62 | instances[index].cpu = metric.Values.pop(); 63 | }); 64 | } 65 | } 66 | 67 | function getHtmlOutput(instances, region, event, context) { 68 | instances = instances.filter(i => i.cpu !== undefined).sort((i1, i2) => i1.cpu = i2.cpu); 69 | if (instances.length == 0) { 70 | return `

No running instances found reporting cpu for time period

`; 71 | } 72 | if (event.maxInstances) { 73 | instances = instances.slice(0, event.maxInstances); 74 | } 75 | 76 | const header = 'NameInstance IDInstance typeAvailability ZoneCPUAction' 77 | const rows = instances.map(i => { 78 | const nameTag = i.Tags.find(tag => tag.Key === 'Name'); 79 | const name = nameTag ? nameTag.Value : ''; 80 | return `${name} 81 | ${i.InstanceId}${i.InstanceType} 82 | ${i.Placement.AvailabilityZone}${i.cpu.toFixed(1)}% 83 | 84 | Reboot 85 | 87 | { "action": "Reboot", "instanceId": "${i.InstanceId}" } 88 | `; 89 | }).join(''); 90 | 91 | return `${header}${rows}
`; 92 | } 93 | 94 | exports.handler = async (event, context) => { 95 | if (event.describe) { 96 | return DOCS; 97 | } 98 | const widgetContext = event.widgetContext; 99 | const timeRange = widgetContext.timeRange.zoom || widgetContext.timeRange; 100 | const StartTime = new Date(timeRange.start); 101 | const EndTime = new Date(timeRange.end); 102 | const region = event.region || process.env.AWS_REGION; 103 | const ec2 = new EC2Client({ region }); 104 | const cw = new CloudWatchClient({ region }) 105 | 106 | if (event.action === 'Reboot') { 107 | return await reboot(ec2, event.instanceId); 108 | } else { 109 | const instances = await getInstances(ec2); 110 | await addCpu(cw, instances, StartTime, EndTime); 111 | return getHtmlOutput(instances, region, event, context); 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /samples/ec2Table/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - cloudwatch:GetMetricData 6 | - ec2:DescribeInstances 7 | Effect: Allow 8 | Resource: 9 | - "*" 10 | PolicyName: getEC2InstanceAndMetricData 11 | -------------------------------------------------------------------------------- /samples/ec2Table/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/echo/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 12, 6 | "height": 8, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "params": { 10 | "echo": "

Echo echo echo

" 11 | } 12 | } 13 | }, 14 | { 15 | "type": "custom", 16 | "width": 12, 17 | "height": 8, 18 | "properties": { 19 | "endpoint": "${lambdaFunction.Arn}", 20 | "params": { 21 | "echo": "

Input:

" 22 | } 23 | } 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /samples/echo/echo.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: simple echo script 5 | const DOCS = ` 6 | ## Echo 7 | A simple echo script. Anyting passed in \`\`\`echo\`\`\` parameter is returned as the content of custom widget. 8 | 9 | ### Widget parameters 10 | Param | Description 11 | ---|--- 12 | **echo** | The content to echo back 13 | 14 | ### Example parameters 15 | \`\`\` yaml 16 | echo:

Hello world

17 | \`\`\` 18 | `; 19 | 20 | exports.handler = async (event) => { 21 | if (event.describe) { 22 | return DOCS; 23 | } 24 | 25 | return event.echo || '
No "echo" parameter specified
'; 26 | }; 27 | -------------------------------------------------------------------------------- /samples/echo/echo.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: simple echo script 5 | DOCS = """ 6 | ## Echo 7 | A simple echo script. Anyting passed in ```echo``` parameter is returned as the content of custom widget. 8 | 9 | ### Widget parameters 10 | Param | Description 11 | ---|--- 12 | **echo** | The content to echo back 13 | 14 | ### Example parameters 15 | ``` yaml 16 | echo:

Hello world

17 | ```""" 18 | 19 | def lambda_handler(event, context): 20 | if 'describe' in event: 21 | return DOCS 22 | 23 | return event.get('echo', '
No "echo" parameter specified
') 24 | -------------------------------------------------------------------------------- /samples/echo/permissions.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cloudwatch-custom-widgets-samples/e337b8de4d0f5edf906d7ac2b17d609248301cc8/samples/echo/permissions.yaml -------------------------------------------------------------------------------- /samples/echo/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/emailDashboardSnapshot/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 24, 6 | "height": 3, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "params": { 10 | "sesRegion": "${AWS::Region}", 11 | "width": 24, 12 | "height": 3 13 | }, 14 | "title": "Email dashboard snapshot" 15 | } 16 | }, 17 | { 18 | "type": "metric", 19 | "width": 12, 20 | "height": 9, 21 | "properties": { 22 | "metrics": [ 23 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Invocations\"', 'Sum', 300)", "id": "lambda", "period": 300, "visible": false, "region": "us-east-1" } ], 24 | [ { "expression": "SORT(lambda, SUM, DESC)", "label": "[sum: ${!SUM}]", "id": "sort", "stat": "Sum", "region": "us-east-1" } ] 25 | ], 26 | "view": "timeSeries", 27 | "stacked": false, 28 | "region": "us-east-1", 29 | "stat": "Sum", 30 | "period": 300, 31 | "title": "Lambda functions: by invocations", 32 | "yAxis": { 33 | "left": { 34 | "label": "Count", 35 | "showUnits": false 36 | } 37 | } 38 | } 39 | }, 40 | { 41 | "type": "metric", 42 | "width": 12, 43 | "height": 9, 44 | "properties": { 45 | "metrics": [ 46 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Duration\"', 'p90', 300)", "id": "lambda", "region": "us-east-1", "visible": false } ], 47 | [ { "expression": "SORT(lambda, AVG, DESC)", "label": "[avg: ${!AVG}ms]", "id": "sort", "region": "us-east-1" } ] 48 | ], 49 | "view": "timeSeries", 50 | "stacked": false, 51 | "region": "us-east-1", 52 | "period": 300, 53 | "title": "Lambda functions: by duration (p90)", 54 | "stat": "Average", 55 | "yAxis": { 56 | "left": { 57 | "label": "Milliseconds", 58 | "showUnits": false 59 | } 60 | } 61 | } 62 | }, 63 | { 64 | "type": "metric", 65 | "width": 12, 66 | "height": 9, 67 | "properties": { 68 | "metrics": [ 69 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Errors\"', 'Sum', 300)", "id": "lambda", "period": 300, "visible": false, "region": "us-east-1" } ], 70 | [ { "expression": "SORT(lambda, SUM, DESC)", "label": "[sum: ${!SUM}]", "id": "sort", "stat": "Sum", "region": "us-east-1" } ] 71 | ], 72 | "view": "timeSeries", 73 | "stacked": false, 74 | "region": "us-east-1", 75 | "stat": "Sum", 76 | "period": 300, 77 | "title": "Lambda functions: by errors", 78 | "yAxis": { 79 | "left": { 80 | "label": "Count", 81 | "showUnits": false 82 | } 83 | } 84 | } 85 | }, 86 | { 87 | "type": "metric", 88 | "width": 12, 89 | "height": 9, 90 | "properties": { 91 | "metrics": [ 92 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Throttles\"', 'Sum', 300)", "id": "lambda", "period": 300, "visible": false, "region": "us-east-1" } ], 93 | [ { "expression": "SORT(lambda, SUM, DESC)", "label": "[sum: ${!SUM}]", "id": "sort", "stat": "Sum", "region": "us-east-1" } ] 94 | ], 95 | "view": "timeSeries", 96 | "stacked": false, 97 | "region": "us-east-1", 98 | "stat": "Sum", 99 | "period": 300, 100 | "title": "Lambda functions: by throttles", 101 | "yAxis": { 102 | "left": { 103 | "label": "Count", 104 | "showUnits": false 105 | } 106 | } 107 | } 108 | } 109 | ] 110 | } -------------------------------------------------------------------------------- /samples/emailDashboardSnapshot/emailDashboardSnapshot.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: send bitmap snapshot of a CloudWatch Dashboard by email 5 | import base64 6 | import boto3 7 | import datetime 8 | import json 9 | import os 10 | from email.mime.image import MIMEImage 11 | from email.mime.multipart import MIMEMultipart 12 | from email.mime.text import MIMEText 13 | 14 | DOCS = """ 15 | ## Send bitmap snapshot of a CloudWatch Dashboard by email 16 | Grabs bitmap snapshots of all metric widgets in the current CloudWatch Dashboard and sends them to an email address configured in [SES](/ses/home). 17 | ```""" 18 | 19 | def load_dashboard(dashboardName): 20 | cloudwatch = boto3.client('cloudwatch') 21 | dashboard = cloudwatch.get_dashboard(DashboardName=dashboardName) 22 | return json.loads(dashboard['DashboardBody']) 23 | 24 | def get_metric_bitmaps(widgets, start, end, unitWidth, unitHeight): 25 | bitmaps = [] 26 | for widget in widgets: 27 | if widget['type'] != 'metric': 28 | continue 29 | width = widget['width'] if 'width' in widget else 12 30 | height = widget['height'] if 'height' in widget else 12 31 | widgetProps = widget['properties'] 32 | graph = { **widgetProps, 'start': start, 'end': end, 'width': int(width * unitWidth), 'height': int(height * unitHeight) } 33 | params = { 'MetricWidget': json.dumps(graph) } 34 | region = widgetProps['region'] 35 | cloudwatch = boto3.client('cloudwatch', region_name=region) 36 | image = cloudwatch.get_metric_widget_image(**params) 37 | bitmaps.append(image['MetricWidgetImage']) 38 | 39 | return bitmaps 40 | 41 | def email_bitmaps(dashboardName, email, bitmaps, sesRegion): 42 | subject = f"Dashboard snapshot: {dashboardName}" 43 | html = f"

{subject}

" 44 | msg = MIMEMultipart() 45 | 46 | for index, bitmap in enumerate(bitmaps): 47 | mimeImg = MIMEImage(bitmap) 48 | mimeImg.add_header('Content-ID', f"") 49 | html += f"
" 50 | msg.attach(mimeImg) 51 | 52 | msg['Subject'] = subject 53 | msg['From'] = email 54 | msg['To'] = email 55 | msg.attach(MIMEText(html, 'html')) 56 | ses = boto3.client('ses', region_name=sesRegion) 57 | return ses.send_raw_email(Source=msg['From'], Destinations=[email], RawMessage={'Data': msg.as_string()}) 58 | 59 | def lambda_handler(event, context): 60 | if 'describe' in event: 61 | return DOCS 62 | form = event['widgetContext']['forms']['all'] 63 | email = form.get('email', '') 64 | widgetContext = event['widgetContext'] 65 | dashboardName = widgetContext['dashboardName'] 66 | timeRange = widgetContext['timeRange']['zoom'] if 'zoom' in widgetContext['timeRange'] else widgetContext['timeRange'] 67 | start = datetime.datetime.utcfromtimestamp(timeRange['start']/1000).isoformat() 68 | end = datetime.datetime.utcfromtimestamp(timeRange['end']/1000).isoformat() 69 | width = event['width'] 70 | height = event['height'] 71 | unitWidth = widgetContext['width'] / width 72 | unitHeight = widgetContext['height'] / height 73 | sesRegion = event['sesRegion'] 74 | msg = f"""
Only emails verified in SES Console can receive snapshot""" 75 | if email is not None and email != '': 76 | dashboard = load_dashboard(dashboardName) 77 | widgets = dashboard['widgets'] 78 | bitmaps = get_metric_bitmaps(widgets, start, end, unitWidth, unitHeight) 79 | 80 | try: 81 | sesResult = email_bitmaps(dashboardName, email, bitmaps, sesRegion) 82 | msg = f"
Snapshot was sent successfully to '{email}'
" 83 | except Exception as e: 84 | msg = f"
Snapshot failed to send to '{email}': {e}
" 85 | 86 | return f""" 87 |

88 |
EmailSubmit 89 | {{ "sesRegion": "{sesRegion}", "width": {width}, "height": {height} }}
90 |
{msg}""" 91 | -------------------------------------------------------------------------------- /samples/emailDashboardSnapshot/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - cloudwatch:GetDashboard 6 | - cloudwatch:GetMetricWidgetImage 7 | - ses:SendRawEmail 8 | Effect: Allow 9 | Resource: 10 | - "*" 11 | PolicyName: cloudwatchSendEmail 12 | -------------------------------------------------------------------------------- /samples/emailDashboardSnapshot/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readWrite 3 | -------------------------------------------------------------------------------- /samples/fetchURL/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 12, 6 | "height": 12, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "params": { 10 | "url": "https://images-na.ssl-images-amazon.com/images/G/01/error/137._TTD_.jpg" 11 | }, 12 | "title": "Fetch file from URL: Amazon Dog" 13 | } 14 | }, 15 | { 16 | "type": "custom", 17 | "width": 12, 18 | "height": 12, 19 | "properties": { 20 | "endpoint": "${lambdaFunction.Arn}", 21 | "params": { 22 | "url": "https://en.wikipedia.org/wiki/HTML" 23 | }, 24 | "title": "Fetch file from URL: Wikipedia/HTML" 25 | } 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /samples/fetchURL/fetchURL.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: fetch a file from a URL 5 | const DOCS = ` 6 | ## Fetch contents of a URL 7 | Gets the content of a specified URL, usually a HTML page, and displays it. 8 | 9 | ### Widget parameters 10 | Param | Description 11 | ---|--- 12 | **url** | The URL to display 13 | 14 | ### Example parameters 15 | \`\`\` yaml 16 | url: https://en.wikipedia.org/wiki/HTML 17 | \`\`\` 18 | `; 19 | 20 | const http = require('https'); 21 | const stream = require('stream').Transform; 22 | 23 | const IMAGE_FILETYPES = ['jpg', 'jpeg', 'bmp', 'gif', 'png' ]; 24 | 25 | exports.handler = async (event) => { 26 | if (event.describe) { 27 | return DOCS; 28 | } 29 | 30 | const url = event.url; // url is the parameter to fetch 31 | const fileType = url.substr(url.lastIndexOf('.') + 1); 32 | const isImage = IMAGE_FILETYPES.includes(fileType); 33 | const res = await new Promise(resolve => { 34 | http.get(url, resolve); 35 | }); 36 | 37 | // A ServerResponse is a readable stream, so you need to use the 38 | // stream-to-promise pattern to use it with async/await. 39 | let data = await new Promise((resolve, reject) => { 40 | let data = new stream(); 41 | res.on('data', chunk => data.push(chunk)); 42 | res.on('error', err => reject(err)); 43 | res.on('end', () => resolve(data)); 44 | }); 45 | const dataBuffer = new Buffer(data.read()); 46 | 47 | if (isImage) { 48 | return ``; 49 | } else { 50 | return dataBuffer.toString('utf-8') 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /samples/fetchURL/fetchURL.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: fetch a file from a URL 5 | import base64 6 | import os 7 | from urllib.request import urlopen 8 | 9 | DOCS = """ 10 | ## Fetch contents of a URL 11 | Gets the content of a specified URL, usually a HTML page, and displays it. 12 | 13 | ### Widget parameters 14 | Param | Description 15 | ---|--- 16 | **url** | The URL to display 17 | 18 | ### Example parameters 19 | ``` yaml 20 | url: https://en.wikipedia.org/wiki/HTML 21 | ```""" 22 | 23 | IMAGE_FILETYPES = ['.jpg', '.jpeg', '.bmp', '.gif', '.png' ]; 24 | 25 | def lambda_handler(event, context): 26 | if 'describe' in event: 27 | return DOCS 28 | 29 | url = event['url'] 30 | filename, file_extension = os.path.splitext(url) 31 | is_image = file_extension in IMAGE_FILETYPES 32 | data = urlopen(url).read() 33 | if is_image: 34 | base64_image = base64.b64encode(data).decode('UTF-8') 35 | return f"""""" 36 | else: 37 | return data.decode('utf-8') 38 | -------------------------------------------------------------------------------- /samples/fetchURL/permissions.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cloudwatch-custom-widgets-samples/e337b8de4d0f5edf906d7ac2b17d609248301cc8/samples/fetchURL/permissions.yaml -------------------------------------------------------------------------------- /samples/fetchURL/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/helloWorld/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 8, 6 | "height": 8, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}" 9 | } 10 | }, 11 | { 12 | "type": "custom", 13 | "width": 8, 14 | "height": 8, 15 | "properties": { 16 | "endpoint": "${lambdaFunction.Arn}", 17 | "params": { 18 | "name": "Jim" 19 | } 20 | } 21 | }, 22 | { 23 | "type": "custom", 24 | "width": 8, 25 | "height": 8, 26 | "properties": { 27 | "endpoint": "${lambdaFunction.Arn}", 28 | "params": { 29 | "name": "Mary" 30 | } 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /samples/helloWorld/helloWorld.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: simple hello world, takes optional 'name' as a parameter 5 | const DOCS = ` 6 | ## Hello World 7 | The basic starter "hello world" widget. Takes one optional parameter, **name**. 8 | 9 | ### Widget parameters 10 | Param | Description 11 | ---|--- 12 | **name** | The name to greet (optional) 13 | 14 | ### Example parameters 15 | \`\`\` yaml 16 | name: widget developer 17 | \`\`\` 18 | `; 19 | 20 | exports.handler = async (event) => { 21 | if (event.describe) { 22 | return DOCS; 23 | } 24 | 25 | const name = event.name || 'friend'; 26 | return `

Hello ${name}

`; 27 | }; 28 | -------------------------------------------------------------------------------- /samples/helloWorld/helloWorld.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: simple hello world, takes optional 'name' as a parameter 5 | DOCS = """ 6 | ## Hello World 7 | The basic starter "hello world" widget. Takes one optional parameter, **name**. 8 | 9 | ### Widget parameters 10 | Param | Description 11 | ---|--- 12 | **name** | The name to greet (optional) 13 | 14 | ### Example parameters 15 | ``` yaml 16 | name: widget developer 17 | ```""" 18 | 19 | def lambda_handler(event, context): 20 | if 'describe' in event: 21 | return DOCS 22 | 23 | name = event.get('name', 'friend') 24 | return f'

Hello {name}

' -------------------------------------------------------------------------------- /samples/helloWorld/permissions.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cloudwatch-custom-widgets-samples/e337b8de4d0f5edf906d7ac2b17d609248301cc8/samples/helloWorld/permissions.yaml -------------------------------------------------------------------------------- /samples/helloWorld/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/includeTextWidget/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 24, 6 | "height": 3, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "params": { 10 | "dashboardName": "" 11 | }, 12 | "title": "Dashboard menu" 13 | } 14 | }, 15 | { 16 | "type": "metric", 17 | "width": 12, 18 | "height": 9, 19 | "properties": { 20 | "metrics": [ 21 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Invocations\"', 'Sum', 300)", "id": "lambda", "period": 300, "visible": false, "region": "us-east-1" } ], 22 | [ { "expression": "SORT(lambda, SUM, DESC)", "label": "[sum: ${!SUM}]", "id": "sort", "stat": "Sum", "region": "us-east-1" } ] 23 | ], 24 | "view": "timeSeries", 25 | "stacked": false, 26 | "region": "us-east-1", 27 | "stat": "Sum", 28 | "period": 300, 29 | "title": "Lambda functions: by invocations", 30 | "yAxis": { 31 | "left": { 32 | "label": "Count", 33 | "showUnits": false 34 | } 35 | } 36 | } 37 | }, 38 | { 39 | "type": "metric", 40 | "width": 12, 41 | "height": 9, 42 | "properties": { 43 | "metrics": [ 44 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Duration\"', 'p90', 300)", "id": "lambda", "region": "us-east-1", "visible": false } ], 45 | [ { "expression": "SORT(lambda, AVG, DESC)", "label": "[avg: ${!AVG}ms]", "id": "sort", "region": "us-east-1" } ] 46 | ], 47 | "view": "timeSeries", 48 | "stacked": false, 49 | "region": "us-east-1", 50 | "period": 300, 51 | "title": "Lambda functions: by duration (p90)", 52 | "stat": "Average", 53 | "yAxis": { 54 | "left": { 55 | "label": "Milliseconds", 56 | "showUnits": false 57 | } 58 | } 59 | } 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /samples/includeTextWidget/includeTextWidget.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: display content of a text widget from other dashboard 5 | const { CloudWatchClient, GetDashboardCommand } = require("@aws-sdk/client-cloudwatch"); 6 | 7 | const DOCS = ` 8 | ## Include Text Widget from CloudWatch Dashboard 9 | This widget displays the first text widget from a specified CloudWatch Dashboard. 10 | 11 | This is useful for embedding the same text context in multiple dashboards and update it from a central place. An example would be a menu of links between dashboards. 12 | 13 | ### Widget parameters 14 | Param | Description 15 | ---|--- 16 | **dashboardName** | The name of the dashboard from which to load the text widget. The first text widget on that dashboard is loaded. 17 | 18 | ### Example parameters 19 | \`\`\` yaml 20 | dashboardName: sharedMenu 21 | \`\`\` 22 | `; 23 | 24 | const CSS = ``; 29 | 30 | exports.handler = async (event) => { 31 | if (event.describe) { 32 | return DOCS; 33 | } 34 | 35 | const region = event.region || process.env.AWS_REGION; 36 | const cloudwatch = new CloudWatchClient({ region }); 37 | const dashboardName = event.dashboardName || ''; 38 | if (dashboardName === '') { 39 | return `${CSS}
dashboardName parameter not set, please set by editing widget and entering dashboard name to load text widget from
`; 40 | } 41 | 42 | try { 43 | const command = new GetDashboardCommand({ DashboardName: dashboardName }); 44 | const dashboardResponse = await cloudwatch.send(command); 45 | const widgets = JSON.parse(dashboardResponse.DashboardBody).widgets; 46 | for (const widget of widgets) { 47 | if (widget['type'] === 'text') { 48 | return { markdown: widget.properties.markdown }; 49 | } 50 | } 51 | 52 | return `${CSS}
No text widget found in dashboard ${dashboardName}
Content:
${JSON.stringify(widgets, null, 4)}
`; 53 | } catch (error) { 54 | console.error("Error fetching dashboard:", error); 55 | return `${CSS}
Error fetching dashboard: ${error.message}
`; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /samples/includeTextWidget/includeTextWidget.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: display content of a text widget from other dashboard 5 | import base64 6 | import boto3 7 | import datetime 8 | import json 9 | 10 | DOCS = """ 11 | ## Include Text Widget from CloudWatch Dashboard 12 | This widget displays the first text widget from a specified CloudWatch Dashboard. 13 | 14 | This is useful for embedding the same text context in multiple dashboards and update it from a central place. An example would be a menu of links between dashboards. 15 | 16 | ### Widget parameters 17 | Param | Description 18 | ---|--- 19 | **dashboardName** | The name of the dashboard from which to load the text widget. The first text widget on that dashboard is loaded. 20 | 21 | ### Example parameters 22 | ``` yaml 23 | dashboardName: sharedMenu 24 | ```""" 25 | 26 | CSS = """""" 31 | 32 | def lambda_handler(event, context): 33 | if 'describe' in event: 34 | return DOCS 35 | 36 | cloudwatch = boto3.client('cloudwatch') 37 | dashboardName = event['dashboardName'] if 'dashboardName' in event else '' 38 | 39 | if dashboardName == '': 40 | return f"""{CSS}
dashboardName parameter not set, please set by editing widget and entering dashboard name to load text widget from
""" 41 | 42 | dashboard = cloudwatch.get_dashboard(DashboardName=dashboardName) 43 | dashboardBody = json.loads(dashboard['DashboardBody']) 44 | widgets = dashboardBody['widgets'] 45 | for widget in widgets: 46 | if widget['type'] == 'text': 47 | return { 'markdown': widget['properties']['markdown'] } 48 | 49 | return f"{CSS}
No text widget found in dashboard {dashboardName}
Content:
{dashboardBody}
"; 50 | -------------------------------------------------------------------------------- /samples/includeTextWidget/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - cloudwatch:GetDashboard 6 | Effect: Allow 7 | Resource: 8 | - "*" 9 | PolicyName: cloudwatchGetDashboard 10 | -------------------------------------------------------------------------------- /samples/includeTextWidget/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/logsInsightsQuery/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "-PT1H", 3 | "widgets": [ 4 | { 5 | "type": "custom", 6 | "width": 24, 7 | "height": 18, 8 | "properties": { 9 | "endpoint": "${lambdaFunction.Arn}", 10 | "params": { 11 | "region": "${AWS::Region}", 12 | "logGroups": "/aws/lambda/${AWS::StackName}", 13 | "query": "fields @timestamp, @message | sort @timestamp desc | limit 20" 14 | }, 15 | "updateOn": { 16 | "refresh": true, 17 | "timeRange": true 18 | }, 19 | "title": "Logs Insights Query, ${AWS::Region}" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /samples/logsInsightsQuery/logsInsightsQuery.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: display results of Logs Insights queries 5 | const { CloudWatchLogsClient, StartQueryCommand, GetQueryResultsCommand } = require("@aws-sdk/client-cloudwatch-logs"); 6 | 7 | const CHECK_QUERY_STATUS_DELAY_MS = 250; 8 | const CSS = '' 9 | const ORIGINAL_QUERY = 'fields @timestamp, @message | sort @timestamp desc | limit 20'; 10 | 11 | const DOCS = ` 12 | ## Run Logs Insights Query 13 | Runs a Logs Insights query and displays results in a table. 14 | 15 | ### Widget parameters 16 | Param | Description 17 | ---|--- 18 | **logGroups** | The log groups (comma-separated) to run query against 19 | **query** | The query to run 20 | **region** | The region to run the query in 21 | 22 | ### Example parameters 23 | \`\`\` yaml 24 | logGroups: ${process.env.AWS_LAMBDA_LOG_GROUP_NAME} 25 | query: '${ORIGINAL_QUERY}' 26 | region: ${process.env.AWS_REGION} 27 | \`\`\` 28 | `; 29 | 30 | const runQuery = async (logsClient, logGroups, queryString, startTime, endTime) => { 31 | const startQueryCommand = new StartQueryCommand({ 32 | logGroupNames: logGroups.replace(/\s/g, '').split(','), 33 | queryString, 34 | startTime, 35 | endTime 36 | }); 37 | const startQuery = await logsClient.send(startQueryCommand); 38 | const queryId = startQuery.queryId; 39 | 40 | while (true) { 41 | const getQueryResultsCommand = new GetQueryResultsCommand({ queryId }); 42 | const queryResults = await logsClient.send(getQueryResultsCommand); 43 | if (queryResults.status !== 'Complete') { 44 | await sleep(CHECK_QUERY_STATUS_DELAY_MS); // Sleep before calling again 45 | } else { 46 | return queryResults.results; 47 | } 48 | } 49 | }; 50 | 51 | const sleep = async (delay) => { 52 | return new Promise((resolve) => setTimeout(resolve, delay)); 53 | }; 54 | 55 | const displayResults = async (logGroups, query, results, context) => { 56 | let html = ` 57 |
58 | 59 | 60 | 61 | 62 | 63 |
Log Groups
Query
64 |
Run query 65 | 66 | Reset to original query 67 | 68 | { "resetQuery": true } 69 | 70 |

71 |

Results

72 | `; 73 | const stripPtr = result => result.filter(entry => entry.field !== '@ptr'); 74 | 75 | if (results && results.length > 0) { 76 | const cols = stripPtr(results[0]).map(entry => entry.field); 77 | 78 | html += ``; 79 | 80 | results.forEach(row => { 81 | const vals = stripPtr(row).map(entry => entry.value); 82 | html += ``; 83 | }); 84 | 85 | html += `
${cols.join('')}
${vals.join('')}
` 86 | } else { 87 | html += `
${JSON.stringify(results, null, 4)}
`; 88 | } 89 | 90 | return html; 91 | }; 92 | 93 | exports.handler = async (event, context) => { 94 | if (event.describe) { 95 | return DOCS; 96 | } 97 | 98 | const widgetContext = event.widgetContext; 99 | const form = widgetContext.forms.all; 100 | const logGroups = form.logGroups || event.logGroups; 101 | const region = widgetContext.params.region || event.region || process.env.AWS_REGION; 102 | const timeRange = widgetContext.timeRange.zoom || widgetContext.timeRange; 103 | const logsClient = new CloudWatchLogsClient({ region }); 104 | const resetQuery = event.resetQuery; 105 | 106 | let query = form.query || event.query; 107 | if (resetQuery) { 108 | query = widgetContext.params.query || ORIGINAL_QUERY; 109 | } 110 | 111 | let results; 112 | 113 | if (query && query.trim() !== '') { 114 | try { 115 | results = await runQuery(logsClient, logGroups, query, timeRange.start, timeRange.end); 116 | } catch (e) { 117 | results = e; 118 | } 119 | } 120 | 121 | return CSS + await displayResults(logGroups, query, results, context); 122 | }; 123 | -------------------------------------------------------------------------------- /samples/logsInsightsQuery/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - logs:StartQuery 6 | - logs:GetQueryResults 7 | Effect: Allow 8 | Resource: 9 | - "*" 10 | PolicyName: cloudwatchRunsLogsInsightsQueries 11 | -------------------------------------------------------------------------------- /samples/logsInsightsQuery/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/rssReader/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 12, 6 | "height": 9, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "params": { 10 | "url": "http://status.aws.amazon.com/rss/cloudwatch-us-east-1.rss", 11 | "entryTag": "./channel/item", 12 | "fields": [ "title", "pubDate", "description" ] 13 | }, 14 | "updateOn": { 15 | "refresh": true 16 | }, 17 | "title": "RSS: CloudWatch status updates" 18 | } 19 | }, 20 | { 21 | "type": "custom", 22 | "width": 12, 23 | "height": 9, 24 | "properties": { 25 | "endpoint": "${lambdaFunction.Arn}", 26 | "params": { 27 | "url": "https://gizmodo.com/rss", 28 | "entryTag": "./channel/item", 29 | "fields": [ "title", "pubDate", "description" ] 30 | }, 31 | "updateOn": { 32 | "refresh": true 33 | }, 34 | "title": "RSS: Gizmodo" 35 | } 36 | }, 37 | { 38 | "type": "custom", 39 | "width": 12, 40 | "height": 9, 41 | "properties": { 42 | "endpoint": "${lambdaFunction.Arn}", 43 | "params": { 44 | "url": "http://www.reddit.com/r/Videos/top/.rss", 45 | "entryTag": "entry", 46 | "fields": [ "title", "updated", "content" ] 47 | }, 48 | "title": "RSS: reddit, r/Videos" 49 | } 50 | }, 51 | { 52 | "type": "custom", 53 | "width": 12, 54 | "height": 9, 55 | "properties": { 56 | "endpoint": "${lambdaFunction.Arn}", 57 | "params": { 58 | "url": "http://aws.amazon.com/new/feed/", 59 | "entryTag": "./channel/item", 60 | "fields": [ "title", "pubDate", "description" ] 61 | }, 62 | "updateOn": { 63 | "refresh": true 64 | }, 65 | "title": "RSS: AWS What's New" 66 | } 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /samples/rssReader/permissions.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cloudwatch-custom-widgets-samples/e337b8de4d0f5edf906d7ac2b17d609248301cc8/samples/rssReader/permissions.yaml -------------------------------------------------------------------------------- /samples/rssReader/rssReader.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: simple RSS reader 5 | import base64 6 | import os 7 | from urllib.request import urlopen 8 | import json 9 | import xml.etree.ElementTree as ET 10 | from io import StringIO 11 | 12 | DOCS = """ 13 | ## RSS Reader 14 | Reads and displays content of an RSS feed. 15 | 16 | ### Widget parameters 17 | Param | Description 18 | ---|--- 19 | **url** | The URL of the RSS feed 20 | **entryTag** | The XML tag of the element in the feed containing each "news" entry 21 | **fields** | An array of field names to extract and display from each entry 22 | 23 | ### Example parameters 24 | ``` yaml 25 | url: http://status.aws.amazon.com/rss/cloudwatch-us-east-1.rss 26 | entryTag: ./channel/item 27 | fields: [ title, pubDate, description ] 28 | ```""" 29 | 30 | CSS = """ 31 | """ 45 | 46 | def parse_xml(xml): 47 | it = ET.iterparse(StringIO(xml)) 48 | for _, el in it: 49 | prefix, has_namespace, postfix = el.tag.partition('}') 50 | if has_namespace: 51 | el.tag = postfix # strip all namespaces 52 | root = it.root 53 | return root 54 | 55 | def lambda_handler(event, context): 56 | if 'describe' in event: 57 | return DOCS 58 | 59 | url = event['url'] 60 | entryTag = event['entryTag'] 61 | fields = event['fields'] 62 | data = urlopen(url).read() 63 | xml = data.decode('utf-8') 64 | rss = parse_xml(xml) 65 | html = '' 66 | 67 | for entry in rss.findall(entryTag): 68 | html += '
' 69 | for index, field in enumerate(fields): 70 | split = field.split(".") 71 | if len(split) == 2: 72 | value = entry.find(split[0]) 73 | if value is not None: 74 | attr = value.get(split[1]) 75 | if attr is not None: 76 | html += f"{split[0]}: {attr}
" 77 | else: 78 | value = entry.find(field) 79 | if value is not None and value.text is not None: 80 | # first field is the title 81 | if index == 0: 82 | html += f"

{value.text}


" 83 | else: 84 | html += f"{value.text}
" 85 | html += '
' 86 | 87 | return CSS + html -------------------------------------------------------------------------------- /samples/rssReader/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/s3GetObject/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 24, 6 | "height": 12, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "params": { 10 | "bucket": "custom-widget-demo-bucket", 11 | "key": "sample-report.html" 12 | }, 13 | "title": "Sample S3 Report - Power stats" 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /samples/s3GetObject/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - s3:GetObject 6 | Effect: Allow 7 | Resource: 8 | - "*" 9 | PolicyName: s3GetObject 10 | -------------------------------------------------------------------------------- /samples/s3GetObject/s3GetObject.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: display an object from S3 bucket 5 | const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3"); 6 | 7 | const DOCS = ` 8 | ## S3 Get Object 9 | Displays the content of a file (usually HTML) stored in S3 in the current account. 10 | 11 | This is useful for displaying dynamic content stored and updated separately from a dashboard, such as the results of a daily or hourly report. 12 | 13 | ### Widget parameters 14 | Param | Description 15 | ---|--- 16 | **bucket** | The name of the S3 bucket owned by this account 17 | **key** | The key of the S3 object 18 | 19 | ### Example parameters 20 | \`\`\` yaml 21 | bucket: custom-widget-demo-bucket 22 | key: sample-report.html 23 | \`\`\` 24 | `; 25 | 26 | exports.handler = async (event) => { 27 | if (event.describe) { 28 | return DOCS; 29 | } 30 | 31 | const region = event.region || process.env.AWS_REGION; 32 | const params = { 33 | Bucket: event.bucket, 34 | Key: event.key 35 | }; 36 | 37 | const s3Client = new S3Client({ region }); 38 | const command = new GetObjectCommand(params); 39 | 40 | const response = await s3Client.send(command); 41 | try { 42 | const bodyContents = await response.Body.transformToString(); 43 | return bodyContents; 44 | } catch (e) { 45 | console.error(e); 46 | return `Error fetching S3 object: ${e.message}`; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /samples/s3GetObject/s3GetObject.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: display an object from S3 bucket 5 | DOCS = """ 6 | ## S3 Get Object 7 | Displays the content of a file (usually HTML) stored in S3 in the current account. 8 | 9 | This is useful for displaying dynamic content stored and updated separately from a dashboard, such as the results of a daily or hourly report. 10 | 11 | ### Widget parameters 12 | Param | Description 13 | ---|--- 14 | **bucket** | The name of the S3 bucket owned by this account 15 | **key** | The key of the S3 object 16 | 17 | ### Example parameters 18 | ``` yaml 19 | bucket: custom-widget-demo-bucket 20 | key: sample-report.html 21 | ```""" 22 | 23 | import boto3 24 | import json 25 | import os 26 | 27 | def lambda_handler(event, context): 28 | if 'describe' in event: 29 | return DOCS 30 | 31 | region = event.get('region', os.environ['AWS_REGION']) 32 | s3 = boto3.client('s3', region_name=region) 33 | result = s3.get_object(Bucket=event['bucket'], Key=event['key']) 34 | 35 | return result['Body'].read().decode('utf-8') 36 | -------------------------------------------------------------------------------- /samples/s3GetObject/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/simplePie/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 12, 6 | "height": 6, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "updateOn": { 10 | "refresh": true 11 | }, 12 | "params": { 13 | "slices": [ 14 | { 15 | "label": "Sky", 16 | "value": 135, 17 | "color": "blue" 18 | }, 19 | { 20 | "label": "Shady", 21 | "value": 22.5, 22 | "color": "#6E4435" 23 | }, 24 | { 25 | "label": "Sunny", 26 | "value": 45, 27 | "color": "#DDAF59" 28 | }, 29 | { 30 | "label": "Sky", 31 | "value": 135, 32 | "color": "blue" 33 | } 34 | ] 35 | }, 36 | "title": "Pyramid" 37 | } 38 | }, 39 | { 40 | "type": "custom", 41 | "width": 12, 42 | "height": 6, 43 | "properties": { 44 | "endpoint": "${lambdaFunction.Arn}", 45 | "updateOn": { 46 | "refresh": true 47 | }, 48 | "params": { 49 | "slices": [ 50 | { 51 | "value": 100, 52 | "label": "Right wall" 53 | }, 54 | { 55 | "value": 130, 56 | "label": "Floor" 57 | }, 58 | { 59 | "value": 130, 60 | "label": "Left wall" 61 | } 62 | ] 63 | }, 64 | "title": "My living room corner" 65 | } 66 | }, 67 | { 68 | "type": "custom", 69 | "width": 12, 70 | "height": 9, 71 | "properties": { 72 | "endpoint": "${lambdaFunction.Arn}", 73 | "updateOn": { 74 | "refresh": true 75 | }, 76 | "params": { 77 | "legendHeight": 75, 78 | "slices": [ 79 | { 80 | "value": 16, 81 | "label": "Give you up" 82 | }, 83 | { 84 | "value": 16, 85 | "label": "Let you down" 86 | }, 87 | { 88 | "value": 16, 89 | "label": "Run around and desert you" 90 | }, 91 | { 92 | "value": 16, 93 | "label": "Make you cry" 94 | }, 95 | { 96 | "value": 16, 97 | "label": "Say goodbye" 98 | }, 99 | { 100 | "value": 16, 101 | "label": "Tell a lie and hurt you" 102 | }, 103 | { 104 | "value": 5, 105 | "label": "Give, never gonna give" 106 | } 107 | ] 108 | }, 109 | "title": "Rick Astley is \"never gonna ...\"" 110 | } 111 | }, 112 | { 113 | "type": "custom", 114 | "width": 12, 115 | "height": 9, 116 | "properties": { 117 | "endpoint": "${lambdaFunction.Arn}", 118 | "updateOn": { 119 | "refresh": true 120 | }, 121 | "params": { 122 | "slices": [ 123 | { 124 | "label": "Remaining", 125 | "value": 90 126 | }, 127 | { 128 | "label": "Eaten", 129 | "color": "white", 130 | "value": 10 131 | } 132 | ] 133 | }, 134 | "title": "Pizza consumption" 135 | } 136 | } 137 | ] 138 | } 139 | -------------------------------------------------------------------------------- /samples/simplePie/permissions.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cloudwatch-custom-widgets-samples/e337b8de4d0f5edf906d7ac2b17d609248301cc8/samples/simplePie/permissions.yaml -------------------------------------------------------------------------------- /samples/simplePie/simplePie.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // CloudWatch Custom Widget sample: draw a simple SVG pie chart 5 | const DOCS = ` 6 | ## Simple Pie, using SVG 7 | This pie chart is a simple example of how SVG can be used to display graphical content in a custom widget. 8 | 9 | ### Widget parameters 10 | Param | Description 11 | ---|--- 12 | **slices** | Array containing slice data. Each entry must contain **value** (numerical value for size of slice) and **label** (the slice's label) 13 | **legendHeight** | The height of the legend in pixels (optional, defaults to 25) 14 | 15 | ### Example parameters 16 | \`\`\` yaml 17 | --- 18 | legendHeight: 75 19 | slices: 20 | - value: 16 21 | label: Give you up 22 | - value: 16 23 | label: Let you down 24 | - value: 16 25 | label: Run around and desert you 26 | - value: 16 27 | label: Make you cry 28 | - value: 16 29 | label: Say goodbye 30 | - value: 16 31 | label: Tell a lie and hurt you 32 | - value: 5 33 | label: Give, never gonna give 34 | \`\`\` 35 | `; 36 | 37 | const PALETTE = [ '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#aec7e8', '#ffbb78', '#98df8a', '#ff9896', '#c5b0d5', '#c49c94', '#f7b6d2', '#c7c7c7', '#dbdb8d', '#9edae5' ]; 38 | const SVG_RADIUS = 200; 39 | const css = margin => { 40 | return ``; 63 | } 64 | 65 | const getCoordinatesForAngle = (angle) => { 66 | const x = parseInt(Math.round(SVG_RADIUS + (SVG_RADIUS - 5) * Math.cos(Math.PI * angle / 180))); 67 | const y = parseInt(Math.round(SVG_RADIUS + (SVG_RADIUS - 5) * Math.sin(Math.PI * angle / 180))); 68 | return [x, y]; 69 | }; 70 | 71 | exports.handler = async (event) => { 72 | if (event.describe) { 73 | return DOCS; 74 | } 75 | 76 | const slices = event.slices; 77 | const total = slices.reduce((total, slice) => total + slice.value, 0); 78 | let startAngle, endAngle = -90; 79 | 80 | const slicesHtml = slices.map((slice, i) => { 81 | const angle = 360 * slice.value / total; 82 | startAngle = endAngle; 83 | endAngle = startAngle + angle; 84 | const [x1, y1] = getCoordinatesForAngle(startAngle); 85 | const [x2, y2] = getCoordinatesForAngle(endAngle); 86 | 87 | var d = `M200,200 L${x1},${y1} A195,195 0 ${((endAngle-startAngle > 180) ? 1 : 0)},1 ${x2},${y2} z`; 88 | const color = slice.color || PALETTE[i % PALETTE.length]; 89 | 90 | // create a and append it to the element 91 | return {path: ``, 92 | label:`
${slice.label}
` }; 93 | }); 94 | 95 | return `${css(event.legendHeight || 25)} 96 | 97 | ${slicesHtml.map(slice => slice.path).join('')} 98 | 99 |
${slicesHtml.map(slice => slice.label).join(' ')}
`; 100 | } -------------------------------------------------------------------------------- /samples/simplePie/simplePie.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: draw a simple SVG pie chart 5 | import math 6 | 7 | DOCS = """ 8 | ## Simple Pie, using SVG 9 | This pie chart is a simple example of how SVG can be used to display graphical content in a custom widget. 10 | 11 | ### Widget parameters 12 | Param | Description 13 | ---|--- 14 | **slices** | Array containing slice data. Each entry must contain **value** (numerical value for size of slice) and **label** (the slice's label) 15 | **legendHeight** | The height of the legend in pixels (optional, defaults to 25) 16 | 17 | ### Example parameters 18 | ``` yaml 19 | --- 20 | legendHeight: 75 21 | slices: 22 | - value: 16 23 | label: Give you up 24 | - value: 16 25 | label: Let you down 26 | - value: 16 27 | label: Run around and desert you 28 | - value: 16 29 | label: Make you cry 30 | - value: 16 31 | label: Say goodbye 32 | - value: 16 33 | label: Tell a lie and hurt you 34 | - value: 5 35 | label: Give, never gonna give 36 | ```""" 37 | 38 | PALETTE = [ '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#aec7e8', '#ffbb78', '#98df8a', '#ff9896', '#c5b0d5', '#c49c94', '#f7b6d2', '#c7c7c7', '#dbdb8d', '#9edae5' ] 39 | SVG_RADIUS = 200 40 | 41 | def css(margin): 42 | return f"""""" 65 | 66 | def getCoordinatesForAngle(angle): 67 | x = int(round(SVG_RADIUS + (SVG_RADIUS - 5) * math.cos(math.pi * angle / 180))) 68 | y = int(round(SVG_RADIUS + (SVG_RADIUS - 5) * math.sin(math.pi * angle / 180))) 69 | return [x, y] 70 | 71 | 72 | def lambda_handler(event, context): 73 | if 'describe' in event: 74 | return DOCS 75 | 76 | slices = event['slices'] 77 | legendHeight = event['legendHeight'] if 'legendHeight' in event else 25 78 | styles = css(legendHeight) 79 | startAngle = -90 80 | endAngle = -90 81 | total = 0 82 | paths = [] 83 | labels = [] 84 | 85 | for slice in slices: 86 | total += slice['value'] 87 | 88 | for i, slice in enumerate(slices): 89 | value = slice['value'] 90 | label = slice['label'] 91 | color = slice['color'] if 'color' in slice else PALETTE[i % len(PALETTE)] 92 | angle = 360 * value / total 93 | startAngle = endAngle; 94 | endAngle = startAngle + angle; 95 | 96 | [x1, y1] = getCoordinatesForAngle(startAngle) 97 | [x2, y2] = getCoordinatesForAngle(endAngle) 98 | 99 | # create an array and join it just for code readability 100 | pathData = f"M200,200 L{x1},{y1} A195,195 0 {1 if (endAngle-startAngle > 180) else 0},1 {x2},{y2} z" 101 | 102 | # create a and append it to the element 103 | paths.append(f"""""") 104 | labels.append(f"""
{label}
""") 105 | 106 | return f"""{styles} 107 | 108 | {' '.join(paths)} 109 | 110 |
{' '.join(labels)}
""" 111 | -------------------------------------------------------------------------------- /samples/simplePie/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readOnly 3 | -------------------------------------------------------------------------------- /samples/snapshotDashboardToS3/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "custom", 5 | "width": 24, 6 | "height": 3, 7 | "properties": { 8 | "endpoint": "${lambdaFunction.Arn}", 9 | "params": { 10 | "s3Bucket": "", 11 | "s3Region": "${AWS::Region}", 12 | "width": 24, 13 | "height": 3 14 | }, 15 | "title": "Snapshot dashboard to S3" 16 | } 17 | }, 18 | { 19 | "type": "metric", 20 | "width": 12, 21 | "height": 9, 22 | "properties": { 23 | "metrics": [ 24 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Invocations\"', 'Sum', 300)", "id": "lambda", "period": 300, "visible": false, "region": "us-east-1" } ], 25 | [ { "expression": "SORT(lambda, SUM, DESC)", "label": "[sum: ${!SUM}]", "id": "sort", "stat": "Sum", "region": "us-east-1" } ] 26 | ], 27 | "view": "timeSeries", 28 | "stacked": false, 29 | "region": "us-east-1", 30 | "stat": "Sum", 31 | "period": 300, 32 | "title": "Lambda functions: by invocations", 33 | "yAxis": { 34 | "left": { 35 | "label": "Count", 36 | "showUnits": false 37 | } 38 | } 39 | } 40 | }, 41 | { 42 | "type": "metric", 43 | "width": 12, 44 | "height": 9, 45 | "properties": { 46 | "metrics": [ 47 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Duration\"', 'p90', 300)", "id": "lambda", "region": "us-east-1", "visible": false } ], 48 | [ { "expression": "SORT(lambda, AVG, DESC)", "label": "[avg: ${!AVG}ms]", "id": "sort", "region": "us-east-1" } ] 49 | ], 50 | "view": "timeSeries", 51 | "stacked": false, 52 | "region": "us-east-1", 53 | "period": 300, 54 | "title": "Lambda functions: by duration (p90)", 55 | "stat": "Average", 56 | "yAxis": { 57 | "left": { 58 | "label": "Milliseconds", 59 | "showUnits": false 60 | } 61 | } 62 | } 63 | }, 64 | { 65 | "type": "metric", 66 | "width": 12, 67 | "height": 9, 68 | "properties": { 69 | "metrics": [ 70 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Errors\"', 'Sum', 300)", "id": "lambda", "period": 300, "visible": false, "region": "us-east-1" } ], 71 | [ { "expression": "SORT(lambda, SUM, DESC)", "label": "[sum: ${!SUM}]", "id": "sort", "stat": "Sum", "region": "us-east-1" } ] 72 | ], 73 | "view": "timeSeries", 74 | "stacked": false, 75 | "region": "us-east-1", 76 | "stat": "Sum", 77 | "period": 300, 78 | "title": "Lambda functions: by errors", 79 | "yAxis": { 80 | "left": { 81 | "label": "Count", 82 | "showUnits": false 83 | } 84 | } 85 | } 86 | }, 87 | { 88 | "type": "metric", 89 | "width": 12, 90 | "height": 9, 91 | "properties": { 92 | "metrics": [ 93 | [ { "expression": "SEARCH('{AWS/Lambda,FunctionName} MetricName=\"Throttles\"', 'Sum', 300)", "id": "lambda", "period": 300, "visible": false, "region": "us-east-1" } ], 94 | [ { "expression": "SORT(lambda, SUM, DESC)", "label": "[sum: ${!SUM}]", "id": "sort", "stat": "Sum", "region": "us-east-1" } ] 95 | ], 96 | "view": "timeSeries", 97 | "stacked": false, 98 | "region": "us-east-1", 99 | "stat": "Sum", 100 | "period": 300, 101 | "title": "Lambda functions: by throttles", 102 | "yAxis": { 103 | "left": { 104 | "label": "Count", 105 | "showUnits": false 106 | } 107 | } 108 | } 109 | } 110 | ] 111 | } -------------------------------------------------------------------------------- /samples/snapshotDashboardToS3/permissions.yaml: -------------------------------------------------------------------------------- 1 | - PolicyDocument: 2 | Version: 2012-10-17 3 | Statement: 4 | - Action: 5 | - cloudwatch:GetDashboard 6 | - cloudwatch:GetMetricWidgetImage 7 | - s3:PutObject 8 | Effect: Allow 9 | Resource: 10 | - "*" 11 | PolicyName: cloudwatchS3Put 12 | -------------------------------------------------------------------------------- /samples/snapshotDashboardToS3/snapshotDashboardToS3.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # CloudWatch Custom Widget sample: snapshot CloudWatch metric graphs in dashboard to S3 5 | import base64 6 | import boto3 7 | import datetime 8 | import json 9 | import os 10 | import secrets 11 | 12 | DOCS = """## Store bitmap snapshot of a CloudWatch Dashboard in S3 13 | Stores bitmap snapshots of all metric widgets in the current CloudWatch Dashboard and in a HTML file in S3.""" 14 | CSS = "" 15 | 16 | def load_dashboard(dashboardName): 17 | cloudwatch = boto3.client('cloudwatch') 18 | dashboard = cloudwatch.get_dashboard(DashboardName=dashboardName) 19 | return json.loads(dashboard['DashboardBody']) 20 | 21 | def get_metric_bitmaps(widgets, start, end, unitWidth, unitHeight): 22 | bitmaps = [] 23 | for widget in widgets: 24 | if widget['type'] != 'metric': 25 | continue 26 | 27 | width = widget['width'] if 'width' in widget else 12 28 | height = widget['height'] if 'height' in widget else 12 29 | widgetProps = widget['properties'] 30 | graph = { **widgetProps, 'start': start, 'end': end, 'width': int(width * unitWidth), 'height': int(height * unitHeight) } 31 | params = { 'MetricWidget': json.dumps(graph) } 32 | region = widgetProps['region'] 33 | cloudwatch = boto3.client('cloudwatch', region_name=region) 34 | image = cloudwatch.get_metric_widget_image(**params) 35 | bitmaps.append(image['MetricWidgetImage']) 36 | 37 | return bitmaps 38 | 39 | def get_snapshot_html(dashboardName, bitmaps): 40 | subject = f"Dashboard snapshot: {dashboardName}" 41 | html = f"

{subject}

" 42 | 43 | for bitmap in bitmaps: 44 | base64_bitmap = base64.b64encode(bitmap).decode('UTF-8') 45 | html += f"""
""" 46 | return html 47 | 48 | def upload_snapshot(s3Bucket, s3Region, path, html): 49 | s3 = boto3.client('s3', region_name=s3Region) 50 | return s3.put_object(Body=html, Bucket=s3Bucket, Key=path, ContentType='text/html') 51 | 52 | def lambda_handler(event, context): 53 | if 'describe' in event: 54 | return DOCS 55 | 56 | s3Bucket = event['s3Bucket'] 57 | s3Region = event['s3Region'] 58 | widgetContext = event['widgetContext'] 59 | dashboardName = widgetContext['dashboardName'] 60 | timeRange = widgetContext['timeRange']['zoom'] if 'zoom' in widgetContext['timeRange'] else widgetContext['timeRange'] 61 | start = datetime.datetime.utcfromtimestamp(timeRange['start']/1000).isoformat() 62 | end = datetime.datetime.utcfromtimestamp(timeRange['end']/1000).isoformat() 63 | width = event['width'] 64 | height = event['height'] 65 | unitWidth = widgetContext['width'] / width 66 | unitHeight = widgetContext['height'] / height 67 | doIt = event['doIt'] if 'doIt' in event else False 68 | msg = "" 69 | if doIt: 70 | if s3Bucket != "": 71 | dashboard = load_dashboard(dashboardName) 72 | widgets = dashboard['widgets'] 73 | bitmaps = get_metric_bitmaps(widgets, start, end, unitWidth, unitHeight) 74 | s3ObjectPath = f"dashboardSnapshots/{dashboardName}_{secrets.token_urlsafe(8)}.html" 75 | s3Path = f"{s3Bucket}/{s3ObjectPath}" 76 | 77 | try: 78 | html = get_snapshot_html(dashboardName, bitmaps) 79 | s3Result = upload_snapshot(s3Bucket, s3Region, s3ObjectPath, html) 80 | msg = f"""
Snapshot uploaded successfully to {s3Path}

Snapshot

{html}""" 81 | except Exception as e: 82 | msg = f"
Snapshot failed to upload to '{s3Path}': {e}
" 83 | else: 84 | msg = "
s3Bucket parameter not set, please set by editing widget and entering an S3 bucket that exists in this account
" 85 | 86 | return f"""{CSS}
Snapshot Dashboard to S3 87 | 88 | {{ "doIt": true, "s3Bucket": "{s3Bucket}", "s3Region": "{s3Region}", "width": {width}, "height": {height} }} 89 |
{msg}""" 90 | -------------------------------------------------------------------------------- /samples/snapshotDashboardToS3/tags: -------------------------------------------------------------------------------- 1 | - Key: cw-custom-widget 2 | Value: describe:readWrite 3 | -------------------------------------------------------------------------------- /scripts/build-assets: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | BIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | source $BIN_DIR/lib/bashFunctions 7 | readArgsIntoVars $* 8 | 9 | ROOT_DIR=$BIN_DIR/.. 10 | SAMPLES_DIR=$ROOT_DIR/samples 11 | BUILD_DIR=$ROOT_DIR/build 12 | CFN_BUILD_DIR=$BUILD_DIR/cfn 13 | 14 | mkdir -p $CFN_BUILD_DIR 15 | 16 | # Go through all the sample code folders 17 | for sampleDir in $SAMPLES_DIR/*/ ; do 18 | echo "$sampleDir" 19 | dashboard=$(cat $sampleDir/dashboard.json | sed 's/^/ /') 20 | permissions=$(cat $sampleDir/permissions.yaml | sed 's/^/ /') 21 | tags=$(cat $sampleDir/tags | sed 's/^/ /') 22 | 23 | # Go through each code file (*.js and *.py supported currently) 24 | codePaths="$sampleDir/*.??" 25 | for codePath in $codePaths; do 26 | code=$(cat $codePath | sed 's/^/ /') 27 | codeFirstLine="${code%%$'\n'*}" 28 | description="${codeFirstLine# *[^ ]* }" 29 | codeFilename=$(basename $codePath) 30 | extension="${codeFilename##*.}" 31 | functionName="${codeFilename%.*}-${extension}" 32 | functionName="$(tr '[:lower:]' '[:upper:]' <<< ${functionName:0:1})${functionName:1}" # capitalize 33 | functionName="customWidget${functionName}" 34 | 35 | if [ $extension == 'js' ]; then 36 | handler="index.handler" 37 | runtime="nodejs22.x" 38 | else 39 | handler="index.lambda_handler" 40 | runtime="python3.12" 41 | fi 42 | 43 | # Create CloudFormation template 44 | cfnPath=$CFN_BUILD_DIR/$functionName.yaml 45 | echo ... creating $cfnPath 46 | cat > $cfnPath < 118 | $dashboard 119 | EOF 120 | done 121 | done 122 | 123 | -------------------------------------------------------------------------------- /scripts/lib/bashFunctions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | function readArgsIntoVars { 6 | for arg in "$@" 7 | do 8 | if [[ "$arg" == --* ]]; then 9 | id=$(echo "$arg" | sed 's/=.*//;s/^[-]*//;s/-/_/g') 10 | val=$(echo "$arg" | sed 's/^[^=]*$/1/;s/^[^=]*=//') 11 | readonly "$id=$val" 12 | fi 13 | done 14 | } 15 | --------------------------------------------------------------------------------