mirror of
https://github.com/wahyd4/telegraf.git
synced 2026-08-10 05:06:40 +10:00
473 lines
14 KiB
Go
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
|
|
}
|