2016-01-18 19:39:14 +00:00
|
|
|
package cloudwatch
|
|
|
|
|
|
|
|
import (
|
|
|
|
"log"
|
|
|
|
"math"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
|
|
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
2018-01-03 00:33:16 +00:00
|
|
|
"github.com/aws/aws-sdk-go/service/sts"
|
2016-01-18 19:39:14 +00:00
|
|
|
|
2016-01-27 21:21:36 +00:00
|
|
|
"github.com/influxdata/telegraf"
|
2016-05-25 11:30:39 +00:00
|
|
|
internalaws "github.com/influxdata/telegraf/internal/config/aws"
|
2016-01-27 23:15:14 +00:00
|
|
|
"github.com/influxdata/telegraf/plugins/outputs"
|
2016-01-18 19:39:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type CloudWatch struct {
|
2018-07-31 22:07:21 +00:00
|
|
|
Region string `toml:"region"`
|
|
|
|
AccessKey string `toml:"access_key"`
|
|
|
|
SecretKey string `toml:"secret_key"`
|
|
|
|
RoleARN string `toml:"role_arn"`
|
|
|
|
Profile string `toml:"profile"`
|
|
|
|
Filename string `toml:"shared_credential_file"`
|
|
|
|
Token string `toml:"token"`
|
|
|
|
EndpointURL string `toml:"endpoint_url"`
|
2016-05-25 11:30:39 +00:00
|
|
|
|
|
|
|
Namespace string `toml:"namespace"` // CloudWatch Metrics Namespace
|
2016-01-18 19:39:14 +00:00
|
|
|
svc *cloudwatch.CloudWatch
|
|
|
|
}
|
|
|
|
|
|
|
|
var sampleConfig = `
|
2016-02-18 21:26:51 +00:00
|
|
|
## Amazon REGION
|
2016-11-04 13:16:41 +00:00
|
|
|
region = "us-east-1"
|
2016-01-18 19:39:14 +00:00
|
|
|
|
2016-04-23 18:19:04 +00:00
|
|
|
## Amazon Credentials
|
|
|
|
## Credentials are loaded in the following order
|
2016-05-25 11:30:39 +00:00
|
|
|
## 1) Assumed credentials via STS if role_arn is specified
|
|
|
|
## 2) explicit credentials from 'access_key' and 'secret_key'
|
|
|
|
## 3) shared profile from 'profile'
|
|
|
|
## 4) environment variables
|
|
|
|
## 5) shared credentials file
|
|
|
|
## 6) EC2 Instance Profile
|
2016-04-23 18:19:04 +00:00
|
|
|
#access_key = ""
|
|
|
|
#secret_key = ""
|
2016-05-25 11:30:39 +00:00
|
|
|
#token = ""
|
|
|
|
#role_arn = ""
|
|
|
|
#profile = ""
|
|
|
|
#shared_credential_file = ""
|
2016-04-23 18:19:04 +00:00
|
|
|
|
2018-07-31 22:07:21 +00:00
|
|
|
## Endpoint to make request against, the correct endpoint is automatically
|
|
|
|
## determined and this option should only be set if you wish to override the
|
|
|
|
## default.
|
|
|
|
## ex: endpoint_url = "http://localhost:8000"
|
|
|
|
# endpoint_url = ""
|
|
|
|
|
2016-02-18 21:26:51 +00:00
|
|
|
## Namespace for the CloudWatch MetricDatums
|
2016-11-04 13:16:41 +00:00
|
|
|
namespace = "InfluxData/Telegraf"
|
2016-01-18 19:39:14 +00:00
|
|
|
`
|
|
|
|
|
|
|
|
func (c *CloudWatch) SampleConfig() string {
|
|
|
|
return sampleConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CloudWatch) Description() string {
|
|
|
|
return "Configuration for AWS CloudWatch output."
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CloudWatch) Connect() error {
|
2016-05-25 11:30:39 +00:00
|
|
|
credentialConfig := &internalaws.CredentialConfig{
|
2018-07-31 22:07:21 +00:00
|
|
|
Region: c.Region,
|
|
|
|
AccessKey: c.AccessKey,
|
|
|
|
SecretKey: c.SecretKey,
|
|
|
|
RoleARN: c.RoleARN,
|
|
|
|
Profile: c.Profile,
|
|
|
|
Filename: c.Filename,
|
|
|
|
Token: c.Token,
|
|
|
|
EndpointURL: c.EndpointURL,
|
2016-04-23 18:19:04 +00:00
|
|
|
}
|
2016-05-25 11:30:39 +00:00
|
|
|
configProvider := credentialConfig.Credentials()
|
2018-01-03 00:33:16 +00:00
|
|
|
|
|
|
|
stsService := sts.New(configProvider)
|
|
|
|
|
|
|
|
params := &sts.GetCallerIdentityInput{}
|
|
|
|
|
|
|
|
_, err := stsService.GetCallerIdentity(params)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("E! cloudwatch: Cannot use credentials to connect to AWS : %+v \n", err.Error())
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-10-13 18:04:40 +00:00
|
|
|
c.svc = cloudwatch.New(configProvider)
|
2018-01-03 00:33:16 +00:00
|
|
|
|
2017-10-13 18:04:40 +00:00
|
|
|
return nil
|
2016-01-18 19:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CloudWatch) Close() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-01-27 23:15:14 +00:00
|
|
|
func (c *CloudWatch) Write(metrics []telegraf.Metric) error {
|
2018-07-23 18:00:35 +00:00
|
|
|
|
|
|
|
var datums []*cloudwatch.MetricDatum
|
2016-01-27 23:15:14 +00:00
|
|
|
for _, m := range metrics {
|
2018-07-23 18:00:35 +00:00
|
|
|
d := BuildMetricDatum(m)
|
|
|
|
datums = append(datums, d...)
|
2016-01-18 19:39:14 +00:00
|
|
|
}
|
|
|
|
|
2016-01-27 23:15:14 +00:00
|
|
|
const maxDatumsPerCall = 20 // PutMetricData only supports up to 20 data metrics per call
|
2016-01-18 19:39:14 +00:00
|
|
|
|
|
|
|
for _, partition := range PartitionDatums(maxDatumsPerCall, datums) {
|
|
|
|
err := c.WriteToCloudWatch(partition)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CloudWatch) WriteToCloudWatch(datums []*cloudwatch.MetricDatum) error {
|
|
|
|
params := &cloudwatch.PutMetricDataInput{
|
|
|
|
MetricData: datums,
|
|
|
|
Namespace: aws.String(c.Namespace),
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := c.svc.PutMetricData(params)
|
|
|
|
|
|
|
|
if err != nil {
|
2016-09-30 21:37:56 +00:00
|
|
|
log.Printf("E! CloudWatch: Unable to write to CloudWatch : %+v \n", err.Error())
|
2016-01-18 19:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Partition the MetricDatums into smaller slices of a max size so that are under the limit
|
|
|
|
// for the AWS API calls.
|
|
|
|
func PartitionDatums(size int, datums []*cloudwatch.MetricDatum) [][]*cloudwatch.MetricDatum {
|
|
|
|
|
|
|
|
numberOfPartitions := len(datums) / size
|
|
|
|
if len(datums)%size != 0 {
|
|
|
|
numberOfPartitions += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
partitions := make([][]*cloudwatch.MetricDatum, numberOfPartitions)
|
|
|
|
|
|
|
|
for i := 0; i < numberOfPartitions; i++ {
|
|
|
|
start := size * i
|
|
|
|
end := size * (i + 1)
|
|
|
|
if end > len(datums) {
|
|
|
|
end = len(datums)
|
|
|
|
}
|
|
|
|
|
|
|
|
partitions[i] = datums[start:end]
|
|
|
|
}
|
|
|
|
|
|
|
|
return partitions
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make a MetricDatum for each field in a Point. Only fields with values that can be
|
|
|
|
// converted to float64 are supported. Non-supported fields are skipped.
|
2016-01-27 23:15:14 +00:00
|
|
|
func BuildMetricDatum(point telegraf.Metric) []*cloudwatch.MetricDatum {
|
2016-01-18 19:39:14 +00:00
|
|
|
datums := make([]*cloudwatch.MetricDatum, len(point.Fields()))
|
|
|
|
i := 0
|
|
|
|
|
|
|
|
var value float64
|
|
|
|
|
|
|
|
for k, v := range point.Fields() {
|
|
|
|
switch t := v.(type) {
|
|
|
|
case int:
|
|
|
|
value = float64(t)
|
|
|
|
case int32:
|
|
|
|
value = float64(t)
|
|
|
|
case int64:
|
|
|
|
value = float64(t)
|
2018-06-01 17:47:40 +00:00
|
|
|
case uint64:
|
|
|
|
value = float64(t)
|
2016-01-18 19:39:14 +00:00
|
|
|
case float64:
|
|
|
|
value = t
|
|
|
|
case bool:
|
|
|
|
if t {
|
|
|
|
value = 1
|
|
|
|
} else {
|
|
|
|
value = 0
|
|
|
|
}
|
|
|
|
case time.Time:
|
|
|
|
value = float64(t.Unix())
|
|
|
|
default:
|
|
|
|
// Skip unsupported type.
|
|
|
|
datums = datums[:len(datums)-1]
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-08-28 23:56:03 +00:00
|
|
|
// Do CloudWatch boundary checking
|
|
|
|
// Constraints at: http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html
|
|
|
|
if math.IsNaN(value) {
|
|
|
|
datums = datums[:len(datums)-1]
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if math.IsInf(value, 0) {
|
|
|
|
datums = datums[:len(datums)-1]
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if value > 0 && value < float64(8.515920e-109) {
|
|
|
|
datums = datums[:len(datums)-1]
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if value > float64(1.174271e+108) {
|
|
|
|
datums = datums[:len(datums)-1]
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2016-01-18 19:39:14 +00:00
|
|
|
datums[i] = &cloudwatch.MetricDatum{
|
|
|
|
MetricName: aws.String(strings.Join([]string{point.Name(), k}, "_")),
|
|
|
|
Value: aws.Float64(value),
|
|
|
|
Dimensions: BuildDimensions(point.Tags()),
|
|
|
|
Timestamp: aws.Time(point.Time()),
|
|
|
|
}
|
|
|
|
|
|
|
|
i += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
return datums
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make a list of Dimensions by using a Point's tags. CloudWatch supports up to
|
|
|
|
// 10 dimensions per metric so we only keep up to the first 10 alphabetically.
|
|
|
|
// This always includes the "host" tag if it exists.
|
2016-01-27 23:15:14 +00:00
|
|
|
func BuildDimensions(mTags map[string]string) []*cloudwatch.Dimension {
|
2016-01-18 19:39:14 +00:00
|
|
|
|
|
|
|
const MaxDimensions = 10
|
2016-01-27 23:15:14 +00:00
|
|
|
dimensions := make([]*cloudwatch.Dimension, int(math.Min(float64(len(mTags)), MaxDimensions)))
|
2016-01-18 19:39:14 +00:00
|
|
|
|
|
|
|
i := 0
|
|
|
|
|
|
|
|
// This is pretty ugly but we always want to include the "host" tag if it exists.
|
2016-01-27 23:15:14 +00:00
|
|
|
if host, ok := mTags["host"]; ok {
|
2016-01-18 19:39:14 +00:00
|
|
|
dimensions[i] = &cloudwatch.Dimension{
|
|
|
|
Name: aws.String("host"),
|
|
|
|
Value: aws.String(host),
|
|
|
|
}
|
|
|
|
i += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
var keys []string
|
2016-01-27 23:15:14 +00:00
|
|
|
for k := range mTags {
|
2016-01-18 19:39:14 +00:00
|
|
|
if k != "host" {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
if i >= MaxDimensions {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
dimensions[i] = &cloudwatch.Dimension{
|
|
|
|
Name: aws.String(k),
|
2016-01-27 23:15:14 +00:00
|
|
|
Value: aws.String(mTags[k]),
|
2016-01-18 19:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
i += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
return dimensions
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2016-01-27 21:21:36 +00:00
|
|
|
outputs.Add("cloudwatch", func() telegraf.Output {
|
2016-01-18 19:39:14 +00:00
|
|
|
return &CloudWatch{}
|
|
|
|
})
|
|
|
|
}
|