Files
telegraf/plugins/inputs/stackdriver/stackdriver.go
T

473 lines
14 KiB
Go

package stackdriver
import (
"context"
"fmt"
"log"
"math"
"strings"
"sync"
"time"
"google.golang.org/api/iterator"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal/limiter"
"github.com/influxdata/telegraf/plugins/inputs"
// Imports the Stackdriver Monitoring client package.
monitoring "cloud.google.com/go/monitoring/apiv3"
googlepbduration "github.com/golang/protobuf/ptypes/duration"
googlepbts "github.com/golang/protobuf/ptypes/timestamp"
distributionpb "google.golang.org/genproto/googleapis/api/distribution"
metricpb "google.golang.org/genproto/googleapis/api/metric"
monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3"
)
type Stackdriver struct {
Project string
RateLimit int
LookbackSeconds int64
IncludeMetricTypePrefixes []string
ExcludeMetricTypePrefixes []string
IncludeTagPrefixes []string
ExcludeTagPrefixes []string
ScrapeDistributionBuckets bool
DistributionAggregationAligners []string
client *monitoring.MetricClient
ctx context.Context
}
func init() {
f := func() telegraf.Input {
return &Stackdriver{
RateLimit: 14,
LookbackSeconds: 600,
ScrapeDistributionBuckets: true,
DistributionAggregationAligners: []string{},
}
}
inputs.Add("stackdriver", f)
}
func (s *Stackdriver) Description() string {
return "Plugin that scrapes Google's v3 monitoring API."
}
func (s *Stackdriver) SampleConfig() string {
return `
## GCP Project
project = "projects/erudite-bloom-151019"
## API rate limit. On a default project, it seems that a single user can make
## ~14 requests per second. This might be configurable. Each API request can
## fetch every time series for a single metric type, though -- this is plenty
## fast for scraping all the builtin metric types (and even a handful of
## custom ones) every 60s.
# rateLimit = 14
## Every query to stackdriver queries for data points that have timestamp t
## such that: (now() - lookbackSeconds) <= t < now(). Note that influx will
## de-dedupe points that are pulled twice, so it's best to be safe here, just
## in case it takes GCP awhile to get around to recording the data you seek.
# lookbackSeconds = 600
## Sets whether or not to scrape all bucket counts for metrics whose value
## type is "distribution". If those ~70 fields per metric
## type are annoying to you, try out the distributionAggregationAligners
## configuration option, wherein you may specifiy a list of aggregate functions
## (e.g., ALIGN_PERCENTILE_99) that might be more useful to you.
# scrapeDistributionBuckets = true
## Excluded GCP metric types. Any string prefix works.
## Only declare either this or includeMetricTypePrefixes
excludeMetricTypePrefixes = [
"agent",
"aws",
"custom"
]
## *Only* include these GCP metric types. Any string prefix works
## Only declare either this or excludeMetricTypePrefixes
# includeMetricTypePrefixes = nil
## Excluded GCP metric and resource tags. Any string prefix works.
## Only declare either this or includeTagPrefixes
excludeTagPrefixes = [
"pod_id",
]
## *Only* include these GCP metric and resource tags. Any string prefix works
## Only declare either this or excludeTagPrefixes
# includeTagPrefixes = nil
## Declares a list of aggregate functions to be used for metric types whose
## value type is "distribution". These aggregate values are recorded in the
## distribution's measurement *in addition* to the bucket counts. That is to
## say: setting this option is not mutually exclusive with
## scrapeDistributionBuckets.
distributionAggregationAligners = [
"ALIGN_PERCENTILE_99",
"ALIGN_PERCENTILE_95",
"ALIGN_PERCENTILE_50",
]
`
}
func (s *Stackdriver) Gather(acc telegraf.Accumulator) error {
err := s.initializeStackdriverClient()
if err != nil {
log.Printf("E! Failed to create stackdriver monitoring client: %v", err)
return err
}
timeSeriesRequests, err := s.generatetimeSeriesConfs()
if err != nil {
log.Printf("E! Failed to get metrics: %s\n", err)
return err
}
s.scrapeAllTimeSeries(acc, timeSeriesRequests)
if err != nil {
return err
}
return nil
}
// Internal structure which holds our configuration for a particular GCP time
// series.
type timeSeriesConf struct {
// The influx measurement name this time series maps to
measurement string
// The prefix to use before any influx field names that we'll write for
// this time series. (Or, if we only decide to write one field name, this
// field just holds the value of the field name.)
fieldPrefix string
// The GCP API request that we'll use to fetch data for this time series.
listTimeSeriesRequest monitoringpb.ListTimeSeriesRequest
}
// Create and initialize a timeSeriesConf for a given GCP metric type with
// defaults taken from the gcp_stackdriver plugin configuration.
func (s *Stackdriver) newTimeSeriesConf(metricType string) *timeSeriesConf {
endSec := time.Now().Unix()
startSec := endSec - s.LookbackSeconds
endTs := &googlepbts.Timestamp{Seconds: endSec}
startTs := &googlepbts.Timestamp{Seconds: startSec}
filter := fmt.Sprintf("metric.type = \"%s\"", metricType)
interval := &monitoringpb.TimeInterval{
EndTime: endTs,
StartTime: startTs}
tsReq := monitoringpb.ListTimeSeriesRequest{
Name: s.Project,
Filter: filter,
Interval: interval}
cfg := &timeSeriesConf{
measurement: metricType,
fieldPrefix: "value",
listTimeSeriesRequest: tsReq,
}
// GCP metric types have at least one slash, but we'll be defensive anyway.
slashIdx := strings.LastIndex(metricType, "/")
if slashIdx != -1 {
cfg.measurement = metricType[:slashIdx]
cfg.fieldPrefix = metricType[slashIdx+1:]
}
return cfg
}
// Change this configuration to query an aggregate by specifying an "aligner".
// In GCP monitoring, "aligning" is aggregation performed *within* a time
// series, to distill a pile of data points down to a single data point for
// some given time period (here, we specify 60s as our time period). This is
// especially useful for scraping GCP "distribution" metric types, whose raw
// data amounts to a ~60 bucket histogram, which is fairly hard to query and
// visualize in the TICK stack.
func (t *timeSeriesConf) initForAggregate(alignerStr string) *timeSeriesConf {
alignerInt := monitoringpb.Aggregation_Aligner_value[alignerStr]
aligner := monitoringpb.Aggregation_Aligner(alignerInt)
agg := &monitoringpb.Aggregation{
AlignmentPeriod: &googlepbduration.Duration{Seconds: 60},
PerSeriesAligner: aligner,
}
t.fieldPrefix = t.fieldPrefix + "_" + strings.ToLower(alignerStr)
t.listTimeSeriesRequest.Aggregation = agg
return t
}
// Change this configuration to query a distribution type. Time series of type
// distribution will generate a lot of fields (one for each bucket).
func (t *timeSeriesConf) initForDistribution() *timeSeriesConf {
t.fieldPrefix = t.fieldPrefix + "_"
return t
}
func (s *Stackdriver) initializeStackdriverClient() error {
if s.client == nil {
s.ctx = context.Background()
// Creates a client.
client, err := monitoring.NewMetricClient(s.ctx)
if err != nil {
return err
}
s.client = client
}
return nil
}
func includeExcludeHelper(key string, includes []string, excludes []string) bool {
if includes != nil {
for _, includeStr := range includes {
if strings.Index(key, includeStr) == 0 {
return true
}
}
} else if excludes != nil {
found := false
for _, excludeStr := range excludes {
if strings.Index(key, excludeStr) == 0 {
found = true
break
}
}
if !found {
return true
}
} else {
return true
}
return false
}
// Test whether a particular GCP metric type should be scraped by this plugin
// by checking the plugin name against the configuration's
// "includeMetricTypePrefixes" and "excludeMetricTypePrefixes"
func (s *Stackdriver) includeMetricType(metricType string) bool {
k := metricType
inc := s.IncludeMetricTypePrefixes
exc := s.ExcludeMetricTypePrefixes
return includeExcludeHelper(k, inc, exc)
}
func (s *Stackdriver) includeTag(tagKey string) bool {
k := tagKey
inc := s.IncludeTagPrefixes
exc := s.ExcludeTagPrefixes
return includeExcludeHelper(k, inc, exc)
}
// Generate a list of timeSeriesConfig structs by making a ListMetricDescriptors
// API request and filtering the result against our configuration.
func (s *Stackdriver) generatetimeSeriesConfs() ([]timeSeriesConf, error) {
ret := []timeSeriesConf{}
req := &monitoringpb.ListMetricDescriptorsRequest{Name: s.Project}
resp := s.client.ListMetricDescriptors(s.ctx, req)
for {
metricDescriptor, err := resp.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
metricType := metricDescriptor.Type
valueType := metricDescriptor.ValueType
if s.includeMetricType(metricType) {
if valueType == metricpb.MetricDescriptor_DISTRIBUTION {
if s.ScrapeDistributionBuckets {
tsConf := s.newTimeSeriesConf(metricType)
tsConf = tsConf.initForDistribution()
ret = append(ret, *tsConf)
}
for _, alignerStr := range s.DistributionAggregationAligners {
tsConf := s.newTimeSeriesConf(metricType)
tsConf = tsConf.initForAggregate(alignerStr)
ret = append(ret, *tsConf)
}
} else {
ret = append(ret, *s.newTimeSeriesConf(metricType))
}
}
}
return ret, nil
}
// Do the work! Create a (rate-limited) goroutine for each timeSeriesConf. The
// rate limiting is super-necessary: GCP will trout-slap us if we talk to it
// too quickly.
func (s *Stackdriver) scrapeAllTimeSeries(
acc telegraf.Accumulator,
types []timeSeriesConf) error {
wg := &sync.WaitGroup{}
lmtr := limiter.NewRateLimiter(s.RateLimit, time.Second)
defer lmtr.Stop()
wg.Add(len(types))
for _, timeSeriesRequest := range types {
go func(tsc timeSeriesConf) {
defer wg.Done()
<-lmtr.C
acc.AddError(s.scrapeTimeSeries(acc, tsc))
}(timeSeriesRequest)
}
wg.Wait()
return nil
}
// Do the work to scrape an individual time series. Runs inside a
// timeseries-specific goroutine.
func (s *Stackdriver) scrapeTimeSeries(acc telegraf.Accumulator,
tsConf timeSeriesConf) error {
tsReq := tsConf.listTimeSeriesRequest
measurement := tsConf.measurement
fieldPrefix := tsConf.fieldPrefix
tsResp := s.client.ListTimeSeries(s.ctx, &tsReq)
for {
tsDesc, tsErr := tsResp.Next()
if tsErr == iterator.Done {
break
}
if tsErr != nil {
log.Printf("D! Request %s failure: %s\n", tsReq.String(), tsErr)
return tsErr
}
tags := map[string]string{
"resource_type": tsDesc.Resource.Type,
}
for k, v := range tsDesc.Resource.Labels {
if s.includeTag(k) {
tags[k] = v
}
}
for k, v := range tsDesc.Metric.Labels {
if s.includeTag(k) {
tags[k] = v
}
}
for _, p := range tsDesc.Points {
ts := time.Unix(p.Interval.EndTime.Seconds, 0)
var fields map[string]interface{}
if tsDesc.ValueType == metricpb.MetricDescriptor_DISTRIBUTION {
val := p.Value.GetDistributionValue()
fields = s.scrapeDistribution(acc, measurement, fieldPrefix, val)
} else {
var field interface{}
switch tsDesc.ValueType {
case metricpb.MetricDescriptor_BOOL:
field = p.Value.GetBoolValue()
case metricpb.MetricDescriptor_INT64:
field = p.Value.GetInt64Value()
case metricpb.MetricDescriptor_DOUBLE:
field = p.Value.GetDoubleValue()
case metricpb.MetricDescriptor_STRING:
field = p.Value.GetStringValue()
default:
// TODO: AddError here? Is it really an error?
// TODO: Maybe default to `field = p.Value.GetValue()`
continue
}
fields = map[string]interface{}{
fieldPrefix: field,
}
}
acc.AddFields(measurement, fields, tags, ts)
}
}
return nil
}
// Write out fields for a distribution metric. In this function, we interpret
// the width of each bucket and write out a special field name for each bucket
// that encodes the bucket's lower bounds. For example, the bucket named
// "bucket_ge_1.5" includes a count of points in the distribution that were
// greater than the value 1.5. To find the upper bound, simply look at other
// buckets. If the next bucket in the order is, for example, "bucket_ge_2.6",
// that means bucket_ge_1.5 holds points that are strictly less than the value
// 2.6.
func (s *Stackdriver) scrapeDistribution(acc telegraf.Accumulator,
metricType string,
fieldPrefix string,
metric *distributionpb.Distribution) map[string]interface{} {
fields := map[string]interface{}{}
fields[fieldPrefix+"count"] = metric.Count
fields[fieldPrefix+"mean"] = metric.Mean
fields[fieldPrefix+"sum_of_squared_deviation"] = metric.SumOfSquaredDeviation
if metric.Range != nil {
fields[fieldPrefix+"range_min"] = metric.Range.Min
fields[fieldPrefix+"range_max"] = metric.Range.Max
}
if metric.Count > 0 {
fields[fieldPrefix+"bucket_underflow"] = metric.BucketCounts[0]
}
linearBuckets := metric.BucketOptions.GetLinearBuckets()
exponentialBuckets := metric.BucketOptions.GetExponentialBuckets()
explicitBuckets := metric.BucketOptions.GetExplicitBuckets()
var numBuckets int32
if linearBuckets != nil {
numBuckets = linearBuckets.NumFiniteBuckets
} else if exponentialBuckets != nil {
numBuckets = exponentialBuckets.NumFiniteBuckets
} else {
numBuckets = int32(len(explicitBuckets.Bounds)) + 1
}
var i int32
for i = 1; i < numBuckets; i++ {
var num float64
if linearBuckets != nil {
bucketWidth := linearBuckets.Width * float64(i-1)
num = linearBuckets.Offset + bucketWidth
} else if exponentialBuckets != nil {
width := math.Pow(exponentialBuckets.GrowthFactor, float64(i-1))
num = exponentialBuckets.Scale * width
} else if explicitBuckets != nil {
num = explicitBuckets.Bounds[i-1]
}
col := fmt.Sprintf("%sbucket_ge_%.3f", fieldPrefix, num)
if i < int32(len(metric.BucketCounts)) {
fields[col] = metric.BucketCounts[i]
} else {
fields[col] = 0
}
}
return fields
}