-
Notifications
You must be signed in to change notification settings - Fork 5.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AWS CloudWatch Output Plugin #553
Changes from 5 commits
5284480
bb0be0d
262abb4
2b5ea8c
9e34d03
881af1f
02ee480
6d2b4e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
## Amazon CloudWatch Output for Telegraf | ||
|
||
This plugin will send points to Amazon CloudWatch. | ||
|
||
## Amazon Authentication | ||
|
||
This plugin uses a credential chain for Authentication with the CloudWatch API endpoint. In the following order the plugin | ||
will attempt to authenticate. | ||
1. [IAMS Role](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) | ||
2. [Environment Variables](https://github.com/aws/aws-sdk-go/wiki/configuring-sdk) | ||
3. [Shared Credentials](https://github.com/aws/aws-sdk-go/wiki/configuring-sdk) | ||
|
||
## Config | ||
|
||
For this output plugin to function correctly the following variables must be configured. | ||
|
||
* region | ||
* namespace | ||
|
||
### region | ||
|
||
The region is the Amazon region that you wish to connect to. Examples include but are not limited to | ||
* us-west-1 | ||
* us-west-2 | ||
* us-east-1 | ||
* ap-southeast-1 | ||
* ap-southeast-2 | ||
|
||
### namespace | ||
|
||
The namespace used for AWS CloudWatch metrics. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
package cloudwatch | ||
|
||
import ( | ||
"log" | ||
"strings" | ||
"time" | ||
|
||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/credentials" | ||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" | ||
"github.com/aws/aws-sdk-go/aws/ec2metadata" | ||
"github.com/aws/aws-sdk-go/aws/session" | ||
"github.com/aws/aws-sdk-go/service/cloudwatch" | ||
|
||
"github.com/influxdb/influxdb/client/v2" | ||
"github.com/influxdb/telegraf/plugins/outputs" | ||
|
||
"github.com/meirf/gopart" | ||
) | ||
|
||
type CloudWatch struct { | ||
Region string // AWS Region | ||
Namespace string // CloudWatch Metrics Namespace | ||
svc *cloudwatch.CloudWatch | ||
} | ||
|
||
var sampleConfig = ` | ||
# Amazon REGION | ||
region = 'us-east-1' | ||
|
||
# Namespace for the CloudWatch MetricDatums | ||
namespace = 'InfluxData/Telegraf' | ||
` | ||
|
||
func (c *CloudWatch) SampleConfig() string { | ||
return sampleConfig | ||
} | ||
|
||
func (c *CloudWatch) Description() string { | ||
return "Configuration for AWS CloudWatch output." | ||
} | ||
|
||
func (c *CloudWatch) Connect() error { | ||
Config := &aws.Config{ | ||
Region: aws.String(c.Region), | ||
Credentials: credentials.NewChainCredentials( | ||
[]credentials.Provider{ | ||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(session.New())}, | ||
&credentials.EnvProvider{}, | ||
&credentials.SharedCredentialsProvider{}, | ||
}), | ||
} | ||
|
||
svc := cloudwatch.New(session.New(Config)) | ||
|
||
params := &cloudwatch.ListMetricsInput{ | ||
Namespace: aws.String(c.Namespace), | ||
} | ||
|
||
_, err := svc.ListMetrics(params) // Try a read-only call to test connection. | ||
|
||
if err != nil { | ||
log.Printf("cloudwatch: Error in ListMetrics API call : %+v \n", err.Error()) | ||
} | ||
|
||
c.svc = svc | ||
|
||
return err | ||
} | ||
|
||
func (c *CloudWatch) Close() error { | ||
return nil | ||
} | ||
|
||
func (c *CloudWatch) Write(points []*client.Point) error { | ||
for _, pt := range points { | ||
err := c.WriteSinglePoint(pt) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Write data for a single point. A point can have many fields and one field | ||
// is equal to one MetricDatum. There is a limit on how many MetricDatums a | ||
// request can have so we process one Point at a time. | ||
func (c *CloudWatch) WriteSinglePoint(point *client.Point) error { | ||
datums := buildMetricDatum(point) | ||
|
||
const maxDatumsPerCall = 20 // PutMetricData only supports up to 20 data points per call | ||
|
||
for idxRange := range gopart.Partition(len(datums), maxDatumsPerCall) { | ||
err := c.WriteToCloudWatch(datums[idxRange.Low:idxRange.High]) | ||
|
||
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 { | ||
log.Printf("CloudWatch: Unable to write to CloudWatch : %+v \n", err.Error()) | ||
} | ||
|
||
return err | ||
} | ||
|
||
// 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. | ||
func buildMetricDatum(point *client.Point) []*cloudwatch.MetricDatum { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function can be unit tested |
||
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) | ||
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 | ||
} | ||
|
||
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. | ||
func buildDimensions(ptTags map[string]string) []*cloudwatch.Dimension { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's actually a bug here. CloudWatch only supports up to 10 dimensions per datum. Some points have more than 10 tags. I'll fix this but I'm not sure the best approach here. I'm thinking of sorting and keeping the first 10. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm your solution seems reasonable enough, I'm not sure the best way to get around that either, some tags will have to be dropped There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function can be unit tested |
||
|
||
dimensions := make([]*cloudwatch.Dimension, len(ptTags)) | ||
i := 0 | ||
|
||
for k, v := range ptTags { | ||
dimensions[i] = &cloudwatch.Dimension{ | ||
Name: aws.String(k), | ||
Value: aws.String(v), | ||
} | ||
|
||
i += 1 | ||
} | ||
|
||
return dimensions | ||
} | ||
|
||
func init() { | ||
outputs.Add("cloudwatch", func() outputs.Output { | ||
return &CloudWatch{} | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that using
gopart
here makes it elegant, but I don't think it justifies adding another 3rd-party dependency to Telegraf. Could you add a little extra code and do this yourself? One option that comes to mind would be to havebuildMetricDatum
do the partitioning itself and return[][]*cloudwatch.MetricDatum