package dynatrace import ( "bytes" "fmt" "io" "net/http" "strings" "time" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/plugins/common/tls" "github.com/influxdata/telegraf/plugins/outputs" dtMetric "github.com/dynatrace-oss/dynatrace-metric-utils-go/metric" "github.com/dynatrace-oss/dynatrace-metric-utils-go/metric/apiconstants" "github.com/dynatrace-oss/dynatrace-metric-utils-go/metric/dimensions" ) // Dynatrace Configuration for the Dynatrace output plugin type Dynatrace struct { URL string `toml:"url"` APIToken string `toml:"api_token"` Prefix string `toml:"prefix"` Log telegraf.Logger `toml:"-"` Timeout config.Duration `toml:"timeout"` AddCounterMetrics []string `toml:"additional_counters"` DefaultDimensions map[string]string `toml:"default_dimensions"` normalizedDefaultDimensions dimensions.NormalizedDimensionList normalizedStaticDimensions dimensions.NormalizedDimensionList tls.ClientConfig client *http.Client loggedMetrics map[string]bool // New empty set } const sampleConfig = ` ## For usage with the Dynatrace OneAgent you can omit any configuration, ## the only requirement is that the OneAgent is running on the same host. ## Only setup environment url and token if you want to monitor a Host without the OneAgent present. ## ## Your Dynatrace environment URL. ## For Dynatrace OneAgent you can leave this empty or set it to "http://127.0.0.1:14499/metrics/ingest" (default) ## For Dynatrace SaaS environments the URL scheme is "https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest" ## For Dynatrace Managed environments the URL scheme is "https://{your-domain}/e/{your-environment-id}/api/v2/metrics/ingest" url = "" ## Your Dynatrace API token. ## Create an API token within your Dynatrace environment, by navigating to Settings > Integration > Dynatrace API ## The API token needs data ingest scope permission. When using OneAgent, no API token is required. api_token = "" ## Optional prefix for metric names (e.g.: "telegraf") prefix = "telegraf" ## Optional TLS Config # tls_ca = "/etc/telegraf/ca.pem" # tls_cert = "/etc/telegraf/cert.pem" # tls_key = "/etc/telegraf/key.pem" ## Optional flag for ignoring tls certificate check # insecure_skip_verify = false ## Connection timeout, defaults to "5s" if not set. timeout = "5s" ## If you want metrics to be treated and reported as delta counters, add the metric names here additional_counters = [ ] ## Optional dimensions to be added to every metric # [outputs.dynatrace.default_dimensions] # default_key = "default value" ` // Connect Connects the Dynatrace output plugin to the Telegraf stream func (d *Dynatrace) Connect() error { return nil } // Close Closes the Dynatrace output plugin func (d *Dynatrace) Close() error { d.client = nil return nil } // SampleConfig Returns a sample configuration for the Dynatrace output plugin func (d *Dynatrace) SampleConfig() string { return sampleConfig } // Description returns the description for the Dynatrace output plugin func (d *Dynatrace) Description() string { return "Send telegraf metrics to a Dynatrace environment" } func (d *Dynatrace) Write(metrics []telegraf.Metric) error { if len(metrics) == 0 { return nil } lines := []string{} for _, tm := range metrics { dims := []dimensions.Dimension{} for _, tag := range tm.TagList() { // Ignore special tags for histogram and summary types. switch tm.Type() { case telegraf.Histogram: if tag.Key == "le" || tag.Key == "gt" { continue } case telegraf.Summary: if tag.Key == "quantile" { continue } } dims = append(dims, dimensions.NewDimension(tag.Key, tag.Value)) } for _, field := range tm.FieldList() { metricName := tm.Name() + "." + field.Key typeOpt := d.getTypeOption(tm, field) if typeOpt == nil { // Unsupported type. Log only once per unsupported metric name if !d.loggedMetrics[metricName] { d.Log.Warnf("Unsupported type for %s", metricName) d.loggedMetrics[metricName] = true } continue } name := tm.Name() + "." + field.Key dm, err := dtMetric.NewMetric( name, dtMetric.WithPrefix(d.Prefix), dtMetric.WithDimensions( dimensions.MergeLists( d.normalizedDefaultDimensions, dimensions.NewNormalizedDimensionList(dims...), d.normalizedStaticDimensions, ), ), dtMetric.WithTimestamp(tm.Time()), typeOpt, ) if err != nil { d.Log.Warn(fmt.Sprintf("failed to normalize metric: %s - %s", name, err.Error())) continue } line, err := dm.Serialize() if err != nil { d.Log.Warn(fmt.Sprintf("failed to serialize metric: %s - %s", name, err.Error())) continue } lines = append(lines, line) } } limit := apiconstants.GetPayloadLinesLimit() for i := 0; i < len(lines); i += limit { batch := lines[i:min(i+limit, len(lines))] output := strings.Join(batch, "\n") if output != "" { if err := d.send(output); err != nil { return fmt.Errorf("error processing data:, %s", err.Error()) } } } return nil } func (d *Dynatrace) send(msg string) error { var err error req, err := http.NewRequest("POST", d.URL, bytes.NewBufferString(msg)) if err != nil { d.Log.Errorf("Dynatrace error: %s", err.Error()) return fmt.Errorf("error while creating HTTP request:, %s", err.Error()) } req.Header.Add("Content-Type", "text/plain; charset=UTF-8") if len(d.APIToken) != 0 { req.Header.Add("Authorization", "Api-Token "+d.APIToken) } // add user-agent header to identify metric source req.Header.Add("User-Agent", "telegraf") resp, err := d.client.Do(req) if err != nil { d.Log.Errorf("Dynatrace error: %s", err.Error()) return fmt.Errorf("error while sending HTTP request:, %s", err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusBadRequest { return fmt.Errorf("request failed with response code:, %d", resp.StatusCode) } // print metric line results as info log bodyBytes, err := io.ReadAll(resp.Body) if err != nil { d.Log.Errorf("Dynatrace error reading response") } bodyString := string(bodyBytes) d.Log.Debugf("Dynatrace returned: %s", bodyString) return nil } func (d *Dynatrace) Init() error { if len(d.URL) == 0 { d.Log.Infof("Dynatrace URL is empty, defaulting to OneAgent metrics interface") d.URL = apiconstants.GetDefaultOneAgentEndpoint() } if d.URL != apiconstants.GetDefaultOneAgentEndpoint() && len(d.APIToken) == 0 { d.Log.Errorf("Dynatrace api_token is a required field for Dynatrace output") return fmt.Errorf("api_token is a required field for Dynatrace output") } tlsCfg, err := d.ClientConfig.TLSConfig() if err != nil { return err } d.client = &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: tlsCfg, }, Timeout: time.Duration(d.Timeout), } dims := []dimensions.Dimension{} for key, value := range d.DefaultDimensions { dims = append(dims, dimensions.NewDimension(key, value)) } d.normalizedDefaultDimensions = dimensions.NewNormalizedDimensionList(dims...) d.normalizedStaticDimensions = dimensions.NewNormalizedDimensionList(dimensions.NewDimension("dt.metrics.source", "telegraf")) d.loggedMetrics = make(map[string]bool) return nil } func init() { outputs.Add("dynatrace", func() telegraf.Output { return &Dynatrace{ Timeout: config.Duration(time.Second * 5), } }) } func (d *Dynatrace) getTypeOption(metric telegraf.Metric, field *telegraf.Field) dtMetric.MetricOption { metricName := metric.Name() + "." + field.Key for _, i := range d.AddCounterMetrics { if metricName != i { continue } switch v := field.Value.(type) { case float64: return dtMetric.WithFloatCounterValueDelta(v) case uint64: return dtMetric.WithIntCounterValueDelta(int64(v)) case int64: return dtMetric.WithIntCounterValueDelta(v) default: return nil } } switch v := field.Value.(type) { case float64: return dtMetric.WithFloatGaugeValue(v) case uint64: return dtMetric.WithIntGaugeValue(int64(v)) case int64: return dtMetric.WithIntGaugeValue(v) case bool: if v { return dtMetric.WithIntGaugeValue(1) } return dtMetric.WithIntGaugeValue(0) } return nil } func min(a, b int) int { if a <= b { return a } return b }