mirror of
https://github.com/wahyd4/telegraf.git
synced 2026-08-19 01:26:05 +10:00
Standardize name to 'Stackdriver'
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user