package dynatrace import ( "bytes" "fmt" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/plugins/common/tls" "github.com/influxdata/telegraf/plugins/outputs" "io/ioutil" "math" "net/http" "regexp" "sort" "strconv" "strings" "time" ) const ( oneAgentMetricsUrl = "http://127.0.0.1:14499/metrics/ingest" ) var ( reNameAllowedCharList = regexp.MustCompile("[^A-Za-z0-9.]+") maxDimKeyLen = 100 maxMetricKeyLen = 250 ) // 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 internal.Duration `toml:"timeout"` tls.ClientConfig client *http.Client } 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" ` // 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" } // Normalizes a metric keys or metric dimension identifiers // according to Dynatrace format. func (d *Dynatrace) normalize(s string, max int) (string, error) { s = reNameAllowedCharList.ReplaceAllString(s, "_") // Strip Digits and underscores if they are at the beginning of the string normalizedString := strings.TrimLeft(s, "_0123456789") for strings.HasPrefix(normalizedString, "_") { normalizedString = normalizedString[1:] } if len(normalizedString) > max { normalizedString = normalizedString[:max] } for strings.HasSuffix(normalizedString, "_") { normalizedString = normalizedString[:len(normalizedString)-1] } if len(normalizedString) == 0 { return "", fmt.Errorf("error normalizing the string: %s", s) } return normalizedString, nil } func (d *Dynatrace) escape(v string) string { return strconv.Quote(v) } func (d *Dynatrace) Write(metrics []telegraf.Metric) error { var buf bytes.Buffer var tagb bytes.Buffer if len(metrics) == 0 { return nil } for _, metric := range metrics { // first write the tags into a buffer tagb.Reset() if len(metric.Tags()) > 0 { keys := make([]string, 0, len(metric.Tags())) for k := range metric.Tags() { keys = append(keys, k) } // sort tag keys to expect the same order in ech run sort.Strings(keys) for _, k := range keys { tagKey, err := d.normalize(k, maxDimKeyLen) if err != nil { continue } fmt.Fprintf(&tagb, ",%s=%s", strings.ToLower(tagKey), d.escape(metric.Tags()[k])) } } if len(metric.Fields()) > 0 { for k, v := range metric.Fields() { var value string switch v := v.(type) { case string: continue case float64: if !math.IsNaN(v) && !math.IsInf(v, 0) { value = fmt.Sprintf("%f", v) } else { continue } case uint64: value = strconv.FormatUint(v, 10) case int64: value = strconv.FormatInt(v, 10) case bool: if v { value = "1" } else { value = "0" } default: d.Log.Debugf("Dynatrace type not supported! %s", v) continue } // metric name metricKey, err := d.normalize(k, maxMetricKeyLen) if err != nil { continue } metricID, err := d.normalize(d.Prefix+metric.Name()+"."+metricKey, maxMetricKeyLen) // write metric name combined with its field if err != nil { continue } fmt.Fprintf(&buf, "%s", metricID) // add the tag string fmt.Fprintf(&buf, "%s", tagb.String()) // write measured value fmt.Fprintf(&buf, " %v\n", value) } } } return d.send(buf.Bytes()) } func (d *Dynatrace) send(msg []byte) error { var err error req, err := http.NewRequest("POST", d.URL, bytes.NewBuffer(msg)) if err != nil { d.Log.Errorf("Dynatrace error: %s", err.Error()) return fmt.Errorf("Dynatrace 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()) fmt.Println(req) return fmt.Errorf("Dynatrace error while sending HTTP request:, %s", err.Error()) } defer resp.Body.Close() // print metric line results as info log if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusAccepted { bodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { d.Log.Errorf("Dynatrace error reading response") } bodyString := string(bodyBytes) d.Log.Debugf("Dynatrace returned: %s", bodyString) } else { return fmt.Errorf("Dynatrace request failed with response code:, %d", resp.StatusCode) } 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 = oneAgentMetricsUrl } if d.URL != oneAgentMetricsUrl && 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: d.Timeout.Duration, } return nil } func init() { outputs.Add("dynatrace", func() telegraf.Output { return &Dynatrace{ Timeout: internal.Duration{Duration: time.Second * 5}, } }) }