├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── lib ├── check.go ├── print.go └── utils.go ├── main.go └── renovate.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | vendor 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - '1.12' 4 | env: 5 | - PATH=/home/travis/gopath/bin:$PATH GO111MODULE=on 6 | script: go test 7 | before_deploy: 8 | - go get -v github.com/mitchellh/gox 9 | - gox -output build/{{.OS}}_{{.Arch}}/{{.Dir}} 10 | - cd build 11 | - for osarch in * ; do cd $osarch && zip -r ../metrin-$osarch.zip . && cd ../ ; done 12 | - cd ../ 13 | deploy: 14 | provider: releases 15 | api_key: 16 | secure: CzPitqnFnltxB/+RJHT32Q0A/9l6W679srvD7b1FAjl2o9hKThYzhMN6k/64vneVOUqHvj13KwstAL+CmFqlQMrfabgUd+gSzI1wdvF1WguPEhvM7inDLtgYP0i+aqQHgnWKaoPDPG00EwJlCTdEz1UvsBefixEMopeq8Xp+WL6m5519jQOawg2TyPpCYOlbcma9lh+FaZO3dpSitoZWkIJ5umKOI3fSGwb1gR/ermGk9r25Qfkd7E/dc+lcuuD83WEGfInh3NLL5o6tf03iUDjq4LhBUYi26yg5c/Q3vTd2UAzceA/vINrBobUjBa9mmhaajRHfrJg0W36bAJkXFrkyo5cIXHtimW1lts8BFUYIg5Av6oiSAD1Ff5b9Gt4oou8dq4rwVRjO7Dr7YCJqC/BbkxhQU2JpewLmRgCq2qjs9P3pQ6HWqC5nYk1wwLh3oaXxka9Z4gPy1Ig4pFVJwpof/krT0Qypp1sXq7STnm2YLrKu5zPILAXISTYzqhKX84kcXZo+0zJ6wsQ38FaMVHMhsYN71QgF2DgJtsU+nM4ntgMsSAWpj0dZGvo0v+nCLU71tgul2KRSA/3YQ1MaS0emVvUk00Mv3x2mYs/0+RjRe45yEpT/yqvhyzNORgODYxdAnXhBE+oXa2TSH6KpofMqdIvWQSFK6idg0BthW/0= 17 | file: 18 | - build/metrin-darwin_386.zip 19 | - build/metrin-darwin_amd64.zip 20 | - build/metrin-freebsd_386.zip 21 | - build/metrin-freebsd_amd64.zip 22 | - build/metrin-freebsd_arm.zip 23 | - build/metrin-linux_386.zip 24 | - build/metrin-linux_amd64.zip 25 | - build/metrin-linux_arm.zip 26 | - build/metrin-netbsd_386.zip 27 | - build/metrin-netbsd_amd64.zip 28 | - build/metrin-netbsd_arm.zip 29 | - build/metrin-openbsd_386.zip 30 | - build/metrin-openbsd_amd64.zip 31 | - build/metrin-windows_386.zip 32 | - build/metrin-windows_amd64.zip 33 | skip_cleanup: true 34 | all_branches: true 35 | on: 36 | repo: y13i/metrin 37 | tags: true 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yoriki Yamaguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # metrin 2 | 3 | Very simple CloudWatch CLI for Zabbix/Sensu/Mackerel/etc. 4 | 5 | ## Installation 6 | 7 | Download binary from [releases](https://github.com/y13i/metrin/releases). 8 | 9 | Put it into your `$PATH`. 10 | 11 | ## Usage 12 | 13 | View it first. 14 | 15 | ``` 16 | $ metrin --help 17 | ``` 18 | 19 | ### Set credentials and region 20 | 21 | Use environmental variables. `AWS_REGION`, `AWS_PROFILE`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` 22 | 23 | ### Subcommands 24 | 25 | #### `check` 26 | 27 | Act as Nagios/Sensu plugin style. 28 | 29 | Example: Check an EC2 instance's CPU, critical if > 50% 30 | 31 | ``` 32 | $ metrin --namespace AWS/EC2 --metric-name CPUUtilization --statistic Average --dimension InstanceId:i-1234abcd1234abcde check --critical-gt 50 33 | CloudWatch OK, got `22.534000` (Percent) 34 | Params: { 35 | Dimensions: [{ 36 | Name: "InstanceId", 37 | Value: "i-1234abcd1234abcde" 38 | }], 39 | EndTime: 2016-12-04 23:24:55 +0900 JST, 40 | MetricName: "CPUUtilization", 41 | Namespace: "AWS/EC2", 42 | Period: 60, 43 | StartTime: 2016-12-04 23:19:55 +0900 JST, 44 | Statistics: ["Average"] 45 | } 46 | ``` 47 | 48 | Run command below to view full option list. 49 | 50 | ``` 51 | $ metrin check --help 52 | ``` 53 | 54 | #### `print` 55 | 56 | Print metric statistic value with given format (`--template`). 57 | 58 | Example: Metric path, value, timestamp, tab separated (Default template) 59 | 60 | ``` 61 | $ metrin --namespace AWS/EC2 --metric-name CPUUtilization --statistic Average --start-time -900 --period 300 --dimension InstanceId:i-abcd1234abcd12345 print 62 | CloudWatch.InstanceId.i-abcd1234abcd12345.CPUUtilization.Average 1.566 1480861800 63 | CloudWatch.InstanceId.i-abcd1234abcd12345.CPUUtilization.Average 1.536 1480862100 64 | ``` 65 | 66 | Example: Single value only output (for Zabbix, etc.) 67 | 68 | ``` 69 | $ metrin --namespace AWS/EC2 --metric-name CPUUtilization --statistic Average --start-time -900 --period 300 --dimension InstanceId:i-abcd1234abcd12345 print --template '{{.Datapoint.Average}}' --last-value-only 70 | 1.536 71 | ``` 72 | 73 | Run command below to view full option list. 74 | 75 | ``` 76 | $ metrin print --help 77 | ``` 78 | 79 | This feature is using [text/template](https://golang.org/pkg/text/template/) package. 80 | 81 | Additional functions are `unixtime` (Convert `.Datapoint.Timestamp` to UNIX timestamp), `deref` (Convert `*float64` to `float64`, combine usage with `printf`), `getvalue` (Get statistic value dynamically). 82 | 83 | See [print.go](lib/print.go). 84 | 85 | #### `debug` 86 | 87 | For troubleshooting. 88 | 89 | ## FAQ. 90 | 91 | ### `No datapoints`? 92 | 93 | Adjust `--start-time`, `--end-time`, `--period`. 94 | 95 | ### Multiple dimensions? 96 | 97 | ``` 98 | --dimension dim1key:dim1value --dimension dim2key:dim2value 99 | ``` 100 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dtakamoto/metrin 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.29.33 7 | github.com/urfave/cli v1.22.12 8 | github.com/urfave/cli/v2 v2.25.0 9 | github.com/y13i/metrin v0.0.6 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/aws/aws-sdk-go v1.29.33 h1:WP85+WHalTFQR2wYp5xR2sjiVAZXew2bBQXGU1QJBXI= 4 | github.com/aws/aws-sdk-go v1.29.33/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 9 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 14 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 15 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 20 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 21 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 22 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 23 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 24 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 27 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 28 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 29 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 30 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 32 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 33 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 34 | github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU= 35 | github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 36 | github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= 37 | github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 38 | github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= 39 | github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= 40 | github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 41 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 42 | github.com/y13i/metrin v0.0.6 h1:X3IU582dduUODY0CGnH2bypfM8Eg3PE3fXMfChhJrV4= 43 | github.com/y13i/metrin v0.0.6/go.mod h1:MOKQErd2mVQq1ErOuQDoEOSs1hTa98osBlwTwJka5hQ= 44 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 45 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 46 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 47 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 53 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 54 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 55 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 56 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 57 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 58 | -------------------------------------------------------------------------------- /lib/check.go: -------------------------------------------------------------------------------- 1 | package metrin 2 | 3 | import ( 4 | "fmt" 5 | 6 | "reflect" 7 | 8 | "github.com/aws/aws-sdk-go/service/cloudwatch" 9 | ) 10 | 11 | // CheckThresholds - threshold values and its presense 12 | type CheckThresholds struct { 13 | CriticalGtPresent bool 14 | CriticalLtPresent bool 15 | CriticalGtePresent bool 16 | CriticalLtePresent bool 17 | WarningGtPresent bool 18 | WarningLtPresent bool 19 | WarningGtePresent bool 20 | WarningLtePresent bool 21 | CriticalGtValue float64 22 | CriticalLtValue float64 23 | CriticalGteValue float64 24 | CriticalLteValue float64 25 | WarningGtValue float64 26 | WarningLtValue float64 27 | WarningGteValue float64 28 | WarningLteValue float64 29 | } 30 | 31 | // CheckInput - the argument of check 32 | type CheckInput struct { 33 | Thresholds CheckThresholds 34 | Datapoints []*cloudwatch.Datapoint 35 | Statistics []string 36 | ExtendedStatistics []string 37 | UseDefaultValue bool 38 | DefaultValue float64 39 | } 40 | 41 | // CheckOutput - the result of check, includes message and exit code 42 | type CheckOutput struct { 43 | ExitCode int 44 | Messages []string 45 | } 46 | 47 | // Check - performs check 48 | func Check(input CheckInput) CheckOutput { 49 | lastDatapoint := GetLastDatapoint(input.Datapoints, input.UseDefaultValue, input.DefaultValue) 50 | lastDatapointValue := getDatapointValue(lastDatapoint, input.Statistics, input.ExtendedStatistics) 51 | 52 | exitCode := 0 53 | 54 | messages := []string{ 55 | fmt.Sprintf("got `%f` (%s)", lastDatapointValue, *lastDatapoint.Unit), 56 | } 57 | 58 | if input.Thresholds.CriticalGtPresent && lastDatapointValue > input.Thresholds.CriticalGtValue { 59 | exitCode = 2 60 | messages = append(messages, fmt.Sprintf("greater than `%f`", input.Thresholds.CriticalGtValue)) 61 | } else if input.Thresholds.CriticalLtPresent && lastDatapointValue < input.Thresholds.CriticalLtValue { 62 | exitCode = 2 63 | messages = append(messages, fmt.Sprintf("less than `%f`", input.Thresholds.CriticalLtValue)) 64 | } else if input.Thresholds.CriticalGtePresent && lastDatapointValue >= input.Thresholds.CriticalGteValue { 65 | exitCode = 2 66 | messages = append(messages, fmt.Sprintf("greater than or equal to `%f`", input.Thresholds.CriticalGteValue)) 67 | } else if input.Thresholds.CriticalLtePresent && lastDatapointValue <= input.Thresholds.CriticalLteValue { 68 | exitCode = 2 69 | messages = append(messages, fmt.Sprintf("less than or equal to `%f`", input.Thresholds.CriticalLteValue)) 70 | } else if input.Thresholds.WarningGtPresent && lastDatapointValue > input.Thresholds.WarningGtValue { 71 | exitCode = 1 72 | messages = append(messages, fmt.Sprintf("greater than `%f`", input.Thresholds.WarningGtValue)) 73 | } else if input.Thresholds.WarningLtPresent && lastDatapointValue < input.Thresholds.WarningLtValue { 74 | exitCode = 1 75 | messages = append(messages, fmt.Sprintf("less than `%f`", input.Thresholds.WarningLtValue)) 76 | } else if input.Thresholds.WarningGtePresent && lastDatapointValue >= input.Thresholds.WarningGteValue { 77 | exitCode = 1 78 | messages = append(messages, fmt.Sprintf("greater than or equal to `%f`", input.Thresholds.WarningGteValue)) 79 | } else if input.Thresholds.WarningLtePresent && lastDatapointValue <= input.Thresholds.WarningLteValue { 80 | exitCode = 1 81 | messages = append(messages, fmt.Sprintf("less than or equal to `%f`", input.Thresholds.WarningLteValue)) 82 | } 83 | 84 | switch exitCode { 85 | case 0: 86 | messages = append([]string{"CloudWatch OK"}, messages...) 87 | case 1: 88 | messages = append([]string{"CloudWatch WARNING"}, messages...) 89 | case 2: 90 | messages = append([]string{"CloudWatch CRITICAL"}, messages...) 91 | } 92 | 93 | return CheckOutput{ 94 | ExitCode: exitCode, 95 | Messages: messages, 96 | } 97 | } 98 | 99 | func getDatapointValue(datapoint *cloudwatch.Datapoint, statistics []string, extendedStatistics []string) float64 { 100 | var value float64 101 | 102 | if len(statistics) > 0 { 103 | value = getStatisticValue(datapoint, statistics[0]) 104 | } else if len(extendedStatistics) > 0 { 105 | value = getExtendedStatisticValue(datapoint, extendedStatistics[0]) 106 | } 107 | 108 | return value 109 | } 110 | 111 | func getStatisticValue(datapoint *cloudwatch.Datapoint, statistic string) float64 { 112 | r := reflect.Indirect(reflect.ValueOf(datapoint)) 113 | f := r.FieldByName(statistic).Interface().(*float64) 114 | 115 | return float64(*f) 116 | } 117 | 118 | func getExtendedStatisticValue(datapoint *cloudwatch.Datapoint, extendedStatistic string) float64 { 119 | return float64(*datapoint.ExtendedStatistics[extendedStatistic]) 120 | } 121 | -------------------------------------------------------------------------------- /lib/print.go: -------------------------------------------------------------------------------- 1 | package metrin 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "reflect" 7 | 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go/service/cloudwatch" 11 | ) 12 | 13 | // BuildPrintStringInput - includes params and datapoints 14 | type BuildPrintStringInput struct { 15 | Params *cloudwatch.GetMetricStatisticsInput 16 | Datapoints []*cloudwatch.Datapoint 17 | TemplateString string 18 | } 19 | 20 | // TemplateInput - input type for each template execution 21 | type TemplateInput struct { 22 | Params *cloudwatch.GetMetricStatisticsInput 23 | Datapoint *cloudwatch.Datapoint 24 | } 25 | 26 | // BuildPrintStrings - returns slice of built string 27 | func BuildPrintStrings(input BuildPrintStringInput) []string { 28 | var strings []string 29 | 30 | buildTemplate := template.New("") 31 | 32 | buildTemplate.Funcs(template.FuncMap{ 33 | "unixtime": func(t time.Time) int64 { return t.Unix() }, 34 | "deref": func(v *float64) float64 { return *v }, 35 | 36 | "getvalue": func(datapoint *cloudwatch.Datapoint, params *cloudwatch.GetMetricStatisticsInput, statIndex int) *float64 { 37 | r := reflect.Indirect(reflect.ValueOf(datapoint)) 38 | f := r.FieldByName(*params.Statistics[statIndex]).Interface().(*float64) 39 | 40 | return f 41 | }, 42 | }) 43 | 44 | template.Must(buildTemplate.Parse(input.TemplateString)) 45 | 46 | for i := range input.Datapoints { 47 | datapoint := input.Datapoints[i] 48 | buffer := new(bytes.Buffer) 49 | 50 | buildTemplate.Execute(buffer, TemplateInput{ 51 | Params: input.Params, 52 | Datapoint: datapoint, 53 | }) 54 | 55 | strings = append(strings, buffer.String()) 56 | } 57 | 58 | return strings 59 | } 60 | -------------------------------------------------------------------------------- /lib/utils.go: -------------------------------------------------------------------------------- 1 | package metrin 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/cloudwatch" 12 | ) 13 | 14 | // BuildParamsInput - the type for BuildParams function argument 15 | type BuildParamsInput struct { 16 | Namespace string 17 | MetricName string 18 | StartTime int64 19 | EndTime int64 20 | Period int64 21 | Unit string 22 | Statistics []string 23 | ExtendedStatistics []string 24 | Dimensions []string 25 | } 26 | 27 | // BuildParams - used for create GetMetricStatisticsInput 28 | func BuildParams(input BuildParamsInput) *cloudwatch.GetMetricStatisticsInput { 29 | var startTime time.Time 30 | var endTime time.Time 31 | 32 | if input.StartTime <= 0 { 33 | startTime = time.Unix((time.Now().Unix() + input.StartTime), 0) 34 | } 35 | 36 | if input.EndTime <= 0 { 37 | endTime = time.Unix((time.Now().Unix() + input.EndTime), 0) 38 | } 39 | 40 | params := &cloudwatch.GetMetricStatisticsInput{ 41 | Namespace: aws.String(input.Namespace), 42 | MetricName: aws.String(input.MetricName), 43 | StartTime: aws.Time(startTime), 44 | EndTime: aws.Time(endTime), 45 | Period: aws.Int64(input.Period), 46 | } 47 | 48 | if len(input.Unit) > 0 { 49 | params.Unit = aws.String(input.Unit) 50 | } 51 | 52 | if len(input.Statistics) > 0 { 53 | s := make([]*string, len(input.Statistics)) 54 | 55 | for i := range input.Statistics { 56 | s[i] = aws.String(input.Statistics[i]) 57 | } 58 | 59 | params.Statistics = s 60 | } 61 | 62 | if len(input.ExtendedStatistics) > 0 { 63 | s := make([]*string, len(input.ExtendedStatistics)) 64 | 65 | for i := range input.ExtendedStatistics { 66 | s[i] = aws.String(input.ExtendedStatistics[i]) 67 | } 68 | 69 | params.ExtendedStatistics = s 70 | } 71 | 72 | if len(input.Dimensions) > 0 { 73 | s := make([]*cloudwatch.Dimension, len(input.Dimensions)) 74 | 75 | for i := range input.Dimensions { 76 | splitted := strings.Split(input.Dimensions[i], ":") 77 | 78 | s[i] = &cloudwatch.Dimension{ 79 | Name: aws.String(splitted[0]), 80 | Value: aws.String(strings.Join(splitted[1:], ":")), 81 | } 82 | } 83 | 84 | params.Dimensions = s 85 | } 86 | 87 | return params 88 | } 89 | 90 | // GetMetricStatistics wrapper 91 | func GetMetricStatistics(params *cloudwatch.GetMetricStatisticsInput) *cloudwatch.GetMetricStatisticsOutput { 92 | session, err := session.NewSession() 93 | if err != nil { 94 | fmt.Println(err) 95 | os.Exit(3) 96 | } 97 | 98 | service := cloudwatch.New(session) 99 | 100 | response, err := service.GetMetricStatistics(params) 101 | if err != nil { 102 | fmt.Println(err) 103 | os.Exit(3) 104 | } 105 | 106 | return response 107 | } 108 | 109 | // GetLastDatapoint - get latest datapoint from datapoints by timestamp 110 | func GetLastDatapoint(datapoints []*cloudwatch.Datapoint, useDefaultValue bool, defaultValue float64) *cloudwatch.Datapoint { 111 | var lastDatapoint *cloudwatch.Datapoint 112 | 113 | if len(datapoints) < 1 { 114 | if useDefaultValue { 115 | now := time.Now() 116 | noUnit := "No unit" 117 | 118 | lastDatapoint = &cloudwatch.Datapoint{ 119 | Average: &defaultValue, 120 | Maximum: &defaultValue, 121 | Minimum: &defaultValue, 122 | Sum: &defaultValue, 123 | Timestamp: &now, 124 | Unit: &noUnit, 125 | } 126 | } else { 127 | fmt.Println("No datapoint.") 128 | os.Exit(3) 129 | } 130 | } else { 131 | lastDatapoint = datapoints[0] 132 | 133 | for i := range datapoints { 134 | datapoint := datapoints[i] 135 | 136 | if datapoint.Timestamp.Unix() > lastDatapoint.Timestamp.Unix() { 137 | lastDatapoint = datapoint 138 | } 139 | } 140 | } 141 | 142 | return lastDatapoint 143 | } 144 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go/service/cloudwatch" 9 | "github.com/y13i/metrin/lib" 10 | 11 | "github.com/urfave/cli" 12 | ) 13 | 14 | const ( 15 | defaultStartTime = -300 16 | defaultEndTime = 0 17 | defaultPeriod = 60 18 | ) 19 | 20 | func main() { 21 | app := cli.NewApp() 22 | 23 | app.Name = "metrin" 24 | app.Usage = "Very simple CloudWatch CLI for Zabbix/Nagios/Sensu/Mackerel/etc." 25 | app.Version = "0.1.0" 26 | app.EnableBashCompletion = true 27 | 28 | app.Flags = []cli.Flag{ 29 | cli.StringFlag{ 30 | Name: "namespace, n", 31 | Usage: "CloudWatch namespace. e.g. 'AWS/EC2'", 32 | }, 33 | 34 | cli.StringFlag{ 35 | Name: "metric-name, m", 36 | Usage: "CloudWatch metric name. e.g. 'CPUUtilization'", 37 | }, 38 | 39 | cli.Int64Flag{ 40 | Name: "start-time, S", 41 | Value: defaultStartTime, 42 | Usage: "start time as unix timestamp, relative from now if 0 or negative value given", 43 | }, 44 | 45 | cli.Int64Flag{ 46 | Name: "end-time, E", 47 | Value: defaultEndTime, 48 | Usage: "end time as unix timestamp, relative from now if 0 or negative value given", 49 | }, 50 | 51 | cli.Int64Flag{ 52 | Name: "period, p", 53 | Usage: "CloudWatch metric statistic period.", 54 | Value: defaultPeriod, 55 | }, 56 | 57 | cli.StringFlag{ 58 | Name: "unit, u", 59 | Usage: "CloudWatch metric statistic unit. e.g. 'Percent'", 60 | }, 61 | 62 | cli.StringSliceFlag{ 63 | Name: "statistic, s", 64 | Usage: "CloudWatch metrics statistic. e.g. 'Average'", 65 | }, 66 | 67 | cli.StringSliceFlag{ 68 | Name: "extended-statistic, e", 69 | Usage: "CloudWatch extended metrics statistic. e.g. 'p99.5'", 70 | }, 71 | 72 | cli.StringSliceFlag{ 73 | Name: "dimension, d", 74 | Usage: "CloudWatch dimension. `DIM_KEY:DIM_VALUE` e.g. 'InstanceId:i-12345678'", 75 | }, 76 | 77 | cli.StringFlag{ 78 | Name: "region, r", 79 | Usage: "AWS region. e.g. 'us-west-2'", 80 | }, 81 | 82 | cli.StringFlag{ 83 | Name: "profile, P", 84 | Usage: "AWS profile name. e.g. 'myprofile'", 85 | }, 86 | 87 | cli.StringFlag{ 88 | Name: "access-key-id, a", 89 | Usage: "AWS access key id. e.g. 'AKIAIOSFODNN7EXAMPLE'", 90 | }, 91 | 92 | cli.StringFlag{ 93 | Name: "secret-access-key, A", 94 | Usage: "AWS secret access key. e.g. 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'", 95 | }, 96 | 97 | cli.Float64Flag{ 98 | Name: "value-when-no-datapoint, W", 99 | Usage: "use this value when no datapoint fetched", 100 | }, 101 | } 102 | 103 | app.Commands = []cli.Command{ 104 | { 105 | Name: "check", 106 | Usage: "perform check and exit with status codes (0: OK, 1: WARNING, 2: CRITICAL, 3: UNKNOWN)", 107 | 108 | Flags: []cli.Flag{ 109 | cli.Float64Flag{ 110 | Name: "critical-gt", 111 | Usage: "exit as critical (code 2) if latest metric value is greater than the value", 112 | }, 113 | 114 | cli.Float64Flag{ 115 | Name: "critical-lt", 116 | Usage: "exit as critical (code 2) if latest metric value is less than the value", 117 | }, 118 | 119 | cli.Float64Flag{ 120 | Name: "critical-gte", 121 | Usage: "exit as critical (code 2) if latest metric value is greater than or equal to the value", 122 | }, 123 | 124 | cli.Float64Flag{ 125 | Name: "critical-lte", 126 | Usage: "exit as critical (code 2) if latest metric value is less than or equal to the value", 127 | }, 128 | 129 | cli.Float64Flag{ 130 | Name: "warning-gt", 131 | Usage: "exit as warning (code 1) if latest metric value is greater than the value", 132 | }, 133 | 134 | cli.Float64Flag{ 135 | Name: "warning-lt", 136 | Usage: "exit as warning (code 1) if latest metric value is less than the value", 137 | }, 138 | 139 | cli.Float64Flag{ 140 | Name: "warning-gte", 141 | Usage: "exit as warning (code 1) if latest metric value is greater than or equal to the value", 142 | }, 143 | 144 | cli.Float64Flag{ 145 | Name: "warning-lte", 146 | Usage: "exit as warning (code 1) if latest metric value is less than or equal to the value", 147 | }, 148 | }, 149 | 150 | Action: func(ctx *cli.Context) error { 151 | setAwsEnv(ctx) 152 | params := getParams(ctx) 153 | response := metrin.GetMetricStatistics(params) 154 | 155 | thresholds := metrin.CheckThresholds{ 156 | CriticalGtPresent: ctx.IsSet("critical-gt"), 157 | CriticalLtPresent: ctx.IsSet("critical-lt"), 158 | CriticalGtePresent: ctx.IsSet("critical-gte"), 159 | CriticalLtePresent: ctx.IsSet("critical-lte"), 160 | WarningGtPresent: ctx.IsSet("warning-gt"), 161 | WarningLtPresent: ctx.IsSet("warning-lt"), 162 | WarningGtePresent: ctx.IsSet("warning-gte"), 163 | WarningLtePresent: ctx.IsSet("warning-lte"), 164 | CriticalGtValue: ctx.Float64("critical-gt"), 165 | CriticalLtValue: ctx.Float64("critical-lt"), 166 | CriticalGteValue: ctx.Float64("critical-gte"), 167 | CriticalLteValue: ctx.Float64("critical-lte"), 168 | WarningGtValue: ctx.Float64("warning-gt"), 169 | WarningLtValue: ctx.Float64("warning-lt"), 170 | WarningGteValue: ctx.Float64("warning-gte"), 171 | WarningLteValue: ctx.Float64("warning-lte"), 172 | } 173 | 174 | checkOutput := metrin.Check(metrin.CheckInput{ 175 | Thresholds: thresholds, 176 | Datapoints: response.Datapoints, 177 | Statistics: ctx.GlobalStringSlice("statistic"), 178 | ExtendedStatistics: ctx.GlobalStringSlice("extended-statistic"), 179 | UseDefaultValue: ctx.GlobalIsSet("value-when-no-datapoint"), 180 | DefaultValue: ctx.GlobalFloat64("value-when-no-datapoint"), 181 | }) 182 | 183 | fmt.Println(strings.Join(checkOutput.Messages, ", ")) 184 | fmt.Println("Params:", params) 185 | os.Exit(checkOutput.ExitCode) 186 | 187 | return nil 188 | }, 189 | }, 190 | 191 | { 192 | Name: "print", 193 | Usage: "Prints GetMetricStatistics response with given format template", 194 | 195 | Flags: []cli.Flag{ 196 | cli.StringFlag{ 197 | Name: "template, t", 198 | Usage: "output format template (using 'text/template' package. see https://golang.org/pkg/text/template/)", 199 | Value: "CloudWatch.{{(index .Params.Dimensions 0).Name}}.{{(index .Params.Dimensions 0).Value}}.{{.Params.MetricName}}.{{index .Params.Statistics 0}}\t{{getvalue .Datapoint .Params 0 | deref | printf \"%f\"}}\t{{.Datapoint.Timestamp | unixtime}}", 200 | }, 201 | 202 | cli.BoolFlag{ 203 | Name: "last-value-only", 204 | Usage: "if true, print last datapoint value only", 205 | }, 206 | }, 207 | 208 | Action: func(ctx *cli.Context) error { 209 | setAwsEnv(ctx) 210 | params := getParams(ctx) 211 | response := metrin.GetMetricStatistics(params) 212 | 213 | var datapoints []*cloudwatch.Datapoint 214 | 215 | if ctx.Bool("last-value-only") { 216 | datapoints = []*cloudwatch.Datapoint{ 217 | metrin.GetLastDatapoint(response.Datapoints, ctx.GlobalIsSet("value-when-no-datapoint"), ctx.GlobalFloat64("value-when-no-datapoint")), 218 | } 219 | } else { 220 | datapoints = response.Datapoints 221 | } 222 | 223 | outputStrings := metrin.BuildPrintStrings(metrin.BuildPrintStringInput{ 224 | Params: params, 225 | Datapoints: datapoints, 226 | TemplateString: ctx.String("template"), 227 | }) 228 | 229 | fmt.Println(strings.Join(outputStrings, "\n")) 230 | 231 | return nil 232 | }, 233 | }, 234 | 235 | { 236 | Name: "debug", 237 | Usage: "Prints GetMetricStatistics params and response", 238 | 239 | Action: func(ctx *cli.Context) error { 240 | setAwsEnv(ctx) 241 | params := getParams(ctx) 242 | fmt.Println("Params:", params) 243 | 244 | response := metrin.GetMetricStatistics(params) 245 | fmt.Println("Response:", response) 246 | 247 | return nil 248 | }, 249 | }, 250 | } 251 | 252 | app.Run(os.Args) 253 | } 254 | 255 | func getParams(ctx *cli.Context) *cloudwatch.GetMetricStatisticsInput { 256 | return metrin.BuildParams(metrin.BuildParamsInput{ 257 | Namespace: ctx.GlobalString("namespace"), 258 | MetricName: ctx.GlobalString("metric-name"), 259 | StartTime: ctx.GlobalInt64("start-time"), 260 | EndTime: ctx.GlobalInt64("end-time"), 261 | Period: ctx.GlobalInt64("period"), 262 | Unit: ctx.GlobalString("unit"), 263 | Statistics: ctx.GlobalStringSlice("statistic"), 264 | ExtendedStatistics: ctx.GlobalStringSlice("extended-statistic"), 265 | Dimensions: ctx.GlobalStringSlice("dimension"), 266 | }) 267 | } 268 | 269 | func setAwsEnv(ctx *cli.Context) { 270 | if ctx.GlobalIsSet("region") { 271 | os.Setenv("AWS_REGION", ctx.GlobalString("region")) 272 | } 273 | 274 | if ctx.GlobalIsSet("profile") { 275 | os.Setenv("AWS_PROFILE", ctx.GlobalString("profile")) 276 | } 277 | 278 | if ctx.GlobalIsSet("access-key-id") { 279 | os.Setenv("AWS_ACCESS_KEY_ID", ctx.GlobalString("access-key-id")) 280 | } 281 | 282 | if ctx.GlobalIsSet("secret-access-key") { 283 | os.Setenv("AWS_SECRET_ACCESS_KEY", ctx.GlobalString("secret-access-key")) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "packageRules": [ 7 | { 8 | "depTypeList": ["devDependencies"], 9 | "automerge": true 10 | }, 11 | { 12 | "matchUpdateTypes": ["minor", "patch"], 13 | "matchCurrentVersion": "!/^0/", 14 | "automerge": true 15 | } 16 | ] 17 | } 18 | --------------------------------------------------------------------------------