├── NOTICE ├── Dashboard-897ee679-8b4a-4ce5-96c6-5f117fe57a28.zip ├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── Dashboard.json ├── CONTRIBUTING.md ├── README.md ├── LICENSE ├── create_CW_dashboard.py └── lambda_function.py /NOTICE: -------------------------------------------------------------------------------- 1 | MediaLive to MediaPackage CloudWatch Dashboard 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /Dashboard-897ee679-8b4a-4ce5-96c6-5f117fe57a28.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-medialive-mediapackage-cloudwatch-dashboard/HEAD/Dashboard-897ee679-8b4a-4ce5-96c6-5f117fe57a28.zip -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "AWS CloudFormation template to set up MediaLive and MediaPackage Dashboard", 4 | "Parameters" : { 5 | "MediaLiveARN" : { 6 | "Default" : "", 7 | "Description" : "; delimited ARN list of your MediaLiveChannels:", 8 | "Type" : "String" 9 | }, 10 | "DashBoardName" : { 11 | "Default" : "", 12 | "Description" : "Name of your Dashboard:", 13 | "Type" : "String" 14 | } 15 | 16 | }, 17 | "Resources": { 18 | "RunTheLambda": { 19 | "Type": "Custom::LambdaCallout", 20 | "DependsOn": [ 21 | "dashboardsetup4" 22 | ], 23 | "Properties": { 24 | "ServiceToken": { 25 | "Fn::GetAtt": [ 26 | "dashboardsetup4", 27 | "Arn" 28 | ] 29 | }, 30 | "Await": true 31 | } 32 | }, 33 | "dashboardsetup4": { 34 | "Type": "AWS::Lambda::Function", 35 | "Properties": { 36 | "Code": { 37 | "S3Bucket": { "Fn::Sub": "ems-lambdas-${AWS::Region}" }, 38 | "S3Key": "Dashboard-897ee679-8b4a-4ce5-96c6-5f117fe57a28.zip" 39 | }, 40 | "Description": "This is the code that sets up the Dashboard.", 41 | "FunctionName": "dashboardsetup4", 42 | "Handler": "lambda_function.lambda_handler", 43 | "MemorySize": 512, 44 | "Role": { 45 | "Fn::GetAtt": [ 46 | "dashboardrole4", 47 | "Arn" 48 | ] 49 | }, 50 | "Runtime": "python2.7", 51 | "Timeout": 300, 52 | "Environment": { 53 | "Variables": { 54 | "MyArn": { "Ref": "MediaLiveARN" }, 55 | "MyDashB": { "Ref": "DashBoardName" } 56 | } 57 | } 58 | }, 59 | "DependsOn": [ 60 | "dashboardrole4" 61 | ] 62 | }, 63 | "dashboardrole4": { 64 | "Type": "AWS::IAM::Role", 65 | "Properties": { 66 | "RoleName": "dashboardrole4", 67 | "AssumeRolePolicyDocument": { 68 | "Version": "2012-10-17", 69 | "Statement": [ 70 | { 71 | "Effect": "Allow", 72 | "Principal": { 73 | "Service": "lambda.amazonaws.com" 74 | }, 75 | "Action": "sts:AssumeRole" 76 | } 77 | ] 78 | }, 79 | "Path": "/", 80 | "Policies": [ 81 | { 82 | "PolicyName": "DashboardExecutionPolicy", 83 | "PolicyDocument": { 84 | "Version": "2012-10-17", 85 | "Statement": [ 86 | { 87 | "Effect": "Allow", 88 | "Action": [ 89 | "medialive:ListChannels", 90 | "medialive:DescribeChannel" 91 | ], 92 | "Resource": "*" 93 | }, 94 | { 95 | "Effect": "Allow", 96 | "Action": [ 97 | "mediapackage:ListOriginEndpoints", 98 | "mediapackage:ListChannels" 99 | ], 100 | "Resource": "*" 101 | }, 102 | { 103 | "Effect": "Allow", 104 | "Action": [ 105 | "cloudwatch:PutDashboard", 106 | "cloudwatch:ListMetrics" 107 | ], 108 | "Resource": "*" 109 | } 110 | ] 111 | } 112 | } 113 | ] 114 | 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /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](https://github.com/aws-samples/aws-medialive-mediapackage-cloudwatch-dashboard/issues), or [recently closed](https://github.com/aws-samples/aws-medialive-mediapackage-cloudwatch-dashboard/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), 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 *master* 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'](https://github.com/aws-samples/aws-medialive-mediapackage-cloudwatch-dashboard/labels/help%20wanted) 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](https://github.com/aws-samples/aws-medialive-mediapackage-cloudwatch-dashboard/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Automated Way to Create an AWS Media Services-Centric CloudWatch Dashboard 2 | ========================================================================== 3 | 4 | This repository is part of a AWS Media Blog post, called [Automated Way to Create an AWS Media Services-Centric CloudWatch Dashboard](https://aws.amazon.com/blogs/media/automated-way-to-create-an-aws-media-services-centric-cloudwatch-dashboard/) 5 | 6 | This blog post describes two methods for automating the creation of CloudWatch dashboards that present widgets in a consistent, user-friendly layout optimized for media workflows. One method uses a Python script and the other applies an AWS CloudFormation template that utilizes the Python script. If you already have a working environment for the AWS SDK for Python (Boto3), the Python script will be easiest to use. This is also true if you have a large number of dashboards to create. If you don’t use the AWS SDK for Python, then the CloudFormation template will be the easiest solution for you. Both approaches are designed for media workflows that incorporate the AWS Elemental MediaLive and AWS Elemental MediaPackage services and will generate a dashboard with widgets that present key metrics from those services during operation. Please note there are many different ways to achieve the same result programmatically. 7 | 8 | Assumptions 9 | ----------- 10 | 11 | For sake of simplicity, these examples make several assumptions. The assumptions for running the Python script directly are: 12 | 13 | The Python modules boto3 and awscli are installed, and "aws configure" was run to define the access key and secret access key to be used by boto3 to interact with the AWS services. 14 | The user that runs the script has an IAM policy that allows the user to run list/describe commands for MediaLive, MediaPackage, and CloudWatch, and has permission to create the dashboard instance in the user’s account. 15 | Assumptions for both the Python script and the CloudFormation Template are: 16 | 17 | Even though dashboards are not regional, an instance of a given dashboard widget can only present data for a single region. Therefore, this script requires that the MediaLive channels used to create the dashboard all be in the same region. 18 | The MediaLive Channel was started at least once. This is required for the OutputVideoFrameRate CloudWatch Metric to be populated with the names of the outputs of the MediaLive channel. 19 | MediaPackage Channels receiving content from the MediaLive Channel are in the same region as the MediaLive channel. 20 | The CloudWatch Dashboard is created in the same region as the MediaLive Channel 21 | 22 | Execution of the python script 23 | ------------------------------ 24 | 25 | The script has a comprehensive help function that can be accessed by the command line option “-h” or “–help”, e.g. 26 | 27 | ```python create_CW_dashboard.py -h``` 28 | 29 | The script takes as input either a single MediaLive Channel ARN, or a file consisting of a list of MediaLive Channel ARNs, as well as the name of the dashboard. 30 | 31 | ```python create_CW_dashboard.py --arn --name ``` 32 | 33 | or 34 | 35 | ```python create_CW_dashboard.py --list --name ``` 36 | 37 | The MediaLive ARN list file should be a text file that has a single MediaLive Channel ARN per line, e.g. 38 | 39 | arn:aws:medialive:us-west-2:0123456789:channel:123456 40 | arn:aws:medialive:us-west-2:0123456789:channel:234567 41 | arn:aws:medialive:us-west-2:0123456789:channel:345678 42 | 43 | The script ignores duplicate entries and empty lines in the list file. 44 | 45 | Execution of the CloudFormation Template 46 | ---------------------------------------- 47 | 48 | The CloudFormation template creates a Lambda function that will then, in turn, create the CloudWatch dashboard. The Lambda runs a version of the Python script mentioned above. Use the CloudFormation console to create a stack instance in the region you want the dashboard to reside. 49 | 50 | ## Structure of the Dashboard 51 | 52 | 53 | The dashboard is broken up into two sections, namely the Packaging & Origin section (i.e. MediaPackage related) and the Encoding section (MediaLive related). 54 | 55 | ### Packaging & Origin Section 56 | 57 | This section consists of six metrics related to the MediaPackage service: 58 | 59 | 1. Ingress Bytes: Number of bytes that the MediaPackage Channel ingests. 60 | 1. Egress Request Bytes: Number of bytes that MediaPackage successfully outputs for each request. 61 | 1. Ingress Response Time: The time that it takes the MediaPackage to process each ingest request. 62 | 1. Egress Request Count: Number of content requests that MediaPackage receives. 63 | 1. Status Codes for 2xx and 4xx: For each MediaPackage Endpoint graphs the sum of all 2xx Status Codes and 4xx Status Codes. 64 | 1. Status Codes for 3xx and 5xx: For each MediaPackage Endpoint graphs the sum of all 3xx Status Codes and 5xx Status Codes. 65 | 66 | ### Encoding Section 67 | The Encoding section of the dashboard is relate to the metrics of the MediaLive channel: 68 | 69 | 1. Input Video Frame Rate: The input video frame rate averaged over the last ten seconds. 70 | 1. Output Video Frame Rate: This metric graphs the output video frame rate of each output in the MediaLive channel, averaged over the last ten seconds. 71 | 1. Network In: A graph of the sum of the input bit rate 72 | 1. Network Out: A graph of the sum of all the outputs of the MediaLive channel 73 | 1. Active Output Renditions: Sum of the number of active outputs of the MediaLive channel 74 | 1. Dropped Frames: The Dropped Frame metric indicates the cumulative number of dropped input frames since the start of the channel. A frame drop indicates that the system is unable to keep up in real time. 75 | 1. Fill Milliseconds: This metric is the number of consecutive milliseconds of “fill” frames (e.g. repeated frames, black frames and/or slate) inserted by the MediaLive due to the input not being present. Once the input returns it will go back to zero. 76 | 1. SVQ Time: This metric defines the percentage of time, averaged over the last 10 seconds, the encoder has had to reduce quality optimizations to speed up the encode process, in order to keep the MediaLive running in real time. 77 | 78 | 79 | 80 | For more detailed information, please read the blog post. 81 | 82 | 83 | ## License 84 | 85 | This library is licensed under the Apache 2.0 License. 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /create_CW_dashboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import os 5 | import getopt 6 | import boto3 7 | import json 8 | 9 | 10 | version = '0.4' 11 | """ 12 | Notes: 13 | Version 0.1: Initial Release 14 | Version 0.2: Added support of both 'Status Code Range (sum), 2xx4xx' and 'Status Code Range (sum), 3xx5xx' metrics 15 | Added '-l/--list' command line option with which a user can provide a list of MediaLive Channel ARNs for 16 | which the Dashboard will be created. NOTE: All MediaLive Channels in this list must be in the same region 17 | Version 0.3: Added a text widget to contain all the links to the MediaLive/MediaPackage channel consoles of the 18 | channels of this dashboard. 19 | Version 0.4: Switched MediaLive centric widgets to use the MediaLive Channel Name instead of the ARN 20 | Version 0.5: Added support for the new dual channel MediaPackage implementation. 21 | """ 22 | 23 | dashboard_template = """{ 24 | "widgets": [ 25 | { 26 | "type": "text", 27 | "x": 0, 28 | "y": 0, 29 | "width": 24, 30 | "height": 1, 31 | "properties": { 32 | "markdown": "# MediaPackage Section Title" 33 | } 34 | }, 35 | { 36 | "type": "text", 37 | "x": 0, 38 | "y": 1, 39 | "width": 12, 40 | "height": 1, 41 | "properties": { 42 | "markdown": "## Ingress" 43 | } 44 | }, 45 | { 46 | "type": "metric", 47 | "x": 0, 48 | "y": 2, 49 | "width": 12, 50 | "height": 9, 51 | "properties": { 52 | "title": "Ingress Bytes (sum)", 53 | "view": "timeSeries", 54 | "stacked": false, 55 | "metrics": [], 56 | "region": "", 57 | "stat": "Sum", 58 | "period": 60, 59 | "yAxis": { 60 | "left": { 61 | "min": 0 62 | }, 63 | "right": { 64 | "min": 0 65 | } 66 | } 67 | } 68 | }, 69 | { 70 | "type": "metric", 71 | "x": 0, 72 | "y": 11, 73 | "width": 12, 74 | "height": 9, 75 | "properties": { 76 | "title": "Ingress Response Times (avg)", 77 | "view": "timeSeries", 78 | "stacked": false, 79 | "metrics": [], 80 | "region": "", 81 | "stat": "Average", 82 | "period": 60 83 | } 84 | }, 85 | { 86 | "type": "metric", 87 | "x": 12, 88 | "y": 20, 89 | "width": 12, 90 | "height": 12, 91 | "properties": { 92 | "title": "Status Code Range (sum), 3xx,5xx", 93 | "view": "timeSeries", 94 | "stacked": false, 95 | "metrics": [], 96 | "region": "", 97 | "stat": "Sum", 98 | "period": 60, 99 | "yAxis": { 100 | "left": { 101 | "min": 0 102 | }, 103 | "right": { 104 | "min": 0 105 | } 106 | } 107 | } 108 | }, 109 | { 110 | "type": "text", 111 | "x": 12, 112 | "y": 1, 113 | "width": 12, 114 | "height": 1, 115 | "properties": { 116 | "markdown": "## Egress" 117 | } 118 | }, 119 | { 120 | "type": "metric", 121 | "x": 12, 122 | "y": 2, 123 | "width": 12, 124 | "height": 9, 125 | "properties": { 126 | "title": "Egress Request Bytes (sum)", 127 | "view": "timeSeries", 128 | "stacked": false, 129 | "metrics": [], 130 | "region": "", 131 | "stat": "Sum", 132 | "period": 60, 133 | "yAxis": { 134 | "left": { 135 | "min": 0 136 | }, 137 | "right": { 138 | "min": 0 139 | } 140 | } 141 | } 142 | }, 143 | { 144 | "type": "metric", 145 | "x": 12, 146 | "y": 11, 147 | "width": 12, 148 | "height": 9, 149 | "properties": { 150 | "title": "Egress Request Count (sum)", 151 | "view": "timeSeries", 152 | "stacked": false, 153 | "metrics": [], 154 | "region": "", 155 | "stat": "Sum", 156 | "period": 60, 157 | "yAxis": { 158 | "left": { 159 | "min": 0 160 | } 161 | } 162 | } 163 | }, 164 | { 165 | "type": "metric", 166 | "x": 0, 167 | "y": 20, 168 | "width": 12, 169 | "height": 12, 170 | "properties": { 171 | "title": "Status Code Range (sum), 2xx,4xx", 172 | "view": "timeSeries", 173 | "stacked": false, 174 | "metrics": [], 175 | "region": "", 176 | "stat": "Sum", 177 | "period": 60, 178 | "yAxis": { 179 | "left": { 180 | "min": 0 181 | }, 182 | "right": { 183 | "min": 0 184 | } 185 | } 186 | } 187 | }, 188 | { 189 | "type": "text", 190 | "x": 0, 191 | "y": 32, 192 | "width": 24, 193 | "height": 1, 194 | "properties": { 195 | "markdown": "# MediaLive Section Title" 196 | } 197 | }, 198 | { 199 | "type": "text", 200 | "x": 0, 201 | "y": 33, 202 | "width": 12, 203 | "height": 1, 204 | "properties": { 205 | "markdown": "## Input" 206 | } 207 | }, 208 | { 209 | "type": "metric", 210 | "x": 0, 211 | "y": 34, 212 | "width": 12, 213 | "height": 9, 214 | "properties": { 215 | "title": "Input Video Frame Rate (avg)", 216 | "view": "timeSeries", 217 | "stacked": false, 218 | "metrics": [], 219 | "region": "", 220 | "stat": "Average", 221 | "period": 60, 222 | "annotations": { 223 | "horizontal": [ 224 | { 225 | "label": "30 frames per second", 226 | "value": 30 227 | }, 228 | { 229 | "label": "30 frames per second", 230 | "value": 30, 231 | "yAxis": "right" 232 | }, 233 | { 234 | "label": "60 frames per second", 235 | "value": 60 236 | }, 237 | { 238 | "label": "60 frames per second", 239 | "value": 60, 240 | "yAxis": "right" 241 | } 242 | ] 243 | }, 244 | "yAxis": { 245 | "left": { 246 | "min": 0 247 | }, 248 | "right": { 249 | "min": 0 250 | } 251 | } 252 | } 253 | }, 254 | { 255 | "type": "metric", 256 | "x": 0, 257 | "y": 43, 258 | "width": 12, 259 | "height": 9, 260 | "properties": { 261 | "title": "Network In (sum)", 262 | "view": "timeSeries", 263 | "stacked": false, 264 | "metrics": [], 265 | "region": "", 266 | "stat": "Sum", 267 | "period": 60, 268 | "yAxis": { 269 | "left": { 270 | "min": 0 271 | }, 272 | "right": { 273 | "min": 0 274 | } 275 | } 276 | } 277 | }, 278 | { 279 | "type": "metric", 280 | "x": 0, 281 | "y": 52, 282 | "width": 12, 283 | "height": 6, 284 | "properties": { 285 | "title": "Dropped Frames (sum)", 286 | "view": "timeSeries", 287 | "stacked": false, 288 | "metrics": [], 289 | "region": "", 290 | "stat": "Sum", 291 | "period": 60, 292 | "yAxis": { 293 | "left": { 294 | "min": 0 295 | }, 296 | "right": { 297 | "min": 0 298 | } 299 | }, 300 | "legend": {"position": "bottom"} 301 | } 302 | }, 303 | { 304 | "type": "metric", 305 | "x": 0, 306 | "y": 58, 307 | "width": 12, 308 | "height": 6, 309 | "properties": { 310 | "title": "Fill Milliseconds (sum)", 311 | "view": "timeSeries", 312 | "stacked": false, 313 | "metrics": [], 314 | "region": "", 315 | "stat": "Sum", 316 | "period": 60, 317 | "legend": {"position": "bottom"} 318 | } 319 | }, 320 | { 321 | "type": "metric", 322 | "x": 0, 323 | "y": 64, 324 | "width": 12, 325 | "height": 6, 326 | "properties": { 327 | "title": "SVQ Time (percentage)", 328 | "view": "timeSeries", 329 | "stacked": false, 330 | "metrics": [], 331 | "region": "", 332 | "period": 300, 333 | "stat": "Average", 334 | "legend": {"position": "bottom"} 335 | } 336 | }, 337 | { 338 | "type": "text", 339 | "x": 12, 340 | "y": 33, 341 | "width": 12, 342 | "height": 1, 343 | "properties": { 344 | "markdown": "## Output" 345 | } 346 | }, 347 | { 348 | "type": "metric", 349 | "x": 12, 350 | "y": 34, 351 | "width": 12, 352 | "height": 9, 353 | "properties": { 354 | "title": "Output Video Frame Rate (avg)", 355 | "view": "timeSeries", 356 | "stacked": false, 357 | "metrics": [], 358 | "region": "", 359 | "stat": "Average", 360 | "period": 60, 361 | "annotations": { 362 | "horizontal": [ 363 | { 364 | "label": "30 frames per second", 365 | "value": 30 366 | }, 367 | { 368 | "label": "30 frames per second", 369 | "value": 30, 370 | "yAxis": "right" 371 | }, 372 | { 373 | "label": "60 frames per second", 374 | "value": 60 375 | }, 376 | { 377 | "label": "60 frames per second", 378 | "value": 60, 379 | "yAxis": "right" 380 | } 381 | ] 382 | }, 383 | "yAxis": { 384 | "left": { 385 | "min": 0 386 | }, 387 | "right": { 388 | "min": 0 389 | } 390 | } 391 | } 392 | }, 393 | { 394 | "type": "metric", 395 | "x": 12, 396 | "y": 43, 397 | "width": 12, 398 | "height": 9, 399 | "properties": { 400 | "title": "Network Out (sum)", 401 | "view": "timeSeries", 402 | "stacked": false, 403 | "metrics": [], 404 | "region": "", 405 | "stat": "Sum", 406 | "period": 60, 407 | "yAxis": { 408 | "left": { 409 | "min": 0 410 | }, 411 | "right": { 412 | "min": 0 413 | } 414 | } 415 | } 416 | }, 417 | { 418 | "type": "metric", 419 | "x": 12, 420 | "y": 52, 421 | "width": 12, 422 | "height": 12, 423 | "properties": { 424 | "title": "Active Output Renditions (avg)", 425 | "view": "timeSeries", 426 | "stacked": true, 427 | "metrics": [], 428 | "region": "", 429 | "stat": "Average", 430 | "period": 60, 431 | "yAxis": { 432 | "left": { 433 | "min": 0 434 | } 435 | } 436 | } 437 | }, 438 | { 439 | "type": "text", 440 | "x": 12, 441 | "y": 64, 442 | "width": 12, 443 | "height": 6, 444 | "properties": { 445 | "markdown": "# Console Links for all channels\nHold down the Control (Ctrl) key when selecting a link 446 | to open the console in a new tab within the browser. \n\n" 447 | } 448 | } 449 | ] 450 | }""" 451 | 452 | 453 | # General functions 454 | def usage(app_name): 455 | """Function that prints out a detailed help page for the script""" 456 | global version 457 | print '\npython {0} -a MediaLive_ARN -n Dashboard_Name [Optional parameters]\n'.format(app_name) 458 | print 'Version:', version 459 | print '\nThis script creates a CloudWatch Dashboard for a MediaLive/MediaPackage workflow.' 460 | print "It uses the MediaLive Channel Arn as input and determines the MediaPackage instances from the " 461 | print "MediaLive channel configuration. It then creates the CloudWatch Dashboard that contains info on the" 462 | print "MediaLive channel, the two MediaPackage channels, and all of the MediaPackage endpoints." 463 | print "\nRequired parameters:" 464 | print "-a, --arn: MediaLive Channel ARN" 465 | print "-n, --name: Name for the CloudWatch Dashboard. " 466 | print "" 467 | print "Optional parameters" 468 | print "-l, --list: Filename of a file that contains a list of MediaLive Channel ARNs, 1 ARN per line. " 469 | print " All MediaLive channels and their corresponding MediaPackage channels will be included in " 470 | print " the CloudWatch Dashboard." 471 | print " Note: This parameter is ignored if a channel ARN is provided via the '-a/--arn' option" 472 | print " Note: All ARNs in the list must be for channels in the same region. All ARNs not in the same" 473 | print " region as the first ARN in the list will be ignored." 474 | print '-h, --help: Print this help and exit.' 475 | print "" 476 | print 'Examples:' 477 | print "" 478 | print 'Using MediaLive ARN arn:aws:medialive:us-west-2:0123456789:channel:123456 and create a CloudWatch ' \ 479 | 'Dashboard called "My TV Dashboard"' 480 | print 'python {0} -a arn:aws:medialive:us-west-2:0123456789:channel:123456 ' \ 481 | '-n "My TV Dashboard" '.format(app_name) 482 | print "" 483 | print 'Using the MediaLive Channel ARN list defined in the text file "My EML arns.txt" create a CloudWatch' \ 484 | 'Dashboard called "Primary Bouquet".' 485 | print 'python {0} -l "My EML arns.txt" -n "Primary Bouquet"\n'.format(app_name) 486 | 487 | 488 | def print_mini_help(app_name): 489 | """Print statement showing how to use the '-h/--help' option to get help on proper usage of the script""" 490 | print "\nExecute the script with either '-h' or '--help' to obtain detailed help on how to run the script:" 491 | print 'python {0} -h'.format(app_name) 492 | print "or" 493 | print 'python {0} --help\n'.format(app_name) 494 | 495 | 496 | def is_valid_medialive_channel_arn(mlive_channel_arn): 497 | """Determine if the ARN provided is a valid / complete MediaLive Channel ARN""" 498 | if mlive_channel_arn.startswith("arn:aws:medialive:") and "channel" in mlive_channel_arn: 499 | return True 500 | else: 501 | return False 502 | 503 | 504 | def extract_medialive_region(ml_channel_arn): 505 | """"Given a MediaLive Channel Arn determine the region the channel is in.""" 506 | region = None 507 | # arn:aws:medialive:us-west-2:0123456789:channel:123456 508 | if is_valid_medialive_channel_arn(ml_channel_arn): 509 | arn_parts = ml_channel_arn.split(":") 510 | if len(arn_parts) == 7: 511 | region = arn_parts[3] 512 | return region 513 | 514 | 515 | def extract_medialive_channel_id(ml_channel_arn): 516 | """Given a MediaLive Channel ARN, return the MediaLive Channel ID""" 517 | ml_channel_id = None 518 | if is_valid_medialive_channel_arn(ml_channel_arn): 519 | ml_channel_id = ml_channel_arn.strip().split(":")[-1] 520 | return ml_channel_id 521 | 522 | 523 | def load_eml_arn_list(ml_list_file): 524 | """Load the MediaLive Channel ARNs defined in "ml_list_file" and return as a list. 525 | All MediaLive Channels must be in the same region to be eligble for inclusion into a CloudWatch Dashboard metric 526 | widget. Therefore only Channel ARNs in the same region as the first Channel ARN in the list will be returned. 527 | Additionally, any duplicate ARNs will be discarded as well.""" 528 | first_region = None 529 | is_first_arn = True 530 | result = [] 531 | try: 532 | with open(ml_list_file, "rt") as in_file: 533 | for line in in_file: 534 | line = line.strip() 535 | if is_valid_medialive_channel_arn(line): 536 | current_region = extract_medialive_region(line) 537 | if is_first_arn: 538 | first_region = current_region 539 | is_first_arn = False 540 | if current_region == first_region: 541 | if line not in result: 542 | result.append(line) 543 | else: 544 | print "Skipping duplicate MediaLive ARN '{0}', since it already exists in the " \ 545 | "list".format(line) 546 | else: 547 | print "Ignoring MediaLive ARN '{0}', since it's not in the same region as the first " \ 548 | "ARN in the list.".format(line) 549 | else: 550 | if line is not "": 551 | print "'{0}' is not a valid MediaLive Channel ARN".format(line) 552 | return result 553 | except Exception, e: 554 | print "Error: Processing EML Channel ARN List file '{0}'\n{1}".format(ml_list_file, e.message) 555 | 556 | 557 | # MediaLive related functions 558 | def create_medialive_client_instance(ml_region): 559 | """Create a MediaLive Client Instance for the region specified in "ml_region". """ 560 | try: 561 | medialive = boto3.client('medialive', region_name=ml_region) 562 | return medialive 563 | except Exception, e: 564 | print "Error: Creating a MediaLive Client instance:\n '{0}'".format(e.message) 565 | exit(-1) 566 | 567 | 568 | def extract_medialive_channel_info(ml_client, ml_channel_id): 569 | """Perform a list-channels query against all MediaLive channel in the region specified by the 570 | MediaLive Channel ID and retrieve the MediaLive Channel Name, and MediaPackage Channel ARNs 571 | Returns: MediaLive_Channel_Name & a list of MediaPackage channels""" 572 | mediapackage_channel_list = [] 573 | channel_name = None 574 | try: 575 | response = ml_client.describe_channel( 576 | ChannelId=ml_channel_id 577 | ) 578 | channel_name = str(response["Name"]) 579 | destinations = response["Destinations"] 580 | for destination in destinations: 581 | for output in destination["Settings"]: 582 | url = str(output["Url"]) 583 | if "mediapackage" in url: 584 | mediapackage_channel_list.append(url) 585 | except Exception, e: 586 | print "Error:", e.message 587 | return channel_name, mediapackage_channel_list 588 | 589 | 590 | def extract_medialive_outputgroup_names(ml_client, ml_channel_id): 591 | """given the MediaLive Channel ID "ml_channel_id" retrieve a list of all Output Group Names defined in the 592 | channel configuration.""" 593 | mp_outputgroup_names = [] 594 | try: 595 | channel_response = ml_client.describe_channel(ChannelId=ml_channel_id) 596 | outputgroups = channel_response["EncoderSettings"]["OutputGroups"] 597 | for outputgroup in outputgroups: 598 | groupname = str(outputgroup["Name"]) 599 | mp_outputgroup_names.append(groupname) 600 | return mp_outputgroup_names 601 | except Exception, e: 602 | print "Error: Unable to perform the describe-channel() query for the MediaLive Channel", ml_channel_id 603 | print "Error message:", e 604 | 605 | 606 | # MediaPackage related functions 607 | def create_mediapackage_client_instance(mp_region): 608 | """Create a MediaPackage Client Instance for the region specified in "mp_region". """ 609 | try: 610 | mediapackage = boto3.client('mediapackage', region_name=mp_region) 611 | return mediapackage 612 | except Exception, e: 613 | print "Error: Creating a MediaPackage Client instance '{0}'".format(e.message) 614 | 615 | 616 | def extract_mediapackage_channel_names(mp_client, mediapackage_url_list, ml_region): 617 | """Using the list-channels query, in the region "ml_region", find the MediaPackage Channel Id for the MediaPackage 618 | Channels defined in "mediapackage_url_list".""" 619 | mp_uids = [] 620 | mp_channel_names = [] 621 | for mediapackage_url in mediapackage_url_list: 622 | if "mediapackage." + ml_region in mediapackage_url: 623 | if "/v1/" in mediapackage_url: 624 | url_parts = mediapackage_url.split("/") 625 | if len(url_parts) == 7: 626 | emp_ch_id = url_parts[5] 627 | mp_uids.append(emp_ch_id) 628 | elif "/v2/" in mediapackage_url: 629 | url_parts = mediapackage_url.split("/") 630 | if len(url_parts) == 8: 631 | emp_ch_id = url_parts[5] 632 | if emp_ch_id not in mp_uids: 633 | mp_uids.append(emp_ch_id) 634 | if len(mp_uids) > 0: 635 | response = mp_client.list_channels() 636 | for channel in response["Channels"]: 637 | channel_arn = channel["Arn"] 638 | channel_uid = channel_arn.split("/")[-1] 639 | if channel_uid in mp_uids: 640 | mp_channel_names.append(channel["Id"]) 641 | return mp_channel_names 642 | 643 | 644 | def extract_mediapackage_endpoints(mp_client, mp_channel_id_list): 645 | """Using the list_origin_endpoints query, find all the MediaPackage endpoints for the MediaPackage 646 | channels defined in "mediapackage_channel_id_list" """ 647 | emp_endpoint_list = {} 648 | for channel in mp_channel_id_list: 649 | emp_endpoint_list[str(channel)] = [] 650 | response = mp_client.list_origin_endpoints() 651 | for endpoint in response['OriginEndpoints']: 652 | if str(endpoint["ChannelId"]) in mp_channel_id_list: 653 | emp_endpoint_list[str(endpoint["ChannelId"])].append(str(endpoint['Id'])) 654 | return emp_endpoint_list 655 | 656 | 657 | # CloudWatch related functions 658 | def create_cloudwatch_client_instance(): 659 | """Create a CloudWatch Client Instance. """ 660 | try: 661 | cloudwatch = boto3.client('cloudwatch') 662 | return cloudwatch 663 | except Exception, e: 664 | print "Error: Creating a CloudWatch Client instance:\n '{0}'".format(e.message) 665 | exit(-1) 666 | 667 | 668 | def extract_cw_metrics_output_names(cw_client, ml_channel_id): 669 | """Retrieve a list of MediaLive OutputNames for all Outputs defined in the OutputVideoFrameRate CloudWatch Metric 670 | of the MediaLive defined by the MediaLive Channel ID "ml_channel_id" """ 671 | output_name_list = [] 672 | try: 673 | paginator = cw_client.get_paginator('list_metrics') 674 | for response in paginator.paginate(Dimensions=[{'Name': 'ChannelId', 'Value': ml_channel_id}, 675 | {'Name': 'OutputName'}, 676 | {'Name': 'Pipeline'}], 677 | MetricName='OutputVideoFrameRate', 678 | Namespace='MediaLive'): 679 | if len(response["Metrics"]) > 0: 680 | for metric in response["Metrics"]: 681 | entry = {} 682 | dimensions = metric["Dimensions"] 683 | for dimension in dimensions: 684 | if dimension["Name"] == "OutputName": 685 | entry["OutputName"] = dimension["Value"] 686 | elif dimension["Name"] == "ChannelId": 687 | entry["ChannelId"] = dimension["Value"] 688 | elif dimension["Name"] == "Pipeline": 689 | entry["Pipeline"] = dimension["Value"] 690 | output_name_list.append(entry) 691 | return output_name_list 692 | except Exception, e: 693 | print "Error while retrieving CloudWatch OutputName information", e.message 694 | 695 | 696 | def create_cloudwatch_dashboard(cw_client, cw_dashboard_name, cw_dashboard_body): 697 | """Use put_dashboard to create a new CloudWatch Dashboard named "cw_dashboard_name" that consists of the definition 698 | as defined in "cw_dashboard_body" """ 699 | try: 700 | cw_dashboard_name = cw_dashboard_name.replace(" ", "-") 701 | response = cw_client.put_dashboard( 702 | DashboardName=cw_dashboard_name, 703 | DashboardBody=cw_dashboard_body 704 | ) 705 | result_code = response["ResponseMetadata"]["HTTPStatusCode"] 706 | if result_code == 200: 707 | print "Successfully created Dashboard '{0}'".format(cw_dashboard_name) 708 | else: 709 | print "HTTP Status Code:", result_code 710 | except Exception, e: 711 | print "Error while trying to create new CloudWatch Dashboard:\n", e.message 712 | exit(-5) 713 | 714 | 715 | # CloudWatch Dashboard metrics related 716 | def update_ingress_bytes_metric(mp_channel_names): 717 | """Update the metrics of the "Ingress Bytes (sum)" dashboard widget """ 718 | results = [] 719 | for mp_name in mp_channel_names: 720 | entry = ["AWS/MediaPackage", "IngressBytes", "Channel", mp_name] 721 | results.append(entry) 722 | return results 723 | 724 | 725 | def update_ingress_resp_times_metric(mp_channel_names): 726 | """Update the metrics of the "Ingress Response Times (avg)" dashboard widget""" 727 | results = [] 728 | for mp_name in mp_channel_names: 729 | entry = ["AWS/MediaPackage", "IngressResponseTime", "Channel", mp_name] 730 | results.append(entry) 731 | return results 732 | 733 | 734 | def update_egress_req_bytes_metric(mp_endpoint_names): 735 | """Update the metrics of the "Egress Request Bytes (sum)" dashboard widget""" 736 | results = [] 737 | for mp_name in mp_endpoint_names: 738 | endpoints = mp_endpoint_names[mp_name] 739 | for endpoint in endpoints: 740 | entry = ["AWS/MediaPackage", "EgressBytes", "Channel", mp_name, "OriginEndpoint", endpoint] 741 | results.append(entry) 742 | return results 743 | 744 | 745 | def update_egress_req_count_metric(mp_endpoint_names): 746 | """Update the metrics of the "Egress Request Count (sum)" dashboard widget""" 747 | results = [] 748 | for mp_name in mp_endpoint_names: 749 | endpoints = mp_endpoint_names[mp_name] 750 | for endpoint in endpoints: 751 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint] 752 | results.append(entry) 753 | return results 754 | 755 | 756 | def update_status_code_range_2xx4xx_metric(mp_endpoint_names): 757 | """Update the metrics of the "Status Code Range (sum)" dashboard widget""" 758 | results = [] 759 | for mp_name in mp_endpoint_names: 760 | endpoints = mp_endpoint_names[mp_name] 761 | for endpoint in endpoints: 762 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint, 763 | "StatusCodeRange", "2xx"] 764 | results.append(entry) 765 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint, 766 | "StatusCodeRange", "4xx", {"yAxis": "right"}] 767 | results.append(entry) 768 | return results 769 | 770 | 771 | def update_status_code_range_3xx5xx_metric(mp_endpoint_names): 772 | """Update the metrics of the "Status Code Range (sum)" dashboard widget""" 773 | results = [] 774 | for mp_name in mp_endpoint_names: 775 | endpoints = mp_endpoint_names[mp_name] 776 | for endpoint in endpoints: 777 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint, 778 | "StatusCodeRange", "3xx"] 779 | results.append(entry) 780 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint, 781 | "StatusCodeRange", "5xx", {"yAxis": "right"}] 782 | results.append(entry) 783 | return results 784 | 785 | 786 | def update_input_video_frame_rate_metric(ml_channel_id, ml_channel_name): 787 | """Update the metrics of the "Input Video Frame Rate (avg)" dashboard widget""" 788 | result = [] 789 | entry = ["MediaLive", "InputVideoFrameRate", "ChannelId", ml_channel_id, "Pipeline", "0", 790 | {"label": ml_channel_name + "-0"}] 791 | result.append(entry) 792 | entry = ["MediaLive", "InputVideoFrameRate", "ChannelId", ml_channel_id, "Pipeline", "1", 793 | {"yAxis": "right", "label": ml_channel_name + "-1"}] 794 | result.append(entry) 795 | return result 796 | 797 | 798 | def update_network_in_metric(ml_channel_id, ml_channel_name): 799 | """Update the metrics of the "Network In (sum)" dashboard dashboard widget""" 800 | result = [] 801 | entry = ["MediaLive", "NetworkIn", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 802 | result.append(entry) 803 | entry = ["MediaLive", "NetworkIn", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 804 | "label": ml_channel_name + "-1"}] 805 | result.append(entry) 806 | return result 807 | 808 | 809 | def update_dropped_frames_metric(ml_channel_id, ml_channel_name): 810 | """Update the metrics of the "Dropped Frames (sum)" dashboard dashboard widget""" 811 | result = [] 812 | entry = ["MediaLive", "DroppedFrames", "ChannelId", ml_channel_id, "Pipeline", "0", 813 | {"label": ml_channel_name + "-0"}] 814 | result.append(entry) 815 | entry = ["MediaLive", "DroppedFrames", "ChannelId", ml_channel_id, "Pipeline", "1", 816 | {"yAxis": "right", "label": ml_channel_name + "-1"}] 817 | result.append(entry) 818 | return result 819 | 820 | 821 | def update_fill_msec_metric(ml_channel_id, ml_channel_name): 822 | """Update the metrics of the "Fill Milliseconds (sum)" dashboard dashboard widget""" 823 | result = [] 824 | entry = ["MediaLive", "FillMsec", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 825 | result.append(entry) 826 | entry = ["MediaLive", "FillMsec", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 827 | "label": ml_channel_name + "-1"}] 828 | result.append(entry) 829 | return result 830 | 831 | 832 | def update_svq_time_metric(ml_channel_id, ml_channel_name): 833 | """Update the metrics of the "SVQ Time (percentage)" dashboard dashboard widget""" 834 | result = [] 835 | entry = ["MediaLive", "SvqTime", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 836 | result.append(entry) 837 | entry = ["MediaLive", "SvqTime", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 838 | "label": ml_channel_name + "-1"}] 839 | result.append(entry) 840 | return result 841 | 842 | 843 | def update_output_frame_video_rate_metric(ml_output_names): 844 | """Update the metrics of the "Output Video Frame Rate (avg)" dashboard dashboard widget""" 845 | result = [] 846 | for output in ml_output_names: 847 | if output["Pipeline"] == "0": 848 | entry = ["MediaLive", "OutputVideoFrameRate", "ChannelId", output["ChannelId"], "OutputName", 849 | output["OutputName"], "Pipeline", "0"] 850 | result.append(entry) 851 | elif output["Pipeline"] == "1": 852 | entry = ["MediaLive", "OutputVideoFrameRate", "ChannelId", output["ChannelId"], "OutputName", 853 | output["OutputName"], "Pipeline", "1", {"yAxis": "right"}] 854 | result.append(entry) 855 | return result 856 | 857 | 858 | def update_network_output_metric(ml_channel_id, ml_channel_name): 859 | """Update the metrics of the "Network Out (sum)" dashboard dashboard widget""" 860 | result = [] 861 | entry = ["MediaLive", "NetworkOut", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 862 | result.append(entry) 863 | entry = ["MediaLive", "NetworkOut", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 864 | "label": ml_channel_name + "-1"}] 865 | result.append(entry) 866 | return result 867 | 868 | 869 | def update_active_output_renditions_metric(ml_channel_id, ml_channel_name, ml_channelgroup_names): 870 | """Update the metrics of the "Active Output Renditions (avg)" dashboard dashboard widget""" 871 | results = [] 872 | for groupname in ml_channelgroup_names: 873 | entry = ["MediaLive", "ActiveOutputs", "OutputGroupName", groupname, "ChannelId", ml_channel_id, 874 | "Pipeline", "0", {"label": ml_channel_name + "-0"}] 875 | results.append(entry) 876 | entry = ["MediaLive", "ActiveOutputs", "OutputGroupName", groupname, "ChannelId", ml_channel_id, 877 | "Pipeline", "1", {"yAxis": "right", "label": ml_channel_name + "-1"}] 878 | results.append(entry) 879 | return results 880 | 881 | 882 | def update_console_links_markdown(region, ml_channel_name, ml_channel_id, mp_channel_names): 883 | """Update the markdown of the text widget to show the list of console links for the specific MediaLive and 884 | MediaPackage channels""" 885 | result = "MediaLive: [{0} - {1}](https://{2}.console.aws.amazon.com/medialive/home?region={2}#/" \ 886 | "channels/{1}) MediaPackage: ".format(ml_channel_name, ml_channel_id, region) 887 | mp_name_count = len(mp_channel_names) 888 | index = 1 889 | for mp_name in mp_channel_names: 890 | tmp = " [{0}](https://{1}.console.aws.amazon.com/mediapackage/home?region={1}#/channels" \ 891 | "/{0})".format(mp_name, region) 892 | if index < mp_name_count: 893 | tmp += " , " 894 | index += 1 895 | result += tmp 896 | result += " \n" 897 | return result 898 | 899 | 900 | def process_all_medialive_channels(ml_channel_list, cw_dashboard_name): 901 | """Given a list of all MediaLive Channel ARNs process them one at time and update the CloudWatch Dashboard 902 | template""" 903 | global dashboard_template 904 | eml_region = extract_medialive_region(ml_channel_list[0]) 905 | if eml_region is None: 906 | print "Error: Unable to extract the Region from the MediaLive Channel ARN '{0}'.".format(ml_channel_list[0]) 907 | exit(-3) 908 | eml_client = create_medialive_client_instance(eml_region) 909 | cw_client = create_cloudwatch_client_instance() 910 | emp_client = create_mediapackage_client_instance(eml_region) 911 | 912 | dashboard_json = json.loads(dashboard_template, strict=False) 913 | 914 | # Update the titles of the 2 sections within the dashboard 915 | for widget in dashboard_json["widgets"]: 916 | if widget["type"] == "text": 917 | text_title = widget["properties"]["markdown"] 918 | if "MediaPackage Section Title" in text_title: 919 | widget["properties"]["markdown"] = "# {0}: Packaging and Origination".format(cw_dashboard_name) 920 | if "MediaLive Section Title" in text_title: 921 | widget["properties"]["markdown"] = "# {0}: Encoding".format(cw_dashboard_name) 922 | 923 | for ml_channel_arn in ml_channel_list: 924 | print "Retrieving information from MediaLive channel", ml_channel_arn 925 | eml_channel_id = extract_medialive_channel_id(ml_channel_arn) 926 | if eml_channel_id is None: 927 | print "Error: Verify the MediaLive Channel ARN" 928 | exit(-4) 929 | eml_channel_name, emp_channel_arn_list = extract_medialive_channel_info(eml_client, eml_channel_id) 930 | if eml_channel_name is None or emp_channel_arn_list == []: 931 | print "Error: Retrieving MediaLive Name and\or MediaPackage Destinations from the MediaLive Channel " \ 932 | "configuration." 933 | exit(-3) 934 | print "MediaLive Channel Name: ", eml_channel_name 935 | eml_outputgroup_names = extract_medialive_outputgroup_names(eml_client, eml_channel_id) 936 | eml_output_names = extract_cw_metrics_output_names(cw_client, eml_channel_id) 937 | 938 | print "Retrieving information from the MediaPackage channels" 939 | emp_channel_names = extract_mediapackage_channel_names(emp_client, emp_channel_arn_list, eml_region) 940 | emp_endpoint_names = extract_mediapackage_endpoints(emp_client, emp_channel_names) 941 | 942 | for widget in dashboard_json["widgets"]: 943 | if widget["type"] == "metric": 944 | metrics = [] 945 | metric_title = widget["properties"]["title"] 946 | if "Ingress Bytes (sum)" in metric_title: 947 | metrics += update_ingress_bytes_metric(emp_channel_names) 948 | elif "Ingress Response Times" in metric_title: 949 | metrics += update_ingress_resp_times_metric(emp_channel_names) 950 | elif "Egress Request Bytes (sum)" in metric_title: 951 | metrics += update_egress_req_bytes_metric(emp_endpoint_names) 952 | elif "Egress Request Count (sum)" in metric_title: 953 | metrics += update_egress_req_count_metric(emp_endpoint_names) 954 | elif "Status Code Range (sum), 2xx,4xx" in metric_title: 955 | metrics += update_status_code_range_2xx4xx_metric(emp_endpoint_names) 956 | elif "Status Code Range (sum), 3xx,5xx" in metric_title: 957 | metrics += update_status_code_range_3xx5xx_metric(emp_endpoint_names) 958 | elif "Active Output Renditions (avg)" in metric_title: 959 | metrics += update_active_output_renditions_metric(eml_channel_id, eml_channel_name, 960 | eml_outputgroup_names) 961 | elif "Output Video Frame Rate (avg)" in metric_title: 962 | metrics += update_output_frame_video_rate_metric(eml_output_names) 963 | elif "Input Video Frame Rate (avg)" in metric_title: 964 | metrics += update_input_video_frame_rate_metric(eml_channel_id, eml_channel_name) 965 | elif "Network In (sum)" in metric_title: 966 | metrics += update_network_in_metric(eml_channel_id, eml_channel_name) 967 | elif "Dropped Frames (sum)" in metric_title: 968 | metrics += update_dropped_frames_metric(eml_channel_id, eml_channel_name) 969 | elif "Network Out (sum)" in metric_title: 970 | metrics += update_network_output_metric(eml_channel_id, eml_channel_name) 971 | elif "SVQ Time (percentage)" in metric_title: 972 | metrics += update_svq_time_metric(eml_channel_id, eml_channel_name) 973 | elif "Fill Milliseconds (sum)" in metric_title: 974 | metrics += update_fill_msec_metric(eml_channel_id, eml_channel_name) 975 | else: 976 | print "Unsupported metric '{0}' found in the dashboard template".format(metric_title) 977 | if len(metrics) > 0: 978 | widget["properties"]["metrics"] += metrics 979 | widget["properties"]["region"] = eml_region 980 | if widget["type"] == "text": 981 | markdown = widget["properties"]["markdown"] 982 | if "Console Links for all channels" in markdown: 983 | tmp = update_console_links_markdown(eml_region, eml_channel_name, eml_channel_id, emp_channel_names) 984 | widget["properties"]["markdown"] = markdown + tmp 985 | 986 | dashboard_template = json.dumps(dashboard_json, indent=4, sort_keys=True) 987 | create_cloudwatch_dashboard(cw_client, cw_dashboard_name, dashboard_template) 988 | 989 | 990 | def main(argv=None): 991 | try: 992 | opts, args = getopt.getopt(sys.argv[1:], 'ha:n:l:', ['help', 'arn=', 'name=', 'list=']) 993 | except getopt.GetoptError, err: 994 | print str(err) 995 | usage(sys.argv[0]) 996 | exit(-1) 997 | if len(args) > 0: 998 | print "\nError in the command line options. Please validate that the fields were defined correctly." 999 | print "If the dashboard name consist of more than 1 word or contains spaces then please encapsulate the" 1000 | print 'name in "", e.g. -n "My Dashboard name" ' 1001 | print_mini_help(sys.argv[0]) 1002 | exit() 1003 | if len(sys.argv) == 1: 1004 | usage(sys.argv[0]) 1005 | exit() 1006 | medialive_channel_arn = None 1007 | dashboard_name = None 1008 | eml_list_filename = None 1009 | eml_channel_arn_list = [] 1010 | for opt, arg in opts: 1011 | if opt in ("-h", "--help"): 1012 | usage(sys.argv[0]) 1013 | sys.exit(0) 1014 | elif opt in ("-a", "--arn"): 1015 | medialive_channel_arn = arg 1016 | if not is_valid_medialive_channel_arn(medialive_channel_arn): 1017 | print "Error: Invalid MediaLive Channel ARN '{0}'".format(medialive_channel_arn) 1018 | exit(-2) # without a valid MediaLive Channel ARN there is no reason to continue. 1019 | elif opt in ("-n", "--name"): 1020 | dashboard_name = arg 1021 | elif opt in ("-l", "--list"): 1022 | eml_list_filename = arg 1023 | if not os.path.isfile(eml_list_filename): 1024 | print "Error: File '{0}' doesn't exists or is not in the folder provided.".format(eml_list_filename) 1025 | eml_list_filename = None 1026 | else: 1027 | assert False, "Unhandled option '{0}'".format(opt) 1028 | 1029 | if medialive_channel_arn is not None: 1030 | eml_channel_arn_list = [medialive_channel_arn] 1031 | elif medialive_channel_arn is None and eml_list_filename is not None: 1032 | eml_channel_arn_list = load_eml_arn_list(eml_list_filename) 1033 | arn_count = len(eml_channel_arn_list) 1034 | print "\nFound {0} valid MediaLive Channel ARNs in the file {1}".format(arn_count, eml_list_filename) 1035 | if arn_count == 0: 1036 | print "\nMust provide a MediaLive Channel ARN or a file containing a list of MediaLive Channel ARNs" 1037 | print_mini_help(sys.argv[0]) 1038 | exit(-1) # Must have a MediaLive Channel ARN to continue 1039 | elif medialive_channel_arn is None and len(eml_channel_arn_list) == 0: 1040 | print "\nMust provide a MediaLive Channel ARN or a file containing a list of MediaLive Channel ARNs" 1041 | print_mini_help(sys.argv[0]) 1042 | exit(-1) # Must have a MediaLive Channel ARN to continue 1043 | 1044 | if dashboard_name is None: 1045 | print '\nPlease provide a name for the Dashboard. Note to encapsulate the name in "" if it contains spaces.' 1046 | exit() 1047 | 1048 | process_all_medialive_channels(eml_channel_arn_list, dashboard_name) 1049 | 1050 | 1051 | if __name__ == "__main__": 1052 | sys.exit(main()) 1053 | -------------------------------------------------------------------------------- /lambda_function.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import getopt 4 | import boto3 5 | import json 6 | 7 | 8 | def lambda_handler(event, context): 9 | global dashboard_template 10 | global medialive_channel_arn 11 | 12 | version = '0.5' 13 | """ 14 | Notes: 15 | Version 0.1: Initial Release 16 | Version 0.2: Added support of both 'Status Code Range (sum), 2xx4xx' and 'Status Code Range (sum), 3xx5xx' metrics 17 | Added '-l/--list' command line option with which a user can provide a list of MediaLive Channel ARNs 18 | for which the Dashboard will be created. NOTE: All MediaLive Channels in this list must be in the same 19 | region." 20 | Version 0.3: Added a text widget to contain all the links to the MediaLive/MediaPackage channel consoles of the 21 | channels of this dashboard. 22 | Version 0.4: Switched MediaLive centric widgets to use the MediaLive Channel Name instead of the ARN 23 | Version 0.5: Added support for the new dual channel MediaPackage implementation. 24 | """ 25 | 26 | dashboard_template = """{ 27 | "widgets": [ 28 | { 29 | "type": "text", 30 | "x": 0, 31 | "y": 0, 32 | "width": 24, 33 | "height": 1, 34 | "properties": { 35 | "markdown": "# MediaPackage Section Title" 36 | } 37 | }, 38 | { 39 | "type": "text", 40 | "x": 0, 41 | "y": 1, 42 | "width": 12, 43 | "height": 1, 44 | "properties": { 45 | "markdown": "## Ingress" 46 | } 47 | }, 48 | { 49 | "type": "metric", 50 | "x": 0, 51 | "y": 2, 52 | "width": 12, 53 | "height": 9, 54 | "properties": { 55 | "title": "Ingress Bytes (sum)", 56 | "view": "timeSeries", 57 | "stacked": false, 58 | "metrics": [], 59 | "region": "", 60 | "stat": "Sum", 61 | "period": 60, 62 | "yAxis": { 63 | "left": { 64 | "min": 0 65 | }, 66 | "right": { 67 | "min": 0 68 | } 69 | } 70 | } 71 | }, 72 | { 73 | "type": "metric", 74 | "x": 0, 75 | "y": 11, 76 | "width": 12, 77 | "height": 9, 78 | "properties": { 79 | "title": "Ingress Response Times (avg)", 80 | "view": "timeSeries", 81 | "stacked": false, 82 | "metrics": [], 83 | "region": "", 84 | "stat": "Average", 85 | "period": 60 86 | } 87 | }, 88 | { 89 | "type": "metric", 90 | "x": 12, 91 | "y": 20, 92 | "width": 12, 93 | "height": 12, 94 | "properties": { 95 | "title": "Status Code Range (sum), 3xx,5xx", 96 | "view": "timeSeries", 97 | "stacked": false, 98 | "metrics": [], 99 | "region": "", 100 | "stat": "Sum", 101 | "period": 60, 102 | "yAxis": { 103 | "left": { 104 | "min": 0 105 | }, 106 | "right": { 107 | "min": 0 108 | } 109 | } 110 | } 111 | }, 112 | { 113 | "type": "text", 114 | "x": 12, 115 | "y": 1, 116 | "width": 12, 117 | "height": 1, 118 | "properties": { 119 | "markdown": "## Egress" 120 | } 121 | }, 122 | { 123 | "type": "metric", 124 | "x": 12, 125 | "y": 2, 126 | "width": 12, 127 | "height": 9, 128 | "properties": { 129 | "title": "Egress Request Bytes (sum)", 130 | "view": "timeSeries", 131 | "stacked": false, 132 | "metrics": [], 133 | "region": "", 134 | "stat": "Sum", 135 | "period": 60, 136 | "yAxis": { 137 | "left": { 138 | "min": 0 139 | }, 140 | "right": { 141 | "min": 0 142 | } 143 | } 144 | } 145 | }, 146 | { 147 | "type": "metric", 148 | "x": 12, 149 | "y": 11, 150 | "width": 12, 151 | "height": 9, 152 | "properties": { 153 | "title": "Egress Request Count (sum)", 154 | "view": "timeSeries", 155 | "stacked": false, 156 | "metrics": [], 157 | "region": "", 158 | "stat": "Sum", 159 | "period": 60, 160 | "yAxis": { 161 | "left": { 162 | "min": 0 163 | } 164 | } 165 | } 166 | }, 167 | { 168 | "type": "metric", 169 | "x": 0, 170 | "y": 20, 171 | "width": 12, 172 | "height": 12, 173 | "properties": { 174 | "title": "Status Code Range (sum), 2xx,4xx", 175 | "view": "timeSeries", 176 | "stacked": false, 177 | "metrics": [], 178 | "region": "", 179 | "stat": "Sum", 180 | "period": 60, 181 | "yAxis": { 182 | "left": { 183 | "min": 0 184 | }, 185 | "right": { 186 | "min": 0 187 | } 188 | } 189 | } 190 | }, 191 | { 192 | "type": "text", 193 | "x": 0, 194 | "y": 32, 195 | "width": 24, 196 | "height": 1, 197 | "properties": { 198 | "markdown": "# MediaLive Section Title" 199 | } 200 | }, 201 | { 202 | "type": "text", 203 | "x": 0, 204 | "y": 33, 205 | "width": 12, 206 | "height": 1, 207 | "properties": { 208 | "markdown": "## Input" 209 | } 210 | }, 211 | { 212 | "type": "metric", 213 | "x": 0, 214 | "y": 34, 215 | "width": 12, 216 | "height": 9, 217 | "properties": { 218 | "title": "Input Video Frame Rate (avg)", 219 | "view": "timeSeries", 220 | "stacked": false, 221 | "metrics": [], 222 | "region": "", 223 | "stat": "Average", 224 | "period": 60, 225 | "annotations": { 226 | "horizontal": [ 227 | { 228 | "label": "30 frames per second", 229 | "value": 30 230 | }, 231 | { 232 | "label": "30 frames per second", 233 | "value": 30, 234 | "yAxis": "right" 235 | }, 236 | { 237 | "label": "60 frames per second", 238 | "value": 60 239 | }, 240 | { 241 | "label": "60 frames per second", 242 | "value": 60, 243 | "yAxis": "right" 244 | } 245 | ] 246 | }, 247 | "yAxis": { 248 | "left": { 249 | "min": 0 250 | }, 251 | "right": { 252 | "min": 0 253 | } 254 | } 255 | } 256 | }, 257 | { 258 | "type": "metric", 259 | "x": 0, 260 | "y": 43, 261 | "width": 12, 262 | "height": 9, 263 | "properties": { 264 | "title": "Network In (sum)", 265 | "view": "timeSeries", 266 | "stacked": false, 267 | "metrics": [], 268 | "region": "", 269 | "stat": "Sum", 270 | "period": 60, 271 | "yAxis": { 272 | "left": { 273 | "min": 0 274 | }, 275 | "right": { 276 | "min": 0 277 | } 278 | } 279 | } 280 | }, 281 | { 282 | "type": "metric", 283 | "x": 0, 284 | "y": 52, 285 | "width": 12, 286 | "height": 6, 287 | "properties": { 288 | "title": "Dropped Frames (sum)", 289 | "view": "timeSeries", 290 | "stacked": false, 291 | "metrics": [], 292 | "region": "", 293 | "stat": "Sum", 294 | "period": 60, 295 | "yAxis": { 296 | "left": { 297 | "min": 0 298 | }, 299 | "right": { 300 | "min": 0 301 | } 302 | }, 303 | "legend": {"position": "bottom"} 304 | } 305 | }, 306 | { 307 | "type": "metric", 308 | "x": 0, 309 | "y": 58, 310 | "width": 12, 311 | "height": 6, 312 | "properties": { 313 | "title": "Fill Milliseconds (sum)", 314 | "view": "timeSeries", 315 | "stacked": false, 316 | "metrics": [], 317 | "region": "", 318 | "stat": "Sum", 319 | "period": 60, 320 | "legend": {"position": "bottom"} 321 | } 322 | }, 323 | { 324 | "type": "metric", 325 | "x": 0, 326 | "y": 64, 327 | "width": 12, 328 | "height": 6, 329 | "properties": { 330 | "title": "SVQ Time (percentage)", 331 | "view": "timeSeries", 332 | "stacked": false, 333 | "metrics": [], 334 | "region": "", 335 | "period": 300, 336 | "stat": "Average", 337 | "legend": {"position": "bottom"} 338 | } 339 | }, 340 | { 341 | "type": "text", 342 | "x": 12, 343 | "y": 33, 344 | "width": 12, 345 | "height": 1, 346 | "properties": { 347 | "markdown": "## Output" 348 | } 349 | }, 350 | { 351 | "type": "metric", 352 | "x": 12, 353 | "y": 34, 354 | "width": 12, 355 | "height": 9, 356 | "properties": { 357 | "title": "Output Video Frame Rate (avg)", 358 | "view": "timeSeries", 359 | "stacked": false, 360 | "metrics": [], 361 | "region": "", 362 | "stat": "Average", 363 | "period": 60, 364 | "annotations": { 365 | "horizontal": [ 366 | { 367 | "label": "30 frames per second", 368 | "value": 30 369 | }, 370 | { 371 | "label": "30 frames per second", 372 | "value": 30, 373 | "yAxis": "right" 374 | }, 375 | { 376 | "label": "60 frames per second", 377 | "value": 60 378 | }, 379 | { 380 | "label": "60 frames per second", 381 | "value": 60, 382 | "yAxis": "right" 383 | } 384 | ] 385 | }, 386 | "yAxis": { 387 | "left": { 388 | "min": 0 389 | }, 390 | "right": { 391 | "min": 0 392 | } 393 | } 394 | } 395 | }, 396 | { 397 | "type": "metric", 398 | "x": 12, 399 | "y": 43, 400 | "width": 12, 401 | "height": 9, 402 | "properties": { 403 | "title": "Network Out (sum)", 404 | "view": "timeSeries", 405 | "stacked": false, 406 | "metrics": [], 407 | "region": "", 408 | "stat": "Sum", 409 | "period": 60, 410 | "yAxis": { 411 | "left": { 412 | "min": 0 413 | }, 414 | "right": { 415 | "min": 0 416 | } 417 | } 418 | } 419 | }, 420 | { 421 | "type": "metric", 422 | "x": 12, 423 | "y": 52, 424 | "width": 12, 425 | "height": 12, 426 | "properties": { 427 | "title": "Active Output Renditions (avg)", 428 | "view": "timeSeries", 429 | "stacked": true, 430 | "metrics": [], 431 | "region": "", 432 | "stat": "Average", 433 | "period": 60, 434 | "yAxis": { 435 | "left": { 436 | "min": 0 437 | } 438 | } 439 | } 440 | }, 441 | { 442 | "type": "text", 443 | "x": 12, 444 | "y": 64, 445 | "width": 12, 446 | "height": 6, 447 | "properties": { 448 | "markdown": "# Console Links for all channels\nHold down the Control (Ctrl) key when selecting a link 449 | to open the console in a new tab within the browser. \n\n" 450 | } 451 | } 452 | ] 453 | }""" 454 | 455 | 456 | # General functions 457 | def usage(app_name): 458 | """Function that prints out a detailed help page for the script""" 459 | global version 460 | print '\npython {0} -a MediaLive_ARN -n Dashboard_Name [Optional parameters]\n'.format(app_name) 461 | print 'Version:', version 462 | print '\nThis script creates a CloudWatch Dashboard for a MediaLive/MediaPackage workflow.' 463 | print "It uses the MediaLive Channel Arn as input and determines the MediaPackage instances from the " 464 | print "MediaLive channel configuration. It then creates the CloudWatch Dashboard that contains info on the" 465 | print "MediaLive channel, the two MediaPackage channels, and all of the MediaPackage endpoints." 466 | print "\nRequired parameters:" 467 | print "-a, --arn: MediaLive Channel ARN" 468 | print "-n, --name: Name for the CloudWatch Dashboard. " 469 | print "" 470 | print "Optional parameters" 471 | print "-l, --list: Filename of a file that contains a list of MediaLive Channel ARNs, 1 ARN per line. " 472 | print " All MediaLive channels and their corresponding MediaPackage channels will be included in " 473 | print " the CloudWatch Dashboard." 474 | print " Note: This parameter is ignored if a channel ARN is provided via the '-a/--arn' option" 475 | print " Note: All ARNs in the list must be for channels in the same region. All ARNs not in the same" 476 | print " region as the first ARN in the list will be ignored." 477 | print '-h, --help: Print this help and exit.' 478 | print "" 479 | print 'Examples:' 480 | print "" 481 | print 'Using MediaLive ARN arn:aws:medialive:us-west-2:0123456789:channel:123456 and create a CloudWatch ' \ 482 | 'Dashboard called "My TV Dashboard"' 483 | print 'python {0} -a arn:aws:medialive:us-west-2:0123456789:channel:123456 ' \ 484 | '-n "My TV Dashboard" '.format(app_name) 485 | print "" 486 | print 'Using the MediaLive Channel ARN list defined in the text file "My EML arns.txt" create a CloudWatch' \ 487 | 'Dashboard called "Primary Bouquet".' 488 | print 'python {0} -l "My EML arns.txt" -n "Primary Bouquet"\n'.format(app_name) 489 | 490 | 491 | def print_mini_help(app_name): 492 | """Print statement showing how to use the '-h/--help' option to get help on proper usage of the script""" 493 | print "\nExecute the script with either '-h' or '--help' to obtain detailed help on how to run the script:" 494 | print 'python {0} -h'.format(app_name) 495 | print "or" 496 | print 'python {0} --help\n'.format(app_name) 497 | 498 | 499 | def is_valid_medialive_channel_arn(mlive_channel_arn): 500 | """Determine if the ARN provided is a valid / complete MediaLive Channel ARN""" 501 | if mlive_channel_arn.startswith("arn:aws:medialive:") and "channel" in mlive_channel_arn: 502 | return True 503 | else: 504 | return False 505 | 506 | 507 | def extract_medialive_region(ml_channel_arn): 508 | """"Given a MediaLive Channel Arn determine the region the channel is in.""" 509 | region = None 510 | # arn:aws:medialive:us-west-2:0123456789:channel:123456 511 | if is_valid_medialive_channel_arn(ml_channel_arn): 512 | arn_parts = ml_channel_arn.split(":") 513 | if len(arn_parts) == 7: 514 | region = arn_parts[3] 515 | return region 516 | 517 | 518 | def extract_medialive_channel_id(ml_channel_arn): 519 | """Given a MediaLive Channel ARN, return the MediaLive Channel ID""" 520 | ml_channel_id = None 521 | if is_valid_medialive_channel_arn(ml_channel_arn): 522 | ml_channel_id = ml_channel_arn.strip().split(":")[-1] 523 | return ml_channel_id 524 | 525 | 526 | def load_eml_arn_list(ml_list_file): 527 | """Load the MediaLive Channel ARNs defined in "ml_list_file" and return as a list. 528 | All MediaLive Channels must be in the same region to be eligble for inclusion into a CloudWatch Dashboard metric 529 | widget. Therefore only Channel ARNs in the same region as the first Channel ARN in the list will be returned. 530 | Additionally, any duplicate ARNs will be discarded as well.""" 531 | first_region = None 532 | is_first_arn = True 533 | result = [] 534 | try: 535 | with open(ml_list_file, "rt") as in_file: 536 | for line in in_file: 537 | line = line.strip() 538 | if is_valid_medialive_channel_arn(line): 539 | current_region = extract_medialive_region(line) 540 | if is_first_arn: 541 | first_region = current_region 542 | is_first_arn = False 543 | if current_region == first_region: 544 | if line not in result: 545 | result.append(line) 546 | else: 547 | print "Skipping duplicate MediaLive ARN '{0}', since it already exists in the " \ 548 | "list".format(line) 549 | else: 550 | print "Ignoring MediaLive ARN '{0}', since it's not in the same region as the first " \ 551 | "ARN in the list.".format(line) 552 | else: 553 | if line is not "": 554 | print "'{0}' is not a valid MediaLive Channel ARN".format(line) 555 | return result 556 | except Exception, e: 557 | print "Error: Processing EML Channel ARN List file '{0}'\n{1}".format(ml_list_file, e.message) 558 | 559 | 560 | # MediaLive related functions 561 | def create_medialive_client_instance(ml_region): 562 | """Create a MediaLive Client Instance for the region specified in "ml_region". """ 563 | try: 564 | medialive = boto3.client('medialive', region_name=ml_region) 565 | return medialive 566 | except Exception, e: 567 | print "Error: Creating a MediaLive Client instance:\n '{0}'".format(e.message) 568 | responseData = { 'Reason': 'Error: Creating a MediaLive Client instance'} 569 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 570 | return 1 571 | 572 | def extract_medialive_channel_info(ml_client, ml_channel_id): 573 | """Perform a list-channels query against all MediaLive channel in the region specified by the 574 | MediaLive Channel ID and retrieve the MediaLive Channel Name, and MediaPackage Channel ARNs 575 | Returns: MediaLive_Channel_Name & a list of MediaPackage channels""" 576 | mediapackage_channel_list = [] 577 | channel_name = None 578 | try: 579 | response = ml_client.describe_channel( 580 | ChannelId=ml_channel_id 581 | ) 582 | channel_name = str(response["Name"]) 583 | destinations = response["Destinations"] 584 | for destination in destinations: 585 | for output in destination["Settings"]: 586 | url = str(output["Url"]) 587 | if "mediapackage" in url: 588 | mediapackage_channel_list.append(url) 589 | except Exception, e: 590 | print "Error:", e.message 591 | return channel_name, mediapackage_channel_list 592 | 593 | def extract_medialive_outputgroup_names(ml_client, ml_channel_id): 594 | """given the MediaLive Channel ID "ml_channel_id" retrieve a list of all Output Group Names defined in the 595 | channel configuration.""" 596 | mp_outputgroup_names = [] 597 | try: 598 | channel_response = ml_client.describe_channel(ChannelId=ml_channel_id) 599 | outputgroups = channel_response["EncoderSettings"]["OutputGroups"] 600 | for outputgroup in outputgroups: 601 | groupname = str(outputgroup["Name"]) 602 | mp_outputgroup_names.append(groupname) 603 | return mp_outputgroup_names 604 | except Exception, e: 605 | print "Error: Unable to perform the describe-channel() query for the MediaLive Channel", ml_channel_id 606 | print "Error message:", e 607 | 608 | 609 | # MediaPackage related functions 610 | def create_mediapackage_client_instance(mp_region): 611 | """Create a MediaPackage Client Instance for the region specified in "mp_region". """ 612 | try: 613 | mediapackage = boto3.client('mediapackage', region_name=mp_region) 614 | return mediapackage 615 | except Exception, e: 616 | print "Error: Creating a MediaPackage Client instance '{0}'".format(e.message) 617 | 618 | def extract_mediapackage_channel_names(ml_client, mediapackage_url_list, ml_region): 619 | """Using the list-channels query, in the region "ml_region", find the MediaPackage Channel Id for the MediaPackage 620 | Channels defined in "mediapackage_url_list".""" 621 | mp_uids = [] 622 | mp_channel_names = [] 623 | for mediapackage_url in mediapackage_url_list: 624 | if "mediapackage." + ml_region in mediapackage_url: 625 | if "/v1/" in mediapackage_url: 626 | url_parts = mediapackage_url.split("/") 627 | if len(url_parts) == 7: 628 | emp_ch_id = url_parts[5] 629 | mp_uids.append(emp_ch_id) 630 | elif "/v2/" in mediapackage_url: 631 | url_parts = mediapackage_url.split("/") 632 | if len(url_parts) == 8: 633 | emp_ch_id = url_parts[5] 634 | if emp_ch_id not in mp_uids: 635 | mp_uids.append(emp_ch_id) 636 | if len(mp_uids) > 0: 637 | response = ml_client.list_channels() 638 | for channel in response["Channels"]: 639 | channel_arn = channel["Arn"] 640 | channel_uid = channel_arn.split("/")[-1] 641 | if channel_uid in mp_uids: 642 | mp_channel_names.append(channel["Id"]) 643 | return mp_channel_names 644 | 645 | 646 | def extract_mediapackage_endpoints(mp_client, mp_channel_id_list): 647 | """Using the list_origin_endpoints query, find all the MediaPackage endpoints for the MediaPackage 648 | channels defined in "mediapackage_channel_id_list" """ 649 | emp_endpoint_list = {} 650 | for channel in mp_channel_id_list: 651 | emp_endpoint_list[str(channel)] = [] 652 | response = mp_client.list_origin_endpoints() 653 | for endpoint in response['OriginEndpoints']: 654 | if str(endpoint["ChannelId"]) in mp_channel_id_list: 655 | emp_endpoint_list[str(endpoint["ChannelId"])].append(str(endpoint['Id'])) 656 | return emp_endpoint_list 657 | 658 | 659 | # CloudWatch related functions 660 | def create_cloudwatch_client_instance(): 661 | """Create a CloudWatch Client Instance. """ 662 | try: 663 | cloudwatch = boto3.client('cloudwatch') 664 | return cloudwatch 665 | except Exception, e: 666 | responseData = { 'Reason': 'Error creating CloudWatch Client instance'} 667 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 668 | return 1 669 | 670 | 671 | def extract_cw_metrics_output_names(cw_client, ml_channel_id): 672 | """Retrieve a list of MediaLive OutputNames for all Outputs defined in the OutputVideoFrameRate CloudWatch Metric 673 | of the MediaLive defined by the MediaLive Channel ID "ml_channel_id" """ 674 | output_name_list = [] 675 | try: 676 | paginator = cw_client.get_paginator('list_metrics') 677 | for response in paginator.paginate(Dimensions=[{'Name': 'ChannelId', 'Value': ml_channel_id}, 678 | {'Name': 'OutputName'}, 679 | {'Name': 'Pipeline'}], 680 | MetricName='OutputVideoFrameRate', 681 | Namespace='MediaLive'): 682 | if len(response["Metrics"]) > 0: 683 | for metric in response["Metrics"]: 684 | entry = {} 685 | dimensions = metric["Dimensions"] 686 | for dimension in dimensions: 687 | if dimension["Name"] == "OutputName": 688 | entry["OutputName"] = dimension["Value"] 689 | elif dimension["Name"] == "ChannelId": 690 | entry["ChannelId"] = dimension["Value"] 691 | elif dimension["Name"] == "Pipeline": 692 | entry["Pipeline"] = dimension["Value"] 693 | output_name_list.append(entry) 694 | return output_name_list 695 | except Exception, e: 696 | print "Error while retrieving CloudWatch OutputName information", e.message 697 | 698 | 699 | def create_cloudwatch_dashboard(cw_client, cw_dashboard_name, cw_dashboard_body): 700 | """Use put_dashboard to create a new CloudWatch Dashboard named "cw_dashboard_name" that consists of the definition 701 | as defined in "cw_dashboard_body" """ 702 | try: 703 | cw_dashboard_name = cw_dashboard_name.replace(" ", "-") 704 | response = cw_client.put_dashboard( 705 | DashboardName=cw_dashboard_name, 706 | DashboardBody=cw_dashboard_body 707 | ) 708 | result_code = response["ResponseMetadata"]["HTTPStatusCode"] 709 | if result_code == 200: 710 | print "Successfully created Dashboard '{0}'".format(cw_dashboard_name) 711 | else: 712 | print "HTTP Status Code:", result_code 713 | except Exception, e: 714 | responseData = { 'Reason': 'Error while trying to create new CloudWatch Dashboard'} 715 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 716 | return 1 717 | # CloudWatch Dashboard metrics related 718 | def update_ingress_bytes_metric(mp_channel_names): 719 | """Update the metrics of the "Ingress Bytes (sum)" dashboard widget """ 720 | results = [] 721 | for mp_name in mp_channel_names: 722 | entry = ["AWS/MediaPackage", "IngressBytes", "Channel", mp_name] 723 | results.append(entry) 724 | return results 725 | 726 | 727 | def update_ingress_resp_times_metric(mp_channel_names): 728 | """Update the metrics of the "Ingress Response Times (avg)" dashboard widget""" 729 | results = [] 730 | for mp_name in mp_channel_names: 731 | entry = ["AWS/MediaPackage", "IngressResponseTime", "Channel", mp_name] 732 | results.append(entry) 733 | return results 734 | 735 | 736 | def update_egress_req_bytes_metric(mp_endpoint_names): 737 | """Update the metrics of the "Egress Request Bytes (sum)" dashboard widget""" 738 | results = [] 739 | for mp_name in mp_endpoint_names: 740 | endpoints = mp_endpoint_names[mp_name] 741 | for endpoint in endpoints: 742 | entry = ["AWS/MediaPackage", "EgressBytes", "Channel", mp_name, "OriginEndpoint", endpoint] 743 | results.append(entry) 744 | return results 745 | 746 | 747 | def update_egress_req_count_metric(mp_endpoint_names): 748 | """Update the metrics of the "Egress Request Count (sum)" dashboard widget""" 749 | results = [] 750 | for mp_name in mp_endpoint_names: 751 | endpoints = mp_endpoint_names[mp_name] 752 | for endpoint in endpoints: 753 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint] 754 | results.append(entry) 755 | return results 756 | 757 | 758 | def update_status_code_range_2xx4xx_metric(mp_endpoint_names): 759 | """Update the metrics of the "Status Code Range (sum)" dashboard widget""" 760 | results = [] 761 | for mp_name in mp_endpoint_names: 762 | endpoints = mp_endpoint_names[mp_name] 763 | for endpoint in endpoints: 764 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint, 765 | "StatusCodeRange", "2xx"] 766 | results.append(entry) 767 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint, 768 | "StatusCodeRange", "4xx", {"yAxis": "right"}] 769 | results.append(entry) 770 | return results 771 | 772 | 773 | def update_status_code_range_3xx5xx_metric(mp_endpoint_names): 774 | """Update the metrics of the "Status Code Range (sum)" dashboard widget""" 775 | results = [] 776 | for mp_name in mp_endpoint_names: 777 | endpoints = mp_endpoint_names[mp_name] 778 | for endpoint in endpoints: 779 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint, 780 | "StatusCodeRange", "3xx"] 781 | results.append(entry) 782 | entry = ["AWS/MediaPackage", "EgressRequestCount", "Channel", mp_name, "OriginEndpoint", endpoint, 783 | "StatusCodeRange", "5xx", {"yAxis": "right"}] 784 | results.append(entry) 785 | return results 786 | 787 | 788 | def update_input_video_frame_rate_metric(ml_channel_id, ml_channel_name): 789 | """Update the metrics of the "Input Video Frame Rate (avg)" dashboard widget""" 790 | result = [] 791 | entry = ["MediaLive", "InputVideoFrameRate", "ChannelId", ml_channel_id, "Pipeline", "0", 792 | {"label": ml_channel_name + "-0"}] 793 | result.append(entry) 794 | entry = ["MediaLive", "InputVideoFrameRate", "ChannelId", ml_channel_id, "Pipeline", "1", 795 | {"yAxis": "right", "label": ml_channel_name + "-1"}] 796 | result.append(entry) 797 | return result 798 | 799 | 800 | def update_network_in_metric(ml_channel_id, ml_channel_name): 801 | """Update the metrics of the "Network In (sum)" dashboard dashboard widget""" 802 | result = [] 803 | entry = ["MediaLive", "NetworkIn", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 804 | result.append(entry) 805 | entry = ["MediaLive", "NetworkIn", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 806 | "label": ml_channel_name + "-1"}] 807 | result.append(entry) 808 | return result 809 | 810 | 811 | def update_dropped_frames_metric(ml_channel_id, ml_channel_name): 812 | """Update the metrics of the "Dropped Frames (sum)" dashboard dashboard widget""" 813 | result = [] 814 | entry = ["MediaLive", "DroppedFrames", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 815 | result.append(entry) 816 | entry = ["MediaLive", "DroppedFrames", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 817 | "label": ml_channel_name + "-1"}] 818 | result.append(entry) 819 | return result 820 | 821 | 822 | def update_fill_msec_metric(ml_channel_id, ml_channel_name): 823 | """Update the metrics of the "Fill Milliseconds (sum)" dashboard dashboard widget""" 824 | result = [] 825 | entry = ["MediaLive", "FillMsec", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 826 | result.append(entry) 827 | entry = ["MediaLive", "FillMsec", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 828 | "label": ml_channel_name + "-1"}] 829 | result.append(entry) 830 | return result 831 | 832 | 833 | def update_svq_time_metric(ml_channel_id, ml_channel_name): 834 | """Update the metrics of the "SVQ Time (percentage)" dashboard dashboard widget""" 835 | result = [] 836 | entry = ["MediaLive", "SvqTime", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 837 | result.append(entry) 838 | entry = ["MediaLive", "SvqTime", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 839 | "label": ml_channel_name + "-1"}] 840 | result.append(entry) 841 | return result 842 | 843 | 844 | def update_output_frame_video_rate_metric(ml_output_names): 845 | """Update the metrics of the "Output Video Frame Rate (avg)" dashboard dashboard widget""" 846 | result = [] 847 | for output in ml_output_names: 848 | if output["Pipeline"] == "0": 849 | entry = ["MediaLive", "OutputVideoFrameRate", "ChannelId", output["ChannelId"], "OutputName", 850 | output["OutputName"], "Pipeline", "0"] 851 | result.append(entry) 852 | elif output["Pipeline"] == "1": 853 | entry = ["MediaLive", "OutputVideoFrameRate", "ChannelId", output["ChannelId"], "OutputName", 854 | output["OutputName"], "Pipeline", "1", {"yAxis": "right"}] 855 | result.append(entry) 856 | return result 857 | 858 | 859 | def update_network_output_metric(ml_channel_id, ml_channel_name): 860 | """Update the metrics of the "Network Out (sum)" dashboard dashboard widget""" 861 | result = [] 862 | entry = ["MediaLive", "NetworkOut", "ChannelId", ml_channel_id, "Pipeline", "0", {"label": ml_channel_name + "-0"}] 863 | result.append(entry) 864 | entry = ["MediaLive", "NetworkOut", "ChannelId", ml_channel_id, "Pipeline", "1", {"yAxis": "right", 865 | "label": ml_channel_name + "-1"}] 866 | result.append(entry) 867 | return result 868 | 869 | 870 | def update_active_output_renditions_metric(ml_channel_id, ml_channel_name, ml_channelgroup_names): 871 | """Update the metrics of the "Active Output Renditions (avg)" dashboard dashboard widget""" 872 | results = [] 873 | for groupname in ml_channelgroup_names: 874 | entry = ["MediaLive", "ActiveOutputs", "OutputGroupName", groupname, "ChannelId", ml_channel_id, 875 | "Pipeline", "0", {"label": ml_channel_name + "-0"}] 876 | results.append(entry) 877 | entry = ["MediaLive", "ActiveOutputs", "OutputGroupName", groupname, "ChannelId", ml_channel_id, 878 | "Pipeline", "1", {"yAxis": "right", "label": ml_channel_name + "-1"}] 879 | results.append(entry) 880 | return results 881 | 882 | 883 | def update_console_links_markdown(region, ml_channel_name, ml_channel_id, mp_channel_names): 884 | """Update the markdown of the text widget to show the list of console links for the specific MediaLive and 885 | MediaPackage channels""" 886 | result = "MediaLive: [{0} - {1}](https://{2}.console.aws.amazon.com/medialive/home?region={2}#/" \ 887 | "channels/{1}) MediaPackage: ".format(ml_channel_name, ml_channel_id, region) 888 | mp_name_count = len(mp_channel_names) 889 | index = 1 890 | for mp_name in mp_channel_names: 891 | tmp = " [{0}](https://{1}.console.aws.amazon.com/mediapackage/home?region={1}#/channels" \ 892 | "/{0})".format(mp_name, region) 893 | if index < mp_name_count: 894 | tmp += " , " 895 | index += 1 896 | result += tmp 897 | result += " \n" 898 | return result 899 | 900 | 901 | def process_all_medialive_channels(ml_channel_list, cw_dashboard_name): 902 | """Given a list of all MediaLive Channel ARNs process them one at time and update the CloudWatch Dashboard 903 | template""" 904 | global dashboard_template 905 | eml_region = extract_medialive_region(ml_channel_list[0]) 906 | if eml_region is None: 907 | print "Error: Unable to extract the Region from the MediaLive Channel ARN '{0}'.".format(ml_channel_list[0]) 908 | responseData = { 'Reason': 'Error: Unable to extract the Region from the MediaLive Channel ARN'} 909 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 910 | return 1 911 | eml_client = create_medialive_client_instance(eml_region) 912 | cw_client = create_cloudwatch_client_instance() 913 | emp_client = create_mediapackage_client_instance(eml_region) 914 | 915 | dashboard_json = json.loads(dashboard_template, strict=False) 916 | 917 | # Update the titles of the 2 sections within the dashboard 918 | for widget in dashboard_json["widgets"]: 919 | if widget["type"] == "text": 920 | text_title = widget["properties"]["markdown"] 921 | if "MediaPackage Section Title" in text_title: 922 | widget["properties"]["markdown"] = "# {0}: Packaging and Origination".format(cw_dashboard_name) 923 | if "MediaLive Section Title" in text_title: 924 | widget["properties"]["markdown"] = "# {0}: Encoding".format(cw_dashboard_name) 925 | 926 | for ml_channel_arn in ml_channel_list: 927 | print "Retrieving information from MediaLive channel", ml_channel_arn 928 | eml_channel_id = extract_medialive_channel_id(ml_channel_arn) 929 | if eml_channel_id is None: 930 | print "Error: Verify the MediaLive Channel ARN" 931 | responseData = { 'Reason': 'Error: Verify the MediaLive Channel ARN'} 932 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 933 | return 1 934 | eml_channel_name, emp_channel_arn_list = extract_medialive_channel_info(eml_client, eml_channel_id) 935 | if eml_channel_name is None or emp_channel_arn_list == []: 936 | responseData = { 'Reason': 'Error: Retrieving MediaLive Name and\or MediaPackage Destinations from the MediaLive Channel'} 937 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 938 | return 1 939 | print "MediaLive Channel Name: ", eml_channel_name 940 | eml_outputgroup_names = extract_medialive_outputgroup_names(eml_client, eml_channel_id) 941 | eml_output_names = extract_cw_metrics_output_names(cw_client, eml_channel_id) 942 | 943 | print "Retrieving information from the MediaPackage channels" 944 | emp_channel_names = extract_mediapackage_channel_names(emp_client, emp_channel_arn_list, eml_region) 945 | emp_endpoint_names = extract_mediapackage_endpoints(emp_client, emp_channel_names) 946 | for widget in dashboard_json["widgets"]: 947 | if widget["type"] == "metric": 948 | metrics = [] 949 | metric_title = widget["properties"]["title"] 950 | if "Ingress Bytes (sum)" in metric_title: 951 | metrics += update_ingress_bytes_metric(emp_channel_names) 952 | elif "Ingress Response Times" in metric_title: 953 | metrics += update_ingress_resp_times_metric(emp_channel_names) 954 | elif "Egress Request Bytes (sum)" in metric_title: 955 | metrics += update_egress_req_bytes_metric(emp_endpoint_names) 956 | elif "Egress Request Count (sum)" in metric_title: 957 | metrics += update_egress_req_count_metric(emp_endpoint_names) 958 | elif "Status Code Range (sum), 2xx,4xx" in metric_title: 959 | metrics += update_status_code_range_2xx4xx_metric(emp_endpoint_names) 960 | elif "Status Code Range (sum), 3xx,5xx" in metric_title: 961 | metrics += update_status_code_range_3xx5xx_metric(emp_endpoint_names) 962 | elif "Active Output Renditions (avg)" in metric_title: 963 | metrics += update_active_output_renditions_metric(eml_channel_id, eml_channel_name, eml_outputgroup_names) 964 | elif "Output Video Frame Rate (avg)" in metric_title: 965 | metrics += update_output_frame_video_rate_metric(eml_output_names) 966 | elif "Input Video Frame Rate (avg)" in metric_title: 967 | metrics += update_input_video_frame_rate_metric(eml_channel_id, eml_channel_name) 968 | elif "Network In (sum)" in metric_title: 969 | metrics += update_network_in_metric(eml_channel_id, eml_channel_name) 970 | elif "Dropped Frames (sum)" in metric_title: 971 | metrics += update_dropped_frames_metric(eml_channel_id, eml_channel_name) 972 | elif "Network Out (sum)" in metric_title: 973 | metrics += update_network_output_metric(eml_channel_id, eml_channel_name) 974 | elif "SVQ Time (percentage)" in metric_title: 975 | metrics += update_svq_time_metric(eml_channel_id, eml_channel_name) 976 | elif "Fill Milliseconds (sum)" in metric_title: 977 | metrics += update_fill_msec_metric(eml_channel_id, eml_channel_name) 978 | else: 979 | print "Unsupported metric '{0}' found in the dashboard template".format(metric_title) 980 | if len(metrics) > 0: 981 | widget["properties"]["metrics"] += metrics 982 | widget["properties"]["region"] = eml_region 983 | if widget["type"] == "text": 984 | markdown = widget["properties"]["markdown"] 985 | if "Console Links for all channels" in markdown: 986 | tmp = update_console_links_markdown(eml_region, eml_channel_name, eml_channel_id, emp_channel_names) 987 | widget["properties"]["markdown"] = markdown + tmp 988 | 989 | dashboard_template = json.dumps(dashboard_json, indent=4, sort_keys=True) 990 | create_cloudwatch_dashboard(cw_client, cw_dashboard_name, dashboard_template) 991 | 992 | 993 | def main(argv=None): 994 | medialive_channel_arn = os.environ['MyArn'] 995 | dashboard_name = os.environ['MyDashB'] 996 | if dashboard_name is '': 997 | responseData = { 'Reason': 'Please provide a name for the Dashboard'} 998 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 999 | return 1 1000 | eml_list_filename = None 1001 | eml_channel_arn_list = [] 1002 | if medialive_channel_arn is not None: 1003 | medialive_channel_arn = medialive_channel_arn.replace(' ','') 1004 | eml_channel_arn_list = medialive_channel_arn.split(';') 1005 | elif medialive_channel_arn is None and eml_list_filename is not None: 1006 | eml_channel_arn_list = load_eml_arn_list(eml_list_filename) 1007 | arn_count = len(eml_channel_arn_list) 1008 | print "\nFound {0} valid MediaLive Channel ARNs in the file {1}".format(arn_count, eml_list_filename) 1009 | if arn_count == 0: 1010 | responseData = { 'Reason': 'Please provide a MediaLive Channel ARN'} 1011 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 1012 | return 1 # Must have a MediaLive Channel ARN to continue 1013 | elif medialive_channel_arn is None and len(eml_channel_arn_list) == 0: 1014 | responseData = { 'Must provide a MediaLive Channel ARN or a file containing a list of MediaLive Channel ARNs'} 1015 | send(event, context, FAILED, responseData, "CustomResourcePhysicalID") 1016 | return 1 # Must have a MediaLive Channel ARN to continue 1017 | 1018 | 1019 | 1020 | 1021 | process_all_medialive_channels(eml_channel_arn_list, dashboard_name) 1022 | 1023 | 1024 | 1025 | if __name__ == "__main__": 1026 | sys.exit(main()) 1027 | main() 1028 | medialive_channel_arn = os.environ['MyArn'] 1029 | responseData = {} 1030 | send(event, context, SUCCESS, responseData, "CustomResourcePhysicalID") 1031 | return 0 1032 | from botocore.vendored import requests 1033 | 1034 | SUCCESS = "SUCCESS" 1035 | FAILED = "FAILED" 1036 | 1037 | def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): 1038 | responseUrl = event['ResponseURL'] 1039 | print responseUrl 1040 | 1041 | responseBody = {} 1042 | responseBody['Status'] = responseStatus 1043 | responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name 1044 | responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name 1045 | responseBody['StackId'] = event['StackId'] 1046 | responseBody['RequestId'] = event['RequestId'] 1047 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 1048 | responseBody['NoEcho'] = noEcho 1049 | responseBody['Data'] = responseData 1050 | 1051 | json_responseBody = json.dumps(responseBody) 1052 | 1053 | print "Response body:\n" + json_responseBody 1054 | 1055 | headers = { 1056 | 'content-type' : '', 1057 | 'content-length' : str(len(json_responseBody)) 1058 | } 1059 | 1060 | try: 1061 | response = requests.put(responseUrl, 1062 | data=json_responseBody, 1063 | headers=headers) 1064 | print "Status code: " + response.reason 1065 | except Exception as e: 1066 | print "send(..) failed executing requests.put(..): " + str(e) 1067 | --------------------------------------------------------------------------------