├── .babelignore ├── .babelrc ├── .env ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── Makefile ├── README.md ├── bin ├── ls-dynamodb-tables ├── ls-ec2 ├── ls-rds └── slack-cloudwatch-chart ├── circle.yml ├── dist ├── cli.js ├── cloudwatch.js ├── dynamodb.js ├── ec2.js ├── gen-chart.js ├── index.js ├── ls-ec2.js ├── ls-rds.js ├── metrics.js ├── phantom-api.js ├── print-names.js ├── print-stats.js ├── proc-gen-chart.js ├── render.js ├── time.js └── upload.js ├── gulpfile.js ├── index.js ├── lib └── modules.js ├── package.json ├── src ├── cli.js ├── cloudwatch.js ├── dynamodb.js ├── gen-chart.js ├── index.js ├── ls-ec2.js ├── ls-rds.js ├── metrics.js ├── phantom-api.js ├── print-stats.js ├── proc-gen-chart.js ├── render.js ├── time.js └── upload.js └── test ├── dynamodb.js ├── ls-ec2.js ├── metrics.js └── time.js /.babelignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmtk75/aws-cloudwatch-chart-slack/3e8fe816b2a685b70ab8512fd01d098f80e32fbd/.babelignore -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | 4 | "plugins": [ 5 | "transform-flow-strip-types" 6 | ], 7 | 8 | "env": { 9 | "test": { 10 | "plugins": [ 11 | "babel-plugin-espower" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | nvm use 4 2 | PATH=`pwd`/node_modules/.bin:$PATH 3 | #. .e/bin/activate 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/types/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "quotes": [2, "double"], 13 | "strict": [2, "never"], 14 | "babel/generator-star-spacing": 1, 15 | "babel/new-cap": [1, {"capIsNewExceptions": ["List", "Set"]}], 16 | "babel/object-shorthand": 1, 17 | //"babel/arrow-parens": [2, "as-needed"], 18 | "babel/no-await-in-loop": 1 19 | }, 20 | "plugins": [ 21 | "babel" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | .*/dist/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | lib 9 | 10 | [options] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .e 2 | .DS_Store 3 | .*.swp 4 | node_modules 5 | .vagrant 6 | #dist 7 | credentials 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | describe-rds: 2 | aws rds describe-db-instances 3 | 4 | 5 | .e/bin/aws-shell: .e/bin/pip 6 | .e/bin/pip install aws-shell 7 | 8 | .e/bin/pip: 9 | virtualenv .e 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README [![Build Status](https://circleci.com/gh/tmtk75/aws-cloudwatch-chart-slack.png)](https://circleci.com/gh/tmtk75/aws-cloudwatch-chart-slack) 2 | This module is a chart renderer and uploader to Slack. 3 | It's easy to share charts of CloudWatch on Slack. 4 | You can render charts for datapoints of CloudWatch, and can upload chart images to channels of Slack. 5 | 6 | 7 | 8 | ## Getting Started 9 | Tested with node-4.3.1 and npm-2.14.12. 10 | ``` 11 | $ npm install [--no-spin] 12 | ``` 13 | NOTE: v0.1.2 doesn't work with npm-3.x due to [here](https://github.com/tmtk75/aws-cloudwatch-chart-slack/blob/v0.1.2/src/gen-chart.js#L136) 14 | 15 | Set four environment variables. 16 | ``` 17 | export AWS_DEFAULT_REGION=ap-northeast-1 │~ 18 | export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE 19 | export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 20 | export SLACK_API_TOKEN=bbbb-xxxxxxxxxx-yyyyyyyyyy-zzzzzzzzzzz-aaaaaaaaaa 21 | ``` 22 | 23 | Try this, of course change channel name as you have. 24 | ``` 25 | SLACK_CHANNEL_NAME=#api-test ./bin/slack-cloudwatch-chart 26 | ``` 27 | 28 | A few seconds later, You can see a chart on the channel. 29 | 30 | 31 | ## Hubot Integration 32 | Please add the below snippet. 33 | ```coffee-script 34 | chart = require "aws-cloudwatch-chart-slack" 35 | module.exports = (robot) -> 36 | robot.respond /cloudwatch (.+)/i, (msg) -> 37 | [id, params...] = msg.match[1].split(" ").map (e) -> e.trim() 38 | console.log "cloudwatch: #{id}" 39 | console.log "message.room: #{msg.message.room}" 40 | 41 | chart.slack.post "#{msg.room}", [id, params...], (err, file)-> 42 | if (err) 43 | console.error err.stack 44 | msg.send err.message 45 | ``` 46 | 47 | 48 | For arguments and available options, see [here](https://github.com/tmtk76/aws-cloudwatch-chart-slack/blob/master/bin/slack-cloudwatch-chart#L1://github.com/tmtk75/aws-cloudwatch-chart-slack/blob/master/bin/slack-cloudwatch-chart#L5). 49 | 50 | 51 | ## How to give arguments 52 | By default, metric is `CPUUtilization` and namespace is `AWS/EC2`. 53 | ``` 54 | cloudwatch i-12345678 55 | ``` 56 | 57 | You can give metric and namespace at 2nd and 3rd arguments. 58 | ``` 59 | cloudwatch main-db FreeableMemory AWS/RDS 60 | ``` 61 | 62 | Multiple IDs can be given seperated with `,`. 63 | ``` 64 | cloudwatch i-xyza5678,i-abcd1234 65 | ``` 66 | 67 | `--region` is to specify a AWS region. 68 | ``` 69 | cloudwatch i-abcd4567 --region us-west-2 70 | ``` 71 | 72 | `--statistics` has `Maximum`, `Minimum`, `Average`, `Sum` and `SampleCount`. 73 | ``` 74 | cloudwatch i-abcd1234 --statistics Maximum 75 | ``` 76 | 77 | Duration and period also can be given with `--duration` and `period` options. 78 | ``` 79 | cloudwatch i-abcd1234 --duration 3days --period 1hour 80 | ``` 81 | 82 | Regarding AWS/EC2, you can filter EC2 instances with some tags. 83 | Next example is that `site` is `dev` and `role` is `webapp` or `db`. 84 | ``` 85 | cloudwatch "tag:site=dev,role=webapp|db" 86 | ``` 87 | 88 | 89 | ## Development 90 | ``` 91 | $ gulp 92 | ``` 93 | The gulp default task is to complie watching change of sources. 94 | `src/*.js` are compiled and saved under `dist`. 95 | 96 | ``` 97 | $ npm test 98 | or 99 | $ gulp test 100 | ``` 101 | The 1st one is to run test once, the 2nd one watches change of sources. 102 | 103 | ``` 104 | $ npm run lint 105 | ``` 106 | Linting with ESLint. 107 | 108 | ``` 109 | $ npm run typecheck 110 | ``` 111 | Run type check with [flow](http://flowtype.org/). 112 | 113 | 114 | ### How it works 115 | ``` 116 | dist/index.js 117 | | 118 | v 119 | dist/render.js Generate a png file 120 | | 121 | v 122 | dist/print-stats.js Retrieve stats with aws-sdk CloudWatch. 123 | | 124 | | stdin 125 | v 126 | spawn: dist/gen-chart.js Generate a .js file for c3 and a .html file. 127 | | Load the .html file with phantomjs and render a chart as .png 128 | | filename 129 | | 130 | +<--+ 131 | | 132 | v 133 | dist/upload.js Read file. 134 | | Upload it to Slack with a REST API. 135 | | 136 | v 137 | unlink the file 138 | ``` 139 | 140 | 141 | ## Sub modules 142 | ### Print statistics 143 | Print stats using aws-sdk. Environment variables for AWS are referred. 144 | ``` 145 | $ node ./dist/print-stats.js [options] 146 | [{"Namespace":"AWS/EC2","InstanceId":"i-003bb906","Label":"CPUUtilization","Respon... 147 | ``` 148 | 149 | ### Generating chart image in .png 150 | Generate a png image and show the path. 151 | ``` 152 | $ cat | phantomjs ./dist/gen-chart.js 153 | ./.97516-1454216914841.png 154 | ``` 155 | 156 | ### Render 157 | `render.js` calls `print-stats.js` and `gen-chart.js`. 158 | ``` 159 | $ node dist/render.js 160 | ./.97516-1454216914841.png 161 | ``` 162 | 163 | ### Upload to a channel of Slack 164 | ``` 165 | $ node dist/upload.js ./.97516-1454216914841.png 166 | { ok: true, 167 | file: 168 | ... 169 | ``` 170 | 171 | 172 | ## How to debug for rendered charts 173 | You can prevent removing temporary files with two options. 174 | ``` 175 | $ node dist/render.js i-003bb906 --filename a.png --keep-html --keep-js 176 | a.png 177 | ``` 178 | 179 | The options preserve temporary files `a.png.html` and `a.png.js`. 180 | You can open the html file and see the chart rendered by c3.js. 181 | ``` 182 | $ open a.png.html 183 | ``` 184 | 185 | ## Contribution 186 | 1. Fork me () 187 | 1. Create your feature branch (git checkout -b my-new-feature) 188 | 1. Commit your changes (git commit -am 'Add some feature') 189 | 1. Pass `npm run typecheck` 190 | 1. Pass `npm run lint` 191 | 1. Add test cases for your changes 192 | 1. Pass `npm test` 193 | 1. Push to the branch (git push origin my-new-feature) 194 | 1. Create your new pull request 195 | 196 | 197 | ## License 198 | 199 | [MIT License](http://opensource.org/licenses/MIT) 200 | 201 | -------------------------------------------------------------------------------- /bin/ls-dynamodb-tables: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sort_key=${1-Name} 3 | aws dynamodb list-tables \ 4 | --output table \ 5 | --query 'TableNames' 6 | -------------------------------------------------------------------------------- /bin/ls-ec2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sort_key=${1-Name} 3 | aws ec2 describe-instances \ 4 | --output table \ 5 | --filter Name=instance-state-name,Values=running \ 6 | --query ' 7 | sort_by( 8 | Reservations[].Instances[], 9 | &(Tags[?Key==`'$sort_key'`]|[0].Value) 10 | )[] 11 | .[ 12 | Tags[?Key==`Name`]|[0].Value, 13 | InstanceType, 14 | InstanceId, 15 | PublicIpAddress, 16 | PrivateIpAddress, 17 | Placement.AvailabilityZone 18 | ]' 19 | -------------------------------------------------------------------------------- /bin/ls-rds: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sort_key=${1-Name} 3 | aws rds describe-db-instances \ 4 | --output table \ 5 | --query ' 6 | DBInstances[] 7 | .[ 8 | DBInstanceIdentifier, 9 | DBInstanceClass, 10 | AllocatedStorage, 11 | MultiAZ 12 | ]' 13 | -------------------------------------------------------------------------------- /bin/slack-cloudwatch-chart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | usage() { 3 | name=$(basename $0) 4 | cat<&2 5 | Usage: ${name} [options] [metric-name] [namespace] 6 | 7 | Timeseries chart renderer retrieving from CloudWatch. 8 | metrics-name is CPUUtilization, namespace is AWS/EC2 by default. 9 | 10 | Options: 11 | --region AWS region name e.g) us-west-1 12 | --duration Duration e.g) 3days, 1h, 30minutes 13 | --period Period e.g) 30minutes 14 | --statistics Statistics Maximum,Minimum,Average,Sum,SampleCount 15 | 16 | --width Chart width e.g) 800 17 | --height Chart height e.g) 300 18 | --max Y axis max 19 | --min Y axis min 20 | --grid-x Show x grid 21 | --grid-y Show y grid 22 | --utc X axis in UTC 23 | --bytes Y axis in bytes instead of megabytes 24 | --x-label Label for x 25 | 26 | --keep-html Disable to remove temporary .html file 27 | --keep-js Disable to remove temporary .js file 28 | --filename,-f Generated filename 29 | --base64 Output as base64 30 | --without-image Disable genrating image 31 | 32 | Examples: 33 | ${name} i-0935c127 34 | ${name} i-0935c127,i-003bb900 35 | ${name} i-0935c127 DiskReadOps AWS/EC2 36 | 37 | ${name} test-db cpu AWS/RDS 38 | ${name} test-db CPUUtilization AWS/RDS 39 | 40 | EOF 41 | } 42 | 43 | if [ "$1" == "" ]; then 44 | usage 45 | exit 1 46 | fi 47 | 48 | cli_path="$(cd ${0%/*}/..; pwd)"/aws-cloudwatch-chart-slack/dist/cli.js 49 | if [ -e $cli_path ]; then 50 | node $cli_path $* 51 | else 52 | node dist/cli.js $* 53 | fi 54 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.12 4 | 5 | -------------------------------------------------------------------------------- /dist/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | var _index = require("./index.js"); 5 | 6 | var channel_name = process.env.SLACK_CHANNEL_NAME || "#api-test"; 7 | _index.slack.post(channel_name, process.argv.slice(2), function (err, file) { 8 | if (err) { 9 | console.error(err.stack); 10 | return; 11 | } 12 | console.log(file.thumb_80); 13 | }); -------------------------------------------------------------------------------- /dist/cloudwatch.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _awsSdk = require("aws-sdk"); 12 | 13 | var _awsSdk2 = _interopRequireDefault(_awsSdk); 14 | 15 | var _time = require("./time.js"); 16 | 17 | var _time2 = _interopRequireDefault(_time); 18 | 19 | var _moment = require("moment"); 20 | 21 | var _moment2 = _interopRequireDefault(_moment); 22 | 23 | var _metrics = require("./metrics.js"); 24 | 25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 26 | 27 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 28 | 29 | var CloudWatch = function () { 30 | function CloudWatch() { 31 | _classCallCheck(this, CloudWatch); 32 | } 33 | 34 | _createClass(CloudWatch, [{ 35 | key: "endTime", 36 | 37 | 38 | /** */ 39 | value: function endTime(d) { 40 | this._endTime = d; 41 | return this; 42 | } 43 | 44 | /** */ 45 | 46 | }, { 47 | key: "duration", 48 | value: function duration(d) { 49 | this._duration = d; 50 | return this; 51 | } 52 | 53 | /** */ 54 | 55 | }, { 56 | key: "period", 57 | value: function period(p) { 58 | this._period = p; 59 | return this; 60 | } 61 | 62 | /** */ 63 | 64 | }, { 65 | key: "statistics", 66 | value: function statistics(name) { 67 | this._statistics = name; 68 | return this; 69 | } 70 | 71 | /** */ 72 | 73 | }, { 74 | key: "metricStatistics", 75 | value: function metricStatistics(namespace, instanceID, metricName) { 76 | var dimName = (0, _metrics.nsToDimName)(namespace); 77 | var metric = (0, _metrics.searchMetric)(namespace, metricName); 78 | var sep = _time2.default.toSEP(this._duration, this._endTime); 79 | if (this._period) { 80 | sep.Period = this._period; 81 | } 82 | if (this._statistics) { 83 | metric.Statistics = [this._statistics]; 84 | } 85 | 86 | var params = _extends({}, sep, metric, { 87 | Namespace: namespace, 88 | Dimensions: [{ 89 | Name: dimName, 90 | Value: instanceID 91 | }] 92 | }); 93 | 94 | //process.stderr.write(JSON.stringify(params)); 95 | var cloudwatch = new _awsSdk2.default.CloudWatch(); 96 | return new Promise(function (resolve, reject) { 97 | return cloudwatch.getMetricStatistics(params, function (err, data) { 98 | return err ? reject(err) : resolve([sep, data]); 99 | }); 100 | }); 101 | } 102 | }]); 103 | 104 | return CloudWatch; 105 | }(); 106 | 107 | exports.default = CloudWatch; -------------------------------------------------------------------------------- /dist/dynamodb.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.mimic = mimic; 7 | exports.toY = toY; 8 | 9 | var _metrics = require("./metrics.js"); 10 | 11 | var m = _interopRequireWildcard(_metrics); 12 | 13 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 14 | 15 | /** 16 | * Returns true if stat is exactly in a condition. 17 | */ 18 | function mimic(stat) { 19 | return stat.Namespace === "AWS/DynamoDB" && stat.Period === 60 && (stat.Label === "ConsumedReadCapacityUnits" || stat.Label === "ConsumedWriteCapacityUnits") && stat.Datapoints[0].Sum !== undefined; 20 | } 21 | function toY(e) { 22 | var bytes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 23 | 24 | return m.toY(e, bytes) / 60; 25 | } 26 | 27 | exports.default = { 28 | mimic: mimic, 29 | toY: toY 30 | }; -------------------------------------------------------------------------------- /dist/ec2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AWS = require("aws-sdk"); 4 | 5 | var params = { 6 | //DryRun: true || false, 7 | //Filters: [ 8 | // { 9 | // Name: 'STRING_VALUE', 10 | // Values: [ 11 | // 'STRING_VALUE', 12 | // /* more items */ 13 | // ] 14 | // }, 15 | // /* more items */ 16 | //], 17 | InstanceIds: ["i-003bb906", "i-0935c111", "i-1c41aa18"] 18 | }; 19 | 20 | //MaxResults: 0, 21 | //NextToken: 'STRING_VALUE' 22 | AWS.config.update({ region: "ap-northeast-1" }); 23 | var ec2 = new AWS.EC2(); 24 | ec2.describeInstances(params, function (err, data) { 25 | if (err) console.log(err, err.stack); // an error occurred 26 | else console.log(JSON.stringify(data)); // successful response 27 | }); -------------------------------------------------------------------------------- /dist/gen-chart.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _phantomApi = require("./phantom-api.js"); 4 | 5 | var _moment = require("moment"); 6 | 7 | var _moment2 = _interopRequireDefault(_moment); 8 | 9 | var _metrics = require("./metrics.js"); 10 | 11 | var _dynamodb = require("./dynamodb.js"); 12 | 13 | var _dynamodb2 = _interopRequireDefault(_dynamodb); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | var argv = require("minimist")(_phantomApi.system.args.slice(1), { 18 | string: ["filename", "width", "height", "max", "min", "node_modules_path", "x-label", "format", "bindto", "point-r"], 19 | boolean: ["base64", "keep-html", "keep-js", "grid-x", "grid-y", "utc", "bytes", "without-image"], 20 | alias: { 21 | f: "filename" 22 | }, 23 | default: { 24 | width: 800, 25 | height: 300, 26 | node_modules_path: "./node_modules", 27 | format: "png", 28 | "x-tick-count": 120, 29 | "x-tick-culling-max": 10, 30 | "bindto": "container", 31 | "point-r": 2.5 32 | } 33 | }); 34 | 35 | try { 36 | (function () { 37 | var stats_data = JSON.parse(_phantomApi.system.stdin.read()); 38 | var repre = stats_data[0]; 39 | var MetricName = repre.Label || ""; 40 | var Namespace = repre.Namespace || ""; 41 | var sort = function sort(datapoints) { 42 | return datapoints.sort(function (a, b) { 43 | return a.Timestamp.localeCompare(b.Timestamp); 44 | }); 45 | }; 46 | var yData = stats_data.map(function (stats) { 47 | if (stats.Datapoints.length < 2) { 48 | throw new Error("Number of datapoints is less than 2 for " + MetricName + " of " + stats.InstanceId + ". There is a possibility InstanceId was wrong. " + JSON.stringify(stats)); 49 | } 50 | var b = _dynamodb2.default.mimic(stats); 51 | return [stats[(0, _metrics.nsToDimName)(Namespace)]].concat(sort(stats.Datapoints).map(function (e) { 52 | return b ? _dynamodb2.default.toY(e) : (0, _metrics.toY)(e, argv.bytes); 53 | })); 54 | }); 55 | var textLabelX = (0, _metrics.to_axis_x_label_text)(repre, argv.utc); 56 | 57 | var data = { 58 | _meta: { StartTime: repre.StartTime, EndTime: repre.EndTime, UTC: argv.utc }, 59 | bindto: "#" + argv.bindto, 60 | data: { 61 | x: "x", 62 | columns: [["x"].concat(sort(repre.Datapoints).map(function (e) { 63 | return _moment2.default.utc(e["Timestamp"]).valueOf(); 64 | }))].concat(yData) 65 | }, 66 | transition: { 67 | duration: null }, 68 | size: { 69 | width: argv.width - 16, // heuristic adjustments 70 | height: argv.height - 16 71 | }, 72 | axis: { 73 | y: { 74 | max: argv.max ? parseInt(argv.max) : (0, _metrics.toMax)(repre), 75 | min: argv.min ? parseInt(argv.min) : (0, _metrics.toMin)(repre), 76 | padding: { top: 0, bottom: 0 }, 77 | label: { 78 | text: Namespace + " " + MetricName + " " + (0, _metrics.toAxisYLabel)(repre, argv.bytes), 79 | position: "outer-middle" 80 | } 81 | }, 82 | x: { 83 | type: "timeseries", 84 | tick: { 85 | count: argv["x-tick-count"], 86 | culling: { 87 | max: argv["x-tick-culling-max"] 88 | }, 89 | _format: "%Y-%m-%dT%H:%M:%S", 90 | format: "%H:%M" 91 | }, 92 | //padding: {left: 0, right: 0}, 93 | label: { 94 | text: argv["x-label"] || textLabelX, 95 | position: "outer-center" 96 | }, 97 | localtime: !argv.utc 98 | } 99 | }, 100 | grid: { 101 | x: { 102 | show: argv["grid-x"] 103 | }, 104 | y: { 105 | show: argv["grid-y"] 106 | } 107 | }, 108 | point: { 109 | r: argv["point-r"] 110 | } 111 | }; 112 | 113 | render(argv, data); 114 | })(); 115 | } catch (ex) { 116 | _phantomApi.system.stderr.write(ex.stack); 117 | _phantomApi.system.stderr.write("\n"); 118 | phantom.exit(1); 119 | } 120 | 121 | /* 122 | * Rendering 123 | */ 124 | function render(argv, data) { 125 | var page = _phantomApi.webpage.create(); 126 | page.onConsoleMessage = function (msg) { 127 | return console.log(msg); 128 | }; 129 | page.viewportSize = { 130 | width: argv.width ? parseInt(argv.width) : page.viewportSize.width, 131 | height: argv.height ? parseInt(argv.height) : page.viewportSize.height 132 | }; 133 | //console.log(JSON.stringify(page.viewportSize)) 134 | 135 | var suffix = argv.filename || "." + _phantomApi.system.pid + "-" + new Date().getTime(); 136 | var tmp_html = "./" + suffix + ".html"; 137 | var tmp_js = "./" + suffix + ".js"; 138 | var filename = argv.filename || "./" + suffix + ".png"; 139 | var node_modules_path = argv.node_modules_path; 140 | 141 | var now = (0, _moment2.default)().format("YYYY-MM-DD HH:mm:ss Z"); 142 | _phantomApi.fs.write(tmp_js, "\n // Generated at " + now + "\n var data = " + JSON.stringify(data) + ";\n data.axis.y.tick = {format: d3.format(',')};\n c3.generate(data);\n "); 143 | _phantomApi.fs.write(tmp_html, "\n \n \n \n \n \n \n
\n \n \n \n "); 144 | 145 | page.open(tmp_html, function (status) { 146 | //console.error(JSON.stringify(argv)) 147 | if (!argv["without-image"]) { 148 | page.render(filename, { format: argv.format }); 149 | _phantomApi.system.stdout.write(filename); 150 | } else if (argv.base64) { 151 | _phantomApi.system.stdout.write(page.renderBase64(argv.format)); 152 | } 153 | if (!argv["keep-html"]) { 154 | _phantomApi.fs.remove(tmp_html); 155 | } 156 | if (!argv["keep-js"]) { 157 | _phantomApi.fs.remove(tmp_js); 158 | } 159 | phantom.exit(); 160 | }); 161 | } -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 5 | 6 | var _fs = require("fs"); 7 | 8 | var _fs2 = _interopRequireDefault(_fs); 9 | 10 | var _render = require("./render.js"); 11 | 12 | var _upload = require("./upload.js"); 13 | 14 | var _lsEc = require("./ls-ec2.js"); 15 | 16 | var _procGenChart = require("./proc-gen-chart.js"); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | function unlink(path) { 21 | return new Promise(function (resolve, reject) { 22 | return _fs2.default.unlink(path, function (err) { 23 | return err ? reject(err) : resolve(path); 24 | }); 25 | }); 26 | } 27 | 28 | function post(channel, args, callback) { 29 | var cb_ok = callback || function (err, data) { 30 | return console.log(data); 31 | }; 32 | var cb_err = callback || function (err, data) { 33 | return console.error(err); 34 | }; 35 | return (0, _render.render)(args).then(function (path) { 36 | return Promise.all([(0, _upload.upload)(channel, path), unlink(path)]); 37 | }).then(function (_ref) { 38 | var _ref2 = _slicedToArray(_ref, 2), 39 | file = _ref2[0].file, 40 | path = _ref2[1]; 41 | 42 | return cb_ok(null, file); 43 | }).catch(function (err) { 44 | return cb_err(err); 45 | }); 46 | } 47 | 48 | module.exports = { 49 | slack: { 50 | post: post 51 | }, 52 | ls_ec2: _lsEc.ls_ec2, 53 | render: _render.render, 54 | proc_gen_chart: _procGenChart.proc_gen_chart 55 | }; -------------------------------------------------------------------------------- /dist/ls-ec2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 8 | 9 | exports.to_filters = to_filters; 10 | exports.tag_string_to_filters = tag_string_to_filters; 11 | exports.ls_ec2 = ls_ec2; 12 | 13 | var _awsSdk = require("aws-sdk"); 14 | 15 | var _awsSdk2 = _interopRequireDefault(_awsSdk); 16 | 17 | var _immutable = require("immutable"); 18 | 19 | var _immutable2 = _interopRequireDefault(_immutable); 20 | 21 | var _minimist = require("minimist"); 22 | 23 | var _minimist2 = _interopRequireDefault(_minimist); 24 | 25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 26 | 27 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 28 | 29 | /** */ 30 | function describe_instances() { 31 | var filters = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 32 | 33 | //AWS.config.update({region: "ap-northeast-1"}) 34 | var ec2 = new _awsSdk2.default.EC2(); 35 | var params = { 36 | Filters: [{ Name: "instance-state-name", Values: ["running"] }].concat(_toConsumableArray(filters)) 37 | }; 38 | return new Promise(function (resolve, reject) { 39 | return ec2.describeInstances(params, function (err, data) { 40 | return err ? reject(err) : resolve(data); 41 | }); 42 | }).then(function (r) { 43 | return r.Reservations.map(function (e) { 44 | return e.Instances[0]; 45 | }); 46 | }).then(function (r) { 47 | return r.map(function (e) { 48 | return { 49 | InstanceId: e.InstanceId, 50 | InstanceType: e.InstanceType, 51 | Name: (_immutable2.default.List(e.Tags).find(function (e) { 52 | return e.Key === "Name"; 53 | }) || {}).Value 54 | }; 55 | }); 56 | }); 57 | } 58 | 59 | /** 60 | * in: tag:site=dev,role=webapp|db 61 | * 62 | * out: [ 63 | * {Name: "tag:site", Values: ["dev"]}, 64 | * {Name: "tag:role", Values: ["webapp", "db"]}, 65 | * ] 66 | */ 67 | function to_filters(s) { 68 | return s.trim().split(",").map(function (e) { 69 | return e.trim().split("="); 70 | }).map(function (_ref) { 71 | var _ref2 = _slicedToArray(_ref, 2), 72 | k = _ref2[0], 73 | v = _ref2[1]; 74 | 75 | return { Name: "tag:" + k, Values: [].concat(_toConsumableArray(v.split("|"))) }; 76 | }); 77 | } 78 | 79 | function tag_string_to_filters(s) { 80 | if (!s) return []; 81 | var a = s.match("^tag:(.*)"); 82 | return a ? to_filters(a[1]) : []; 83 | } 84 | 85 | function ls_ec2(f) { 86 | return describe_instances(to_filters(f)).then(function (e) { 87 | return e.map(function (s) { 88 | return s.InstanceId; 89 | }); 90 | }).then(function (e) { 91 | return e.join(","); 92 | }); 93 | } 94 | 95 | if (require.main === module) { 96 | var argv = (0, _minimist2.default)(process.argv.slice(2)); 97 | _awsSdk2.default.config.update({ region: argv.region || process.env.AWS_DEFAULT_REGION || "ap-northeast-1" }); 98 | describe_instances(tag_string_to_filters(argv._[0])).then(function (r) { 99 | return console.log(JSON.stringify(r, null, 2)); 100 | }).catch(function (err) { 101 | return console.error(err.stack); 102 | }); 103 | } -------------------------------------------------------------------------------- /dist/ls-rds.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.ls_rds = ls_rds; 7 | 8 | var _awsSdk = require("aws-sdk"); 9 | 10 | var _awsSdk2 = _interopRequireDefault(_awsSdk); 11 | 12 | var _immutable = require("immutable"); 13 | 14 | var _immutable2 = _interopRequireDefault(_immutable); 15 | 16 | var _lsEc = require("./ls-ec2.js"); 17 | 18 | var ec2 = _interopRequireWildcard(_lsEc); 19 | 20 | var _minimist = require("minimist"); 21 | 22 | var _minimist2 = _interopRequireDefault(_minimist); 23 | 24 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 25 | 26 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 27 | 28 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 29 | 30 | /** */ 31 | function describe_db_instances() { 32 | var filters = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 33 | 34 | //AWS.config.update({region: "ap-northeast-1"}) 35 | var rds = new _awsSdk2.default.RDS(); 36 | var params = { 37 | Filters: [].concat(_toConsumableArray(filters)) 38 | }; 39 | return new Promise(function (resolve, reject) { 40 | return rds.describeDBInstances(params, function (err, data) { 41 | return err ? reject(err) : resolve(data); 42 | }); 43 | }).then(function (r) { 44 | return r; 45 | }); 46 | } 47 | 48 | function ls_rds(f) { 49 | return describe_db_instances(ec2.to_filters(f)).then(function (e) { 50 | return e.map(function (s) { 51 | return s.InstanceId; 52 | }); 53 | }).then(function (e) { 54 | return e.join(","); 55 | }); 56 | } 57 | 58 | if (require.main === module) { 59 | var argv = (0, _minimist2.default)(process.argv.slice(2)); 60 | _awsSdk2.default.config.update({ region: argv.region || process.env.AWS_DEFAULT_REGION || "ap-northeast-1" }); 61 | describe_db_instances(ec2.tag_string_to_filters(argv._[0])).then(function (r) { 62 | return console.log(JSON.stringify(r, null, 2)); 63 | }).catch(function (err) { 64 | return console.error(err.stack); 65 | }); 66 | } -------------------------------------------------------------------------------- /dist/metrics.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.METRICS = undefined; 7 | 8 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 9 | 10 | exports.searchMetric = searchMetric; 11 | exports.nsToDimName = nsToDimName; 12 | exports.toMax = toMax; 13 | exports.toMin = toMin; 14 | exports.toAxisYLabel = toAxisYLabel; 15 | exports.toY = toY; 16 | exports.find_stat_name = find_stat_name; 17 | exports.calc_period = calc_period; 18 | exports.to_axis_x_label_text = to_axis_x_label_text; 19 | 20 | var _immutable = require("immutable"); 21 | 22 | var _immutable2 = _interopRequireDefault(_immutable); 23 | 24 | var _moment = require("moment"); 25 | 26 | var _moment2 = _interopRequireDefault(_moment); 27 | 28 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 29 | 30 | var metricsRDS = [{ MetricName: "BinLogDiskUsage", Statistics: ["Maximum"] }, { MetricName: "CPUUtilization", Statistics: ["Average"] }, { MetricName: "DatabaseConnections", Statistics: ["Maximum"] }, { MetricName: "DiskQueueDepth", Statistics: ["Maximum"] }, { MetricName: "FreeStorageSpace", Statistics: ["Minimum"] }, { MetricName: "FreeableMemory", Statistics: ["Minimum"] }, { MetricName: "NetworkReceiveThroughput", Statistics: ["Maximum"] }, { MetricName: "NetworkTransmitThroughput", Statistics: ["Maximum"] }, { MetricName: "ReadIOPS", Statistics: ["Maximum"] }, { MetricName: "ReadLatency", Statistics: ["Maximum"] }, { MetricName: "ReadThroughput", Statistics: ["Maximum"] }, { MetricName: "SwapUsage", Statistics: ["Maximum"] }, { MetricName: "WriteIOPS", Statistics: ["Maximum"] }, { MetricName: "WriteLatency", Statistics: ["Maximum"] }, { MetricName: "WriteThroughput", Statistics: ["Maximum"] }]; 31 | 32 | var metricsEC2 = [{ MetricName: "CPUCreditUsage", Statistics: ["Maximum"] }, { MetricName: "CPUCreditBalance", Statistics: ["Maximum"] }, { MetricName: "CPUUtilization", Statistics: ["Average"] }, { MetricName: "DiskReadOps", Statistics: ["Maximum"] }, { MetricName: "DiskWriteOps", Statistics: ["Maximum"] }, { MetricName: "DiskReadBytes", Statistics: ["Maximum"] }, { MetricName: "DiskWriteBytes", Statistics: ["Maximum"] }, { MetricName: "NetworkIn", Statistics: ["Maximum"] }, { MetricName: "NetworkOut", Statistics: ["Maximum"] }, { MetricName: "StatusCheckFailed", Statistics: ["Maximum"] }, { MetricName: "StatusCheckFailed_Instance", Statistics: ["Maximum"] }, { MetricName: "StatusCheckFailed_System", Statistics: ["Maximum"] }]; 33 | 34 | var metricsDynamoDB = [{ MetricName: "ConsumedReadCapacityUnits", Statistics: ["Sum"] }, { MetricName: "ConsumedWriteCapacityUnits", Statistics: ["Sum"] }, { MetricName: "ProvisionedReadCapacityUnits", Statistics: ["Maximum"] }, { MetricName: "ProvisionedWriteCapacityUnits", Statistics: ["Maximum"] }, { MetricName: "ConditionalCheckFailedRequests", Statistics: ["Maximum"] }, { MetricName: "OnlineIndexConsumedWriteCapacity", Statistics: ["Maximum"] }, { MetricName: "OnlineIndexPercentageProgress", Statistics: ["Maximum"] }, { MetricName: "OnlineIndexThrottleEvents", Statistics: ["Maximum"] }, { MetricName: "ReturnedItemCount", Statistics: ["Maximum"] }, { MetricName: "SuccessfulRequestLatency", Statistics: ["Maximum"] }, { MetricName: "SystemErrors", Statistics: ["Maximum"] }, { MetricName: "ThrottledRequests", Statistics: ["Maximum"] }, { MetricName: "UserErrors", Statistics: ["Maximum"] }, { MetricName: "WriteThrottleEvents", Statistics: ["Maximum"] }, { MetricName: "ReadThrottleEvents", Statistics: ["Sum"] }]; 35 | 36 | "Seconds | Microseconds | Milliseconds | Bytes | Kilobytes | Megabytes | Gigabytes | Terabytes | Bits | Kilobits | Megabits | Gigabits | Terabits | Percent | Count | Bytes/Second | Kilobytes/Second | Megabytes/Second | Gigabytes/Second | Terabytes/Second | Bits/Second | Kilobits/Second | Megabits/Second | Gigabits/Second | Terabits/Second | Count/Second | None"; 37 | "Minimum | Maximum | Sum | Average | SampleCount"; 38 | 39 | var METRICS = exports.METRICS = { 40 | "AWS/EC2": metricsEC2, 41 | "AWS/RDS": metricsRDS, 42 | "AWS/DynamoDB": metricsDynamoDB 43 | }; 44 | 45 | function searchMetric(ns, metricName) { 46 | var a = METRICS[ns].filter(function (_ref) { 47 | var n = _ref.MetricName; 48 | return n.match(new RegExp(metricName, "i")); 49 | }); 50 | return a[0]; 51 | } 52 | 53 | function nsToDimName(ns) { 54 | return { 55 | "AWS/RDS": "DBInstanceIdentifier", 56 | "AWS/EC2": "InstanceId", 57 | "AWS/DynamoDB": "TableName" 58 | }[ns]; 59 | } 60 | 61 | function toMax(metrics) { 62 | if (!metrics.Datapoints[0]) { 63 | return null; 64 | } 65 | if (metrics.Datapoints[0].Unit === "Percent") { 66 | return 100.0; 67 | } 68 | return null; 69 | } 70 | 71 | function toMin(metrics) { 72 | if (!metrics.Datapoints[0]) { 73 | return null; 74 | } 75 | if (metrics.Datapoints[0].Unit === "Percent") { 76 | return 0.0; 77 | } 78 | return null; 79 | } 80 | 81 | function toAxisYLabel(metrics, bytes) { 82 | if (metrics.Datapoints[0].Unit === "Bytes" && !bytes) { 83 | return "Megabytes"; 84 | } 85 | return metrics.Datapoints[0].Unit; 86 | } 87 | 88 | function toY(metric, bytes) { 89 | var e = metric["Maximum"] || metric["Average"] || metric["Minimum"] || metric["Sum"] || metric["SampleCount"]; 90 | if (metric.Unit === "Bytes" && !bytes) { 91 | return e / (1024 * 1024); // Megabytes 92 | } 93 | return e || 0; 94 | } 95 | 96 | var _stats = _immutable2.default.Set(["Maximum", "Average", "Minimum", "Sum", "SampleCount"]); 97 | function find_stat_name(datapoints) { 98 | if (!(datapoints && datapoints.length > 0)) return null; 99 | var dp = datapoints[0]; 100 | return _immutable2.default.List(Object.keys(dp)).find(function (e) { 101 | return _stats.has(e); 102 | }); 103 | } 104 | 105 | function calc_period(datapoints) { 106 | var measurement = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "minutes"; 107 | 108 | if (!(datapoints && datapoints.length > 1)) return null; 109 | 110 | var _datapoints$sort = datapoints.sort(function (a, b) { 111 | return a.Timestamp.localeCompare(b.Timestamp); 112 | }), 113 | _datapoints$sort2 = _slicedToArray(_datapoints$sort, 2), 114 | a = _datapoints$sort2[0], 115 | b = _datapoints$sort2[1]; 116 | 117 | return (0, _moment2.default)(b.Timestamp).diff((0, _moment2.default)(a.Timestamp), measurement); 118 | } 119 | 120 | function to_axis_x_label_text(stats, utc) { 121 | var et = (0, _moment2.default)(stats.EndTime); 122 | var diff = (0, _moment2.default)(stats.StartTime).diff(et); 123 | var tz = utc ? "UTC" : new Date().getTimezoneOffset() / 60 + "h"; 124 | 125 | var ago = _moment2.default.duration(diff).humanize(); 126 | var from = (utc ? et.utc() : et).format("YYYY-MM-DD HH:mm"); 127 | return find_stat_name(stats.Datapoints) + " every " + calc_period(stats.Datapoints) + "minutes from " + from + " (tz:" + tz + ") to " + ago + " ago"; 128 | } 129 | 130 | // 131 | if (require.main === module) { 132 | console.log(JSON.stringify(METRICS, null, 2)); 133 | } -------------------------------------------------------------------------------- /dist/phantom-api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.webpage = exports.fs = exports.system = undefined; 7 | 8 | var _system = require("system"); 9 | 10 | var _system2 = _interopRequireDefault(_system); 11 | 12 | var _fs = require("fs"); 13 | 14 | var _fs2 = _interopRequireDefault(_fs); 15 | 16 | var _webpage = require("webpage"); 17 | 18 | var _webpage2 = _interopRequireDefault(_webpage); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | exports.system = _system2.default; 23 | exports.fs = _fs2.default; 24 | exports.webpage = _webpage2.default; -------------------------------------------------------------------------------- /dist/print-names.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 4 | 5 | var _immutable = require("immutable"); 6 | 7 | var _immutable2 = _interopRequireDefault(_immutable); 8 | 9 | var _cloudwatch = require("./cloudwatch.js"); 10 | 11 | var _cloudwatch2 = _interopRequireDefault(_cloudwatch); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | var argv = require("minimist")(process.argv.slice(2)); 16 | var region = argv.region || process.env.AWS_DEFAULT_REGION || "ap-northeast-1"; 17 | var namespace = argv.namespace || "AWS/EC2"; 18 | 19 | _cloudwatch2.default.region(region).names().then(function (r) { 20 | return r.Metrics; 21 | }).then(function (r) { 22 | return [r.map(function (e) { 23 | return e.MetricName; 24 | }), r.map(function (_ref) { 25 | var _ref$Dimensions = _slicedToArray(_ref.Dimensions, 1); 26 | 27 | var d = _ref$Dimensions[0]; 28 | return d; 29 | }).filter(function (e) { 30 | return e; 31 | }) 32 | //.filter(d => d.Name === "DBInstanceIdentifier") // declared in params 33 | .map(function (d) { 34 | return d ? d.Value : null; 35 | })]; 36 | }).then(function (_ref2) { 37 | var _ref3 = _slicedToArray(_ref2, 2); 38 | 39 | var a = _ref3[0]; 40 | var b = _ref3[1]; 41 | return [_immutable2.default.Set(a), _immutable2.default.Set(b)]; 42 | }).then(function (_ref4) { 43 | var _ref5 = _slicedToArray(_ref4, 2); 44 | 45 | var a = _ref5[0]; 46 | var b = _ref5[1]; 47 | return [a.sort()]; 48 | }). /*b.sort()*/then(function (r) { 49 | return console.log(JSON.stringify(r)); 50 | }).catch(function (err) { 51 | return console.log(err); 52 | }); -------------------------------------------------------------------------------- /dist/print-stats.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 4 | 5 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 6 | 7 | var _awsSdk = require("aws-sdk"); 8 | 9 | var _awsSdk2 = _interopRequireDefault(_awsSdk); 10 | 11 | var _cloudwatch = require("./cloudwatch.js"); 12 | 13 | var _cloudwatch2 = _interopRequireDefault(_cloudwatch); 14 | 15 | var _metrics = require("./metrics.js"); 16 | 17 | var _time = require("./time.js"); 18 | 19 | var _lsEc = require("./ls-ec2.js"); 20 | 21 | var _path = require("path"); 22 | 23 | var _path2 = _interopRequireDefault(_path); 24 | 25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 26 | 27 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 28 | 29 | function print_stats(argv) { 30 | //let [ instID, metricName = 'CPUUtilization', ns = 'AWS/EC2'] = argv._; // flow doesn't support 31 | var instIDs = argv._[0]; 32 | var metricName = argv._[1] || "CPUUtilization"; 33 | var ns = argv._[2] || "AWS/EC2"; 34 | var dimName = (0, _metrics.nsToDimName)(ns); 35 | var region = argv.region || process.env.AWS_DEFAULT_REGION || "ap-northeast-1"; 36 | var period = (0, _time.toSeconds)(argv.period || "30minutes"); 37 | var stats = argv.statistics; 38 | 39 | if (!instIDs) { 40 | //throw new Error("instanceID is missing") 41 | return Promise.reject(new Error("InstanceId is missing")); 42 | } 43 | 44 | _awsSdk2.default.config.update({ region: region }); 45 | 46 | var watch = function watch(instanceID) { 47 | return new _cloudwatch2.default().endTime(argv["end-time"]).duration(argv.duration || "1day").period(period).statistics(stats).metricStatistics(ns, instanceID, metricName).then(function (_ref) { 48 | var _ref2 = _slicedToArray(_ref, 2), 49 | sep = _ref2[0], 50 | data = _ref2[1]; 51 | 52 | return _extends(_defineProperty({ 53 | Namespace: ns 54 | }, dimName, instanceID), sep, data); 55 | }); 56 | }; 57 | 58 | var a = instIDs.match("^tag:(.*)"); 59 | if (a && ns === "AWS/RDS") return Promise.reject(new Error("filters is not supported AWS/RDS")); 60 | 61 | var p = a ? (0, _lsEc.ls_ec2)(a[1]) : Promise.resolve(instIDs); 62 | return p.then(function (s) { 63 | return s.split(","); 64 | }).then(function (s) { 65 | return Promise.all(s.map(function (e) { 66 | return watch(e.trim()); 67 | })); 68 | }); 69 | } 70 | 71 | module.exports = { 72 | print_stats: print_stats 73 | }; 74 | 75 | if (require.main === module) { 76 | print_stats(require("minimist")(process.argv.slice(2))).then(function (r) { 77 | return process.stdout.write(JSON.stringify(r)); 78 | }).catch(function (err) { 79 | //process.stderr.write(JSON.stringify(err, null, 2)); 80 | console.error(err); 81 | process.exit(1); 82 | }); 83 | } -------------------------------------------------------------------------------- /dist/proc-gen-chart.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.proc_gen_chart = proc_gen_chart; 7 | 8 | var _child_process = require("child_process"); 9 | 10 | var _path = require("path"); 11 | 12 | var _path2 = _interopRequireDefault(_path); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | // workaround: prod.stdin.write doesn't pass for flow check 17 | function write_to_stdin(proc, obj) { 18 | proc.stdin.write(JSON.stringify(obj)); 19 | proc.stdin.end(); 20 | } 21 | 22 | function proc_gen_chart(args) { 23 | return function (r) { 24 | return new Promise(function (resolve, reject) { 25 | var cmd = _path2.default.join(__dirname, "../node_modules/.bin/", "phantomjs"); 26 | var js = _path2.default.join(__dirname, "gen-chart.js"); 27 | var nmp = _path2.default.join(__dirname, "../node_modules"); 28 | var p = (0, _child_process.spawn)(cmd, [js, "--node_modules_path", nmp].concat(args)); 29 | write_to_stdin(p, r); 30 | var buf = ""; 31 | var errbuf = ""; 32 | p.stdout.on("data", function (data) { 33 | return buf += "" + data; 34 | }); // filename: String 35 | p.stderr.on("data", function (data) { 36 | return errbuf += "" + data; 37 | }); 38 | p.on("close", function (code) { 39 | return code == 0 ? resolve(buf) : reject(new Error(errbuf)); 40 | }); 41 | }); 42 | }; 43 | } -------------------------------------------------------------------------------- /dist/render.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _minimist = require("minimist"); 4 | 5 | var _minimist2 = _interopRequireDefault(_minimist); 6 | 7 | var _printStats = require("./print-stats.js"); 8 | 9 | var _procGenChart = require("./proc-gen-chart.js"); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | function render(args) { 14 | return (0, _printStats.print_stats)((0, _minimist2.default)(args)).then((0, _procGenChart.proc_gen_chart)(args)); 15 | } 16 | 17 | 18 | module.exports = { 19 | render: render 20 | }; 21 | 22 | if (require.main === module) { 23 | render(process.argv.slice(2)).then(function (r) { 24 | return console.log(r); 25 | }).catch(function (err) { 26 | return console.error(err.stack); 27 | }); 28 | } -------------------------------------------------------------------------------- /dist/time.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 8 | 9 | exports.toSEP = toSEP; 10 | exports.toSeconds = toSeconds; 11 | 12 | var _moment = require("moment"); 13 | 14 | var _moment2 = _interopRequireDefault(_moment); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | /* 19 | * Start End Period 20 | */ 21 | function toSEP() { 22 | var reltime = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "1day"; 23 | var end = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; 24 | 25 | var a = reltime.match(/([0-9]+) *(.*)/); 26 | if (!a) { 27 | throw new Error(reltime); 28 | } 29 | 30 | var _a = _slicedToArray(a, 3), 31 | _ = _a[0], 32 | n = _a[1], 33 | m = _a[2]; 34 | 35 | var endTime = end ? (0, _moment2.default)(end) : (0, _moment2.default)(); 36 | //console.error(endTime); 37 | var duration = _moment2.default.duration(parseInt(n), m); 38 | var startTime = endTime.clone().subtract(duration); 39 | var period = 60 * 30; 40 | 41 | return { 42 | EndTime: endTime.toDate(), 43 | StartTime: startTime.toDate(), 44 | Period: period 45 | }; 46 | } 47 | 48 | function toSeconds(period) { 49 | var a = period.match(/([0-9]+)(.+)/); 50 | if (!a) { 51 | return NaN; 52 | } 53 | 54 | var _a2 = _slicedToArray(a, 3), 55 | _ = _a2[0], 56 | n = _a2[1], 57 | u = _a2[2]; 58 | 59 | return _moment2.default.duration(parseInt(n), u).asSeconds(); 60 | } 61 | 62 | exports.default = { 63 | toSEP: toSEP, 64 | toSeconds: toSeconds 65 | }; -------------------------------------------------------------------------------- /dist/upload.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | var fs = require("fs"); 5 | var fetch = require("node-fetch"); 6 | var FormData = require("form-data"); 7 | 8 | function upload(channel, path) { 9 | return new Promise(function (resolve, reject) { 10 | var form = new FormData(); 11 | form.append("channels", channel); 12 | form.append("token", process.env.SLACK_API_TOKEN); 13 | //form.append("file", fs.createReadStream(path)); //NOTE: This became to fail somehow suddenly... https://github.com/form-data/form-data#notes 14 | var buf = fs.readFileSync(path); 15 | form.append("file", buf, { 16 | filename: path, 17 | contentType: "image/png", 18 | knownLength: buf.length 19 | }); 20 | 21 | return fetch("https://slack.com/api/files.upload", { method: "POST", body: form }).then(function (res) { 22 | return ( 23 | /* 24 | * {ok: true, file: {...}} See https://api.slack.com/types/file 25 | */ 26 | res.json() 27 | ); 28 | }).then(function (e) { 29 | return e.ok ? resolve(e) : reject(new Error(JSON.stringify(e))); 30 | }).catch(function (err) { 31 | return reject(err); 32 | }); 33 | }); 34 | } 35 | 36 | module.exports = { 37 | upload: upload 38 | }; 39 | 40 | if (require.main === module) { 41 | var channel = process.env.SLACK_CHANNEL_NAME || "#api-test"; 42 | upload(channel, process.argv[2]).then(function (json) { 43 | return console.log(json); 44 | }).catch(function (err) { 45 | return console.error(err); 46 | }); 47 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var babel = require('gulp-babel'); 3 | var src_dir = './src/*.js' 4 | 5 | gulp.task('default', ['compile', 'compile:watch']) 6 | gulp.task('test', ['mocha:env', 'mocha', 'mocha:watch']) 7 | gulp.task('lint', ['eslint', 'eslint:watch']) 8 | 9 | /** */ 10 | gulp.task('compile', function () { 11 | gulp.src(src_dir) 12 | .pipe(babel()) 13 | .pipe(gulp.dest('dist')); 14 | }); 15 | 16 | gulp.task('compile:watch', function(){ 17 | gulp.watch(src_dir, ['compile']); 18 | }); 19 | 20 | /** */ 21 | gulp.task('minify', function() { 22 | var uglify= require('gulp-uglify'); 23 | gulp.src(['./dist/**/*.js']) 24 | .pipe(uglify()) 25 | .pipe(gulp.dest('./build')) 26 | }); 27 | 28 | /** */ 29 | gulp.task('mocha', function() { 30 | var mocha = require('gulp-mocha'); 31 | var gutil = require('gulp-util'); 32 | gulp.src(['test/*.js'], {read: false}) 33 | .pipe(mocha({reporter: 'list', require: ["babel-core/register"]})) 34 | .on('error', gutil.log); 35 | }); 36 | 37 | gulp.task('mocha:watch', function() { 38 | gulp.watch(['src/**/*.js', 'test/**/*.js'], ['mocha']); 39 | }); 40 | 41 | gulp.task('mocha:env', function () { 42 | var env = require('gulp-env'); 43 | env({vars: {BABEL_ENV: "test"}}) 44 | }); 45 | 46 | /** */ 47 | gulp.task('eslint', function() { 48 | var eslint = require('gulp-eslint'); 49 | gulp.src(["src/**/*.js", "test/**/*.js"]) 50 | .pipe(eslint({useEslintrc: true})) 51 | .pipe(eslint.format()) 52 | .pipe(eslint.failAfterError()); 53 | }); 54 | 55 | gulp.task('eslint:watch', function() { 56 | gulp.watch(['src/**/*.js', "test/**/*.js"], ['lint']); 57 | }); 58 | 59 | /** */ 60 | //gulp.task('typecheck', function() { 61 | // var flow = require("gulp-flowtype"); 62 | // return gulp.src('./src/*.js') 63 | // .pipe(flow({ 64 | // all: false, 65 | // weak: false, 66 | // declarations: './lib', 67 | // killFlow: true, 68 | // beep: true, 69 | // abort: false 70 | // })) 71 | //}); 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist/index.js"); 2 | -------------------------------------------------------------------------------- /lib/modules.js: -------------------------------------------------------------------------------- 1 | 2 | declare module child_process { 3 | declare function spawn(a: string, args: Array): Process; 4 | } 5 | 6 | declare module "form-data" { 7 | declare class exports { 8 | append(key: string, val: any): void; 9 | } 10 | } 11 | 12 | declare module "node-fetch" { 13 | declare function exports(path: string, options: Object): Promise; 14 | } 15 | 16 | declare module "aws-sdk" { 17 | declare class CloudWatch { 18 | listMetrics(params: Object, callback: function): void; 19 | getMetricStatistics(params: Object, callback: function): void; 20 | } 21 | declare class EC2 { 22 | describeInstances(params: Object, callback: function): void; 23 | } 24 | declare class RDS { 25 | describeDBInstances(params: Object, callback: function): void; 26 | } 27 | declare class AWSConfig { 28 | update(conf: Object): void; 29 | } 30 | declare var config: AWSConfig; 31 | } 32 | 33 | // ignore 34 | declare module "moment" { 35 | declare function exports(): any; 36 | } 37 | 38 | declare module "minimist" { 39 | declare function exports(): any; 40 | } 41 | 42 | declare module "immutable" { 43 | declare class List { 44 | static (vals: Array): List; 45 | find(predicate: (value: T) => any): T; 46 | } 47 | declare class Set { 48 | static (vals: Array): Set; 49 | has(e: any): boolean; 50 | } 51 | } 52 | 53 | declare class Phantom { 54 | exit(i: number): void; 55 | exit(): void; 56 | } 57 | 58 | declare var phantom: Phantom; 59 | 60 | declare var require: {(s: string): any, main: Object} 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-cloudwatch-chart-slack", 3 | "version": "0.1.3", 4 | "description": "Chart Renderer for AWS CloudWatch Datapoints and Uploader to Slack", 5 | "keywords": ["AWS", "CloudWatch", "chart", "upload", "Slack"], 6 | "main": "index.js", 7 | "engines": { 8 | "node": "~4", 9 | "npm": "~2" 10 | }, 11 | "scripts": { 12 | "build": "gulp compile", 13 | "test": "BABEL_ENV=test mocha --require babel-core/register --recursive", 14 | "lint": "gulp eslint", 15 | "typecheck": "flow check" 16 | }, 17 | "author": "tmtk75", 18 | "license": "MIT", 19 | "dependencies": { 20 | "aws-sdk": "^2.2.30", 21 | "c3": "^0.4.11-rc4", 22 | "d3": "^3.5.17", 23 | "form-data": "^1.0.0-rc4", 24 | "immutable": "^3.7.6", 25 | "minimist": "^1.2.0", 26 | "moment": "^2.11.1", 27 | "node-fetch": "^1.3.3", 28 | "phantomjs": "^2.1.7" 29 | }, 30 | "devDependencies": { 31 | "babel-core": "^6.3.21", 32 | "babel-eslint": "^5.0.0-beta6", 33 | "babel-loader": "^6.2.0", 34 | "babel-plugin-espower": "^2.1.0", 35 | "babel-plugin-transform-flow-strip-types": "^6.4.0", 36 | "babel-preset-es2015": "^6.3.13", 37 | "babel-preset-stage-0": "^6.3.13", 38 | "eslint": "^1.10.3", 39 | "eslint-plugin-babel": "^3.0.0", 40 | "gulp": "^3.9.0", 41 | "gulp-babel": "^6.1.1", 42 | "gulp-env": "^0.4.0", 43 | "gulp-eslint": "^1.1.1", 44 | "gulp-mocha": "^2.2.0", 45 | "gulp-uglify": "^1.5.1", 46 | "gulp-util": "^3.0.7", 47 | "mocha": "^2.3.4", 48 | "power-assert": "^1.2.0" 49 | }, 50 | "directories": { 51 | "bin": "./bin" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/tmtk75/aws-cloudwatch-chart-slack" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {slack} from "./index.js" 3 | 4 | const channel_name = process.env.SLACK_CHANNEL_NAME || "#api-test" 5 | slack.post(channel_name, process.argv.slice(2), (err, file) => { 6 | if (err) { 7 | console.error(err.stack); 8 | return 9 | } 10 | console.log(file.thumb_80); 11 | }) 12 | -------------------------------------------------------------------------------- /src/cloudwatch.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import AWS from "aws-sdk" 3 | import time from "./time.js" 4 | import moment from "moment" 5 | import { 6 | nsToDimName, 7 | searchMetric, 8 | } from "./metrics.js" 9 | 10 | class CloudWatch { 11 | _endTime: string; 12 | _duration: string; 13 | _period: number; 14 | _statistics: string; 15 | 16 | /** */ 17 | endTime(d: string): CloudWatch { 18 | this._endTime = d; 19 | return this; 20 | } 21 | 22 | /** */ 23 | duration(d: string): CloudWatch { 24 | this._duration = d; 25 | return this; 26 | } 27 | 28 | /** */ 29 | period(p: number): CloudWatch { 30 | this._period = p; 31 | return this; 32 | } 33 | 34 | /** */ 35 | statistics(name: string): CloudWatch { 36 | this._statistics = name; 37 | return this; 38 | } 39 | 40 | /** */ 41 | metricStatistics(namespace: string, instanceID: string, metricName: string): Promise { 42 | let dimName = nsToDimName(namespace); 43 | let metric = searchMetric(namespace, metricName); 44 | let sep = time.toSEP(this._duration, this._endTime); 45 | if (this._period) { 46 | sep.Period = this._period; 47 | } 48 | if (this._statistics) { 49 | metric.Statistics = [ this._statistics ] 50 | } 51 | 52 | let params = { 53 | ...sep, 54 | ...metric, 55 | Namespace: namespace, 56 | Dimensions: [ 57 | { 58 | Name: dimName, 59 | Value: instanceID, 60 | }, 61 | ], 62 | }; 63 | 64 | //process.stderr.write(JSON.stringify(params)); 65 | let cloudwatch = new AWS.CloudWatch(); 66 | return new Promise((resolve, reject) => 67 | cloudwatch.getMetricStatistics(params, (err, data) => err ? reject(err) : resolve([sep, data])) 68 | ) 69 | } 70 | } 71 | 72 | export default CloudWatch; 73 | 74 | -------------------------------------------------------------------------------- /src/dynamodb.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as m from "./metrics.js" 3 | 4 | /** 5 | * Returns true if stat is exactly in a condition. 6 | */ 7 | export function mimic(stat: Object): boolean { 8 | return stat.Namespace === "AWS/DynamoDB" 9 | && stat.Period === 60 10 | && (stat.Label === "ConsumedReadCapacityUnits" || stat.Label === "ConsumedWriteCapacityUnits") 11 | && (stat.Datapoints[0].Sum !== undefined) 12 | } 13 | 14 | export function toY(e: Object, bytes: boolean = false): number { 15 | return m.toY(e, bytes) / 60; 16 | } 17 | 18 | export default { 19 | mimic, 20 | toY, 21 | } 22 | -------------------------------------------------------------------------------- /src/gen-chart.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {system, fs, webpage} from "./phantom-api.js" 3 | import moment from "moment" 4 | import {toMax, toMin, toAxisYLabel, toY, nsToDimName, to_axis_x_label_text} from "./metrics.js" 5 | import dynamodb from "./dynamodb.js" 6 | 7 | const argv = require("minimist")(system.args.slice(1), { 8 | string: ["filename", "width", "height", "max", "min", "node_modules_path", 9 | "x-label", "format", "bindto", "point-r"], 10 | boolean: ["base64", "keep-html", "keep-js", "grid-x", "grid-y", "utc", "bytes", "without-image"], 11 | alias: { 12 | f: "filename", 13 | }, 14 | default: { 15 | width: 800, 16 | height: 300, 17 | node_modules_path: "./node_modules", 18 | format: "png", 19 | "x-tick-count": 120, 20 | "x-tick-culling-max": 10, 21 | "bindto": "container", 22 | "point-r": 2.5, 23 | } 24 | }); 25 | 26 | try { 27 | const stats_data = JSON.parse(system.stdin.read()) 28 | const repre = stats_data[0] 29 | const MetricName = repre.Label || "" 30 | const Namespace = repre.Namespace || "" 31 | const sort = (datapoints) => datapoints.sort((a, b) => a.Timestamp.localeCompare(b.Timestamp)) 32 | const yData = stats_data.map(stats => { 33 | if (stats.Datapoints.length < 2) { 34 | throw new Error(`Number of datapoints is less than 2 for ${MetricName} of ${stats.InstanceId}. There is a possibility InstanceId was wrong. ${JSON.stringify(stats)}`) 35 | } 36 | let b = dynamodb.mimic(stats) 37 | return [stats[nsToDimName(Namespace)]].concat(sort(stats.Datapoints) 38 | .map(e => b ? dynamodb.toY(e) : toY(e, argv.bytes))) 39 | }) 40 | const textLabelX = to_axis_x_label_text(repre, argv.utc) 41 | 42 | const data = { 43 | _meta: {StartTime: repre.StartTime, EndTime: repre.EndTime, UTC: argv.utc}, 44 | bindto: `#${argv.bindto}`, 45 | data: { 46 | x: "x", 47 | columns: [ 48 | ["x"].concat(sort(repre.Datapoints).map(e => moment.utc(e["Timestamp"]).valueOf())), 49 | ].concat(yData), 50 | //colors: { 51 | // [axisXLabel]: (Namespace === "AWS/EC2" ? "#f58536" : null), 52 | //}, 53 | }, 54 | transition: { 55 | duration: null, // 56 | }, 57 | size: { 58 | width: argv.width - 16, // heuristic adjustments 59 | height: argv.height - 16, 60 | }, 61 | axis: { 62 | y: { 63 | max: (argv.max ? parseInt(argv.max) : toMax(repre)), 64 | min: (argv.min ? parseInt(argv.min) : toMin(repre)), 65 | padding: {top: 0, bottom: 0}, 66 | label: { 67 | text: `${Namespace} ${MetricName} ${toAxisYLabel(repre, argv.bytes)}`, 68 | position: "outer-middle", 69 | }, 70 | //tick: { 71 | // format: d3.format('$,'), 72 | //} 73 | }, 74 | x: { 75 | type: "timeseries", 76 | tick: { 77 | count: argv["x-tick-count"], 78 | culling: { 79 | max: argv["x-tick-culling-max"], 80 | }, 81 | _format: "%Y-%m-%dT%H:%M:%S", 82 | format: "%H:%M", 83 | }, 84 | //padding: {left: 0, right: 0}, 85 | label: { 86 | text: (argv["x-label"] || textLabelX), 87 | position: "outer-center", 88 | }, 89 | localtime: !argv.utc, 90 | }, 91 | }, 92 | grid: { 93 | x: { 94 | show: argv["grid-x"], 95 | }, 96 | y: { 97 | show: argv["grid-y"], 98 | } 99 | }, 100 | point: { 101 | r: argv["point-r"], 102 | }, 103 | } 104 | 105 | render(argv, data); 106 | } catch (ex) { 107 | system.stderr.write(ex.stack); 108 | system.stderr.write("\n"); 109 | phantom.exit(1); 110 | } 111 | 112 | /* 113 | * Rendering 114 | */ 115 | function render(argv: Object, data: Object): void { 116 | const page = webpage.create() 117 | page.onConsoleMessage = (msg) => console.log(msg) 118 | page.viewportSize = { 119 | width: argv.width ? parseInt(argv.width) : page.viewportSize.width, 120 | height: argv.height ? parseInt(argv.height) : page.viewportSize.height, 121 | } 122 | //console.log(JSON.stringify(page.viewportSize)) 123 | 124 | const suffix = argv.filename || `.${system.pid}-${new Date().getTime()}` 125 | const tmp_html = `./${suffix}.html` 126 | const tmp_js = `./${suffix}.js` 127 | const filename = argv.filename || `./${suffix}.png` 128 | const node_modules_path = argv.node_modules_path; 129 | 130 | const now = moment().format("YYYY-MM-DD HH:mm:ss Z") 131 | fs.write(tmp_js, ` 132 | // Generated at ${now} 133 | var data = ${JSON.stringify(data)}; 134 | data.axis.y.tick = {format: d3.format(',')}; 135 | c3.generate(data); 136 | `) 137 | fs.write(tmp_html, ` 138 | 139 | 140 | 141 | 142 | 143 | 144 |
145 | 146 | 147 | 148 | `) 149 | 150 | page.open(tmp_html, (status) => { 151 | //console.error(JSON.stringify(argv)) 152 | if (!argv["without-image"]) { 153 | page.render(filename, {format: argv.format}); 154 | system.stdout.write(filename) 155 | } else if (argv.base64) { 156 | system.stdout.write(page.renderBase64(argv.format)) 157 | } 158 | if (!argv["keep-html"]) { 159 | fs.remove(tmp_html); 160 | } 161 | if (!argv["keep-js"]) { 162 | fs.remove(tmp_js); 163 | } 164 | phantom.exit() 165 | }) 166 | } 167 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @flow 3 | import fs from "fs" 4 | import {render} from "./render.js" 5 | import {upload} from "./upload.js" 6 | import {ls_ec2} from "./ls-ec2.js" 7 | import {proc_gen_chart} from "./proc-gen-chart.js" 8 | 9 | function unlink(path: string): Promise { 10 | return new Promise((resolve, reject) => fs.unlink(path, (err) => err ? reject(err) : resolve(path))) 11 | } 12 | 13 | function post(channel: string, args: Array, callback: Function): Promise { 14 | let cb_ok = callback || ((err, data) => console.log(data)) 15 | let cb_err = callback || ((err, data) => console.error(err)) 16 | return render(args) 17 | .then(path => Promise.all([ 18 | upload(channel, path), 19 | unlink(path) 20 | ])) 21 | .then(([{file}, path]) => cb_ok(null, file)) 22 | .catch(err => cb_err(err)) 23 | } 24 | 25 | module.exports = { 26 | slack: { 27 | post, 28 | }, 29 | ls_ec2, 30 | render, 31 | proc_gen_chart, 32 | } 33 | -------------------------------------------------------------------------------- /src/ls-ec2.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import AWS from "aws-sdk" 3 | import I from "immutable" 4 | 5 | /** */ 6 | function describe_instances(filters: Array = []): Promise { 7 | //AWS.config.update({region: "ap-northeast-1"}) 8 | let ec2 = new AWS.EC2(); 9 | let params = { 10 | Filters: [ 11 | {Name:"instance-state-name", Values:["running"]}, 12 | ...filters, 13 | ], 14 | } 15 | return new Promise((resolve, reject) => ec2.describeInstances(params, (err, data) => err ? reject(err) : resolve(data))) 16 | .then(r => r.Reservations.map(e => e.Instances[0])) 17 | .then(r => r.map(e => ({ 18 | InstanceId: e.InstanceId, 19 | InstanceType: e.InstanceType, 20 | Name: (I.List(e.Tags).find(e => e.Key === "Name") || {}).Value, 21 | }))) 22 | } 23 | 24 | /** 25 | * in: tag:site=dev,role=webapp|db 26 | * 27 | * out: [ 28 | * {Name: "tag:site", Values: ["dev"]}, 29 | * {Name: "tag:role", Values: ["webapp", "db"]}, 30 | * ] 31 | */ 32 | export function to_filters(s: string): Array { 33 | return s.trim() 34 | .split(",") 35 | .map(e => e.trim().split("=")) 36 | .map(([k, v]) => ({Name:`tag:${k}`, Values:[...v.split("|")]})) 37 | } 38 | 39 | export function tag_string_to_filters(s: string): Array { 40 | if (!s) 41 | return [] 42 | let a = s.match("^tag:(.*)") 43 | return a ? to_filters(a[1]) : [] 44 | } 45 | 46 | export function ls_ec2(f: string): Promise { 47 | return describe_instances(to_filters(f)) 48 | .then(e => e.map(s => s.InstanceId)) 49 | .then(e => e.join(",")) 50 | } 51 | 52 | import minimist from "minimist" 53 | if (require.main === module) { 54 | let argv = minimist(process.argv.slice(2)) 55 | AWS.config.update({region: argv.region || process.env.AWS_DEFAULT_REGION || "ap-northeast-1"}) 56 | describe_instances(tag_string_to_filters(argv._[0])) 57 | .then(r => console.log(JSON.stringify(r, null, 2))) 58 | .catch(err => console.error(err.stack)) 59 | } 60 | -------------------------------------------------------------------------------- /src/ls-rds.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import AWS from "aws-sdk" 3 | import I from "immutable" 4 | import * as ec2 from "./ls-ec2.js" 5 | 6 | /** */ 7 | function describe_db_instances(filters: Array = []): Promise { 8 | //AWS.config.update({region: "ap-northeast-1"}) 9 | let rds = new AWS.RDS(); 10 | let params = { 11 | Filters: [ 12 | ...filters, 13 | ], 14 | } 15 | return new Promise((resolve, reject) => rds.describeDBInstances(params, (err, data) => err ? reject(err) : resolve(data))) 16 | .then(r => r) 17 | } 18 | 19 | export function ls_rds(f: string): Promise { 20 | return describe_db_instances(ec2.to_filters(f)) 21 | .then(e => e.map(s => s.InstanceId)) 22 | .then(e => e.join(",")) 23 | } 24 | 25 | import minimist from "minimist" 26 | if (require.main === module) { 27 | let argv = minimist(process.argv.slice(2)) 28 | AWS.config.update({region: argv.region || process.env.AWS_DEFAULT_REGION || "ap-northeast-1"}) 29 | describe_db_instances(ec2.tag_string_to_filters(argv._[0])) 30 | .then(r => console.log(JSON.stringify(r, null, 2))) 31 | .catch(err => console.error(err.stack)) 32 | } 33 | -------------------------------------------------------------------------------- /src/metrics.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import I from "immutable" 3 | import moment from "moment" 4 | 5 | const metricsRDS = [ 6 | { MetricName: "BinLogDiskUsage", Statistics: [ "Maximum" ] }, 7 | { MetricName: "CPUUtilization", Statistics: [ "Average" ] }, 8 | { MetricName: "DatabaseConnections", Statistics: [ "Maximum" ] }, 9 | { MetricName: "DiskQueueDepth", Statistics: [ "Maximum" ] }, 10 | { MetricName: "FreeStorageSpace", Statistics: [ "Minimum" ] }, 11 | { MetricName: "FreeableMemory", Statistics: [ "Minimum" ] }, 12 | { MetricName: "NetworkReceiveThroughput", Statistics: [ "Maximum" ] }, 13 | { MetricName: "NetworkTransmitThroughput", Statistics: [ "Maximum" ] }, 14 | { MetricName: "ReadIOPS", Statistics: [ "Maximum" ] }, 15 | { MetricName: "ReadLatency", Statistics: [ "Maximum" ] }, 16 | { MetricName: "ReadThroughput", Statistics: [ "Maximum" ] }, 17 | { MetricName: "SwapUsage", Statistics: [ "Maximum" ] }, 18 | { MetricName: "WriteIOPS", Statistics: [ "Maximum" ] }, 19 | { MetricName: "WriteLatency", Statistics: [ "Maximum" ] }, 20 | { MetricName: "WriteThroughput", Statistics: [ "Maximum" ] }, 21 | ] 22 | 23 | const metricsEC2 = [ 24 | { MetricName: "CPUCreditUsage", Statistics: [ "Maximum" ] }, 25 | { MetricName: "CPUCreditBalance", Statistics: [ "Maximum" ] }, 26 | { MetricName: "CPUUtilization", Statistics: [ "Average" ] }, 27 | { MetricName: "DiskReadOps", Statistics: [ "Maximum" ] }, 28 | { MetricName: "DiskWriteOps", Statistics: [ "Maximum" ] }, 29 | { MetricName: "DiskReadBytes", Statistics: [ "Maximum" ] }, 30 | { MetricName: "DiskWriteBytes", Statistics: [ "Maximum" ] }, 31 | { MetricName: "NetworkIn", Statistics: [ "Maximum" ] }, 32 | { MetricName: "NetworkOut", Statistics: [ "Maximum" ] }, 33 | { MetricName: "StatusCheckFailed", Statistics: [ "Maximum" ] }, 34 | { MetricName: "StatusCheckFailed_Instance", Statistics: [ "Maximum" ] }, 35 | { MetricName: "StatusCheckFailed_System", Statistics: [ "Maximum" ] }, 36 | ] 37 | 38 | const metricsDynamoDB = [ 39 | { MetricName: "ConsumedReadCapacityUnits", Statistics: [ "Sum" ] }, 40 | { MetricName: "ConsumedWriteCapacityUnits", Statistics: [ "Sum" ] }, 41 | { MetricName: "ProvisionedReadCapacityUnits", Statistics: [ "Maximum" ] }, 42 | { MetricName: "ProvisionedWriteCapacityUnits", Statistics: [ "Maximum" ] }, 43 | { MetricName: "ConditionalCheckFailedRequests", Statistics: [ "Maximum" ] }, 44 | { MetricName: "OnlineIndexConsumedWriteCapacity", Statistics: [ "Maximum" ] }, 45 | { MetricName: "OnlineIndexPercentageProgress", Statistics: [ "Maximum" ] }, 46 | { MetricName: "OnlineIndexThrottleEvents", Statistics: [ "Maximum" ] }, 47 | { MetricName: "ReturnedItemCount", Statistics: [ "Maximum" ] }, 48 | { MetricName: "SuccessfulRequestLatency", Statistics: [ "Maximum" ] }, 49 | { MetricName: "SystemErrors", Statistics: [ "Maximum" ] }, 50 | { MetricName: "ThrottledRequests", Statistics: [ "Maximum" ] }, 51 | { MetricName: "UserErrors", Statistics: [ "Maximum" ] }, 52 | { MetricName: "WriteThrottleEvents", Statistics: [ "Maximum" ] }, 53 | { MetricName: "ReadThrottleEvents", Statistics: [ "Sum" ] }, 54 | ] 55 | 56 | "Seconds | Microseconds | Milliseconds | Bytes | Kilobytes | Megabytes | Gigabytes | Terabytes | Bits | Kilobits | Megabits | Gigabits | Terabits | Percent | Count | Bytes/Second | Kilobytes/Second | Megabytes/Second | Gigabytes/Second | Terabytes/Second | Bits/Second | Kilobits/Second | Megabits/Second | Gigabits/Second | Terabits/Second | Count/Second | None" 57 | "Minimum | Maximum | Sum | Average | SampleCount" 58 | 59 | export const METRICS = { 60 | "AWS/EC2": metricsEC2, 61 | "AWS/RDS": metricsRDS, 62 | "AWS/DynamoDB": metricsDynamoDB, 63 | } 64 | 65 | type Metric = { 66 | MetricName: string; 67 | Statistics: Array; 68 | } 69 | 70 | type Datapoint = { 71 | Timestamp: string; 72 | Unit: string; 73 | } 74 | 75 | type Metrics = { 76 | Datapoints: Array; 77 | } 78 | 79 | export function searchMetric(ns: string, metricName: string): Metric { 80 | let a = METRICS[ns].filter(({MetricName: n}) => n.match(new RegExp(metricName, "i"))) 81 | return a[0] 82 | } 83 | 84 | export function nsToDimName(ns: string): string { 85 | return ({ 86 | "AWS/RDS": "DBInstanceIdentifier", 87 | "AWS/EC2": "InstanceId", 88 | "AWS/DynamoDB": "TableName", 89 | })[ns] 90 | } 91 | 92 | export function toMax(metrics: Metrics): ?number { 93 | if (!metrics.Datapoints[0]) { 94 | return null; 95 | } 96 | if (metrics.Datapoints[0].Unit === "Percent") { 97 | return 100.0; 98 | } 99 | return null; 100 | } 101 | 102 | export function toMin(metrics: Metrics): ?number { 103 | if (!metrics.Datapoints[0]) { 104 | return null; 105 | } 106 | if (metrics.Datapoints[0].Unit === "Percent") { 107 | return 0.0; 108 | } 109 | return null; 110 | } 111 | 112 | export function toAxisYLabel(metrics: Metrics, bytes: boolean): string { 113 | if (metrics.Datapoints[0].Unit === "Bytes" && !bytes) { 114 | return "Megabytes" 115 | } 116 | return metrics.Datapoints[0].Unit 117 | } 118 | 119 | export function toY(metric: Object, bytes: boolean): number { 120 | let e = metric["Maximum"] || metric["Average"] || metric["Minimum"]|| metric["Sum"] || metric["SampleCount"] 121 | if (metric.Unit === "Bytes" && !bytes) { 122 | return e / (1024 * 1024) // Megabytes 123 | } 124 | return e || 0 125 | } 126 | 127 | const _stats = I.Set(["Maximum", "Average", "Minimum", "Sum", "SampleCount"]) 128 | export function find_stat_name(datapoints: Array): ?string { 129 | if (!(datapoints && datapoints.length > 0)) 130 | return null 131 | let dp = datapoints[0]; 132 | return I.List(Object.keys(dp)).find(e => _stats.has(e)) 133 | } 134 | 135 | export function calc_period(datapoints: Array, measurement: string = "minutes"): ?number { 136 | if (!(datapoints && datapoints.length > 1)) 137 | return null 138 | let [a, b] = datapoints.sort((a, b) => a.Timestamp.localeCompare(b.Timestamp)) 139 | return moment(b.Timestamp).diff(moment(a.Timestamp), measurement) 140 | } 141 | 142 | export function to_axis_x_label_text(stats: Object, utc: boolean): string { 143 | let et = moment(stats.EndTime) 144 | let diff = moment(stats.StartTime).diff(et) 145 | let tz = utc ? "UTC" : (new Date().getTimezoneOffset() / 60) + "h" 146 | 147 | let ago = moment.duration(diff).humanize() 148 | let from = (utc ? et.utc() : et).format("YYYY-MM-DD HH:mm") 149 | return `${find_stat_name(stats.Datapoints)} every ${calc_period(stats.Datapoints)}minutes from ${from} (tz:${tz}) to ${ago} ago` 150 | } 151 | 152 | // 153 | if (require.main === module) { 154 | console.log(JSON.stringify(METRICS, null, 2)); 155 | } 156 | -------------------------------------------------------------------------------- /src/phantom-api.js: -------------------------------------------------------------------------------- 1 | import system from "system" 2 | import fs from "fs" 3 | import webpage from "webpage" 4 | 5 | export { 6 | system, 7 | fs, 8 | webpage, 9 | } 10 | -------------------------------------------------------------------------------- /src/print-stats.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import AWS from "aws-sdk" 3 | import CloudWatch from "./cloudwatch.js" 4 | import {nsToDimName} from "./metrics.js" 5 | import {toSeconds} from "./time.js" 6 | import {ls_ec2} from "./ls-ec2.js" 7 | import path from "path" 8 | 9 | type Params = { 10 | _: Array; 11 | region: string; 12 | duration: string; 13 | statistics: string; 14 | "end-time": string; 15 | } 16 | 17 | function print_stats(argv: Params): Promise { 18 | //let [ instID, metricName = 'CPUUtilization', ns = 'AWS/EC2'] = argv._; // flow doesn't support 19 | let instIDs = argv._[0] 20 | let metricName = argv._[1] || "CPUUtilization" 21 | let ns = argv._[2] || "AWS/EC2" 22 | let dimName = nsToDimName(ns) 23 | let region = argv.region || process.env.AWS_DEFAULT_REGION || "ap-northeast-1" 24 | let period = toSeconds(argv.period || "30minutes") 25 | let stats = argv.statistics 26 | 27 | if (!instIDs) { 28 | //throw new Error("instanceID is missing") 29 | return Promise.reject(new Error("InstanceId is missing")) 30 | } 31 | 32 | AWS.config.update({region}); 33 | 34 | let watch = (instanceID) => 35 | new CloudWatch() 36 | .endTime(argv["end-time"]) 37 | .duration(argv.duration || "1day") 38 | .period(period) 39 | .statistics(stats) 40 | .metricStatistics(ns, instanceID, metricName) 41 | .then(([sep, data]) => ({ 42 | Namespace: ns, 43 | [dimName]: instanceID, 44 | ...sep, 45 | ...data, 46 | })) 47 | 48 | let a = instIDs.match("^tag:(.*)") 49 | if (a && ns === "AWS/RDS") 50 | return Promise.reject(new Error("filters is not supported AWS/RDS")) 51 | 52 | let p = (a ? ls_ec2(a[1]) : Promise.resolve(instIDs)) 53 | return p.then(s => s.split(",")) 54 | .then(s => Promise.all(s.map(e => watch(e.trim())))) 55 | } 56 | 57 | module.exports = { 58 | print_stats, 59 | } 60 | 61 | if (require.main === module) { 62 | print_stats(require("minimist")(process.argv.slice(2))) 63 | .then(r => process.stdout.write(JSON.stringify(r))) 64 | .catch(err => { 65 | //process.stderr.write(JSON.stringify(err, null, 2)); 66 | console.error(err); 67 | process.exit(1); 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /src/proc-gen-chart.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {spawn} from "child_process" 3 | import path from "path" 4 | 5 | // workaround: prod.stdin.write doesn't pass for flow check 6 | function write_to_stdin(proc: any, obj) { 7 | proc.stdin.write(JSON.stringify(obj)) 8 | proc.stdin.end() 9 | } 10 | 11 | export function proc_gen_chart(args) { 12 | return r => new Promise((resolve, reject) => { 13 | let cmd = path.join(__dirname, "../node_modules/.bin/", "phantomjs") 14 | let js = path.join(__dirname, "gen-chart.js") 15 | let nmp = path.join(__dirname, "../node_modules") 16 | let p = spawn(cmd, [js, "--node_modules_path", nmp].concat(args)) 17 | write_to_stdin(p, r); 18 | var buf = "" 19 | var errbuf = "" 20 | p.stdout.on("data", (data) => buf += `${data}`); // filename: String 21 | p.stderr.on("data", (data) => errbuf += `${data}`); 22 | p.on("close", (code) => code == 0 ? resolve(buf) : reject(new Error(errbuf))); 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import minimist from "minimist" 3 | import {print_stats} from "./print-stats.js" 4 | import {proc_gen_chart} from "./proc-gen-chart.js" 5 | 6 | function render(args: Array): Promise { 7 | return print_stats(minimist(args)) 8 | .then(proc_gen_chart(args)) 9 | } 10 | 11 | module.exports = { 12 | render, 13 | } 14 | 15 | if (require.main === module) { 16 | render(process.argv.slice(2)) 17 | .then(r => console.log(r)) 18 | .catch(err => console.error(err.stack)) 19 | } 20 | -------------------------------------------------------------------------------- /src/time.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import moment from "moment" 3 | 4 | type SEP = { 5 | EndTime: Date; 6 | StartTime: Date; 7 | Period: number; 8 | } 9 | 10 | /* 11 | * Start End Period 12 | */ 13 | export function toSEP(reltime: string = "1day", end: string = ""): SEP { 14 | let a = reltime.match(/([0-9]+) *(.*)/) 15 | if (!a) { 16 | throw new Error(reltime) 17 | } 18 | let [_, n, m] = a 19 | let endTime = end ? moment(end) : moment() 20 | //console.error(endTime); 21 | let duration = moment.duration(parseInt(n), m) 22 | let startTime = endTime.clone().subtract(duration) 23 | let period = 60 * 30 24 | 25 | return { 26 | EndTime: endTime.toDate(), 27 | StartTime: startTime.toDate(), 28 | Period: period, 29 | } 30 | } 31 | 32 | export function toSeconds(period: string): number { 33 | let a = period.match(/([0-9]+)(.+)/); 34 | if (!a) { 35 | return NaN 36 | } 37 | let [_, n, u] = a; 38 | return moment.duration(parseInt(n), u).asSeconds(); 39 | } 40 | 41 | export default { 42 | toSEP, 43 | toSeconds, 44 | } 45 | -------------------------------------------------------------------------------- /src/upload.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @flow 3 | var fs = require("fs"); 4 | var fetch = require("node-fetch"); 5 | var FormData = require("form-data"); 6 | 7 | function upload(channel: string, path: string): Promise { 8 | return new Promise((resolve, reject) => { 9 | var form = new FormData(); 10 | form.append("channels", channel); 11 | form.append("token", process.env.SLACK_API_TOKEN); 12 | //form.append("file", fs.createReadStream(path)); //NOTE: This became to fail somehow suddenly... https://github.com/form-data/form-data#notes 13 | var buf = fs.readFileSync(path); 14 | form.append("file", buf, { 15 | filename: path, 16 | contentType: "image/png", 17 | knownLength: buf.length, 18 | }); 19 | 20 | return fetch("https://slack.com/api/files.upload", {method: "POST", body: form}) 21 | .then(res => 22 | /* 23 | * {ok: true, file: {...}} See https://api.slack.com/types/file 24 | */ 25 | res.json() 26 | ) 27 | .then(e => e.ok ? resolve(e) : reject(new Error(JSON.stringify(e)))) 28 | .catch(err => reject(err)) 29 | }) 30 | } 31 | 32 | module.exports = { 33 | upload, 34 | } 35 | 36 | if (require.main === module) { 37 | let channel = process.env.SLACK_CHANNEL_NAME || "#api-test" 38 | upload(channel, process.argv[2]) 39 | .then(json => console.log(json)) 40 | .catch(err => console.error(err)); 41 | } 42 | 43 | -------------------------------------------------------------------------------- /test/dynamodb.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert" 2 | import { 3 | toY, 4 | mimic, 5 | } from "../src/dynamodb.js" 6 | 7 | describe("dynamodb", () => { 8 | 9 | describe("toY", () => { 10 | it ("returns value divided by 60", () => { 11 | assert(toY({Sum: 60}) === 1) 12 | }) 13 | }) 14 | 15 | describe("mimic", () => { 16 | 17 | describe("for 60 seconds duration", () => { 18 | it ("returns true for ConsumedReadCapacityUnits", () => { 19 | assert(mimic({ 20 | Namespace: "AWS/DynamoDB", 21 | Period: 60, 22 | Label: "ConsumedReadCapacityUnits", 23 | Datapoints: [ 24 | {Sum: 1}, 25 | {Sum: 2}, 26 | {Sum: 2}, 27 | ], 28 | }) === true) 29 | }) 30 | 31 | it ("returns true for ConsumedWriteCapacityUnits", () => { 32 | assert(mimic({ 33 | Namespace: "AWS/DynamoDB", 34 | Period: 60, 35 | Label: "ConsumedWriteCapacityUnits", 36 | Datapoints: [ 37 | {Sum: 2}, 38 | {Sum: 2}, 39 | ], 40 | }) === true) 41 | }) 42 | 43 | }) 44 | 45 | describe("for Average", () => { 46 | it ("returns false for whatever", () => { 47 | assert(!mimic({ 48 | Namespace: "AWS/DynamoDB", 49 | Label: "ConsumedWriteCapacityUnits", 50 | Datapoints: [ 51 | {Average: 10}, 52 | {Average: 20}, 53 | ], 54 | })) 55 | }) 56 | }) 57 | 58 | describe("for 120 seconds period", () => { 59 | it ("returns false for whatever", () => { 60 | assert(!mimic({ 61 | Namespace: "AWS/DynamoDB", 62 | Period: 120, 63 | Label: "ConsumedWriteCapacityUnits", 64 | Datapoints: [ 65 | {Sum: 1234}, 66 | {Sum: 1238}, 67 | ], 68 | })) 69 | }) 70 | }) 71 | 72 | describe("for EC2", () => { 73 | it ("returns false", () => { 74 | assert(!mimic({ 75 | Namespace: "AWS/EC2", 76 | Period: 60, 77 | Label: "CPUUtilization", 78 | Datapoints: [ 79 | {Sum: 5}, 80 | {Sum: 7}, 81 | ], 82 | })) 83 | }) 84 | }) 85 | }) 86 | 87 | }) 88 | -------------------------------------------------------------------------------- /test/ls-ec2.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert" 2 | import { 3 | tag_string_to_filters, 4 | } from "../src/ls-ec2.js" 5 | 6 | describe("ls-ec2", () => { 7 | 8 | describe("tag_string_to_filters", () => { 9 | 10 | it ("undefined", () => { 11 | assert.notStrictEqual(tag_string_to_filters(undefined), []) 12 | }) 13 | 14 | it ("null", () => { 15 | assert.notStrictEqual(tag_string_to_filters(null), []) 16 | }) 17 | 18 | it ("empty string", () => { 19 | assert.notStrictEqual(tag_string_to_filters(""), []) 20 | }) 21 | 22 | it ("not match", () => { 23 | assert.notStrictEqual(tag_string_to_filters("i-xxxxyyyy"), []) 24 | }) 25 | 26 | it ("single tag", () => { 27 | let [a] = tag_string_to_filters("tag:role=db") 28 | assert(a.Name === "tag:role") 29 | assert(a.Values.length === 1) 30 | assert(a.Values[0] === "db") 31 | }) 32 | 33 | it ("two tags", () => { 34 | let [a, b] = tag_string_to_filters("tag:site=dev,role=webapp|db") 35 | assert(a.Name === "tag:site") 36 | assert(a.Values.length === 1) 37 | assert(a.Values[0] === "dev") 38 | assert(b.Name === "tag:role") 39 | assert(b.Values.length === 2) 40 | assert(b.Values[0] === "webapp") 41 | assert(b.Values[1] === "db") 42 | }) 43 | 44 | }) 45 | 46 | }) 47 | -------------------------------------------------------------------------------- /test/metrics.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert" 2 | import { 3 | searchMetric, 4 | find_stat_name, 5 | calc_period, 6 | toY, 7 | } from "../src/metrics.js" 8 | 9 | describe("metrics", () => { 10 | 11 | describe("searchMetric", () => { 12 | 13 | it ("returns CPUCreditUsage", () => { 14 | assert(searchMetric("AWS/EC2", "cpu").MetricName === "CPUCreditUsage") 15 | }) 16 | 17 | it ("returns CPUUtilization", () => { 18 | assert(searchMetric("AWS/EC2", "cpuu").MetricName === "CPUUtilization") 19 | }) 20 | 21 | it ("returns FreeStorageSpace", () => { 22 | assert(searchMetric("AWS/RDS", "free").MetricName === "FreeStorageSpace") 23 | }) 24 | 25 | it ("returns FreeableMemory", () => { 26 | assert(searchMetric("AWS/RDS", "freeable").MetricName === "FreeableMemory") 27 | }) 28 | 29 | }) 30 | 31 | describe("find_stat_name", () => { 32 | 33 | it ("returns null", () => { 34 | assert(find_stat_name([]) === null) 35 | }) 36 | 37 | it ("returns Maximum", () => { 38 | assert(find_stat_name([{Maximum: 0}]) === "Maximum") 39 | }) 40 | 41 | it ("returns Average", () => { 42 | assert(find_stat_name([{Average: 0}]) === "Average") 43 | }) 44 | 45 | it ("returns Total", () => { 46 | assert(find_stat_name([{Total: 0}]) === undefined) 47 | }) 48 | 49 | }) 50 | 51 | describe("calc_period", () => { 52 | 53 | it ("returns null", () => { 54 | assert(calc_period([]) === null) 55 | }) 56 | 57 | it ("returns 60 for 1h duration", () => { 58 | let a = {Timestamp: "2016-01-31T15:35:00.000Z"} 59 | let b = {Timestamp: "2016-01-31T16:35:00.000Z"} 60 | assert(calc_period([a, b]) === 60) 61 | }) 62 | 63 | it ("returns 1 for 1h duration if specifying measurement", () => { 64 | let a = {Timestamp: "2016-01-31T15:35:00.000Z"} 65 | let b = {Timestamp: "2016-01-31T16:35:00.000Z"} 66 | assert(calc_period([a, b], "hours") === 1) 67 | }) 68 | 69 | }) 70 | 71 | describe("toY", () => { 72 | 73 | it ("returns Megabytes", () => { 74 | assert(toY({Maximum: 60 * 1024 * 1024, Unit: "Bytes"}) === 60) 75 | }) 76 | 77 | it ("returns Bytes", () => { 78 | assert(toY({Maximum: 60, Unit: "Bytes"}, true) === 60) 79 | }) 80 | 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/time.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert" 2 | import { 3 | toSeconds, 4 | } from "../src/time.js" 5 | 6 | describe("time", () => { 7 | 8 | describe("toSeconds", () => { 9 | 10 | it ("returns 60 for 1minute", () => { 11 | assert(toSeconds("1minute") === 60) 12 | }) 13 | 14 | it ("returns 120 for 2minutes", () => { 15 | assert(toSeconds("2minutes") === 120) 16 | }) 17 | 18 | }) 19 | 20 | }) 21 | --------------------------------------------------------------------------------