Add prometheus serializer and use it in prometheus output (#6703)

This commit is contained in:
Daniel Nelson 2019-11-26 15:46:31 -08:00 committed by GitHub
parent 8f71bbaa48
commit 80c5edd48e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 2516 additions and 1144 deletions

1
Gopkg.lock generated
View File

@ -1750,6 +1750,7 @@
"github.com/go-sql-driver/mysql", "github.com/go-sql-driver/mysql",
"github.com/gobwas/glob", "github.com/gobwas/glob",
"github.com/gofrs/uuid", "github.com/gofrs/uuid",
"github.com/gogo/protobuf/proto",
"github.com/golang/protobuf/proto", "github.com/golang/protobuf/proto",
"github.com/golang/protobuf/ptypes/duration", "github.com/golang/protobuf/ptypes/duration",
"github.com/golang/protobuf/ptypes/empty", "github.com/golang/protobuf/ptypes/empty",

View File

@ -5,10 +5,11 @@ standard data formats that may be selected from when configuring many output
plugins. plugins.
1. [InfluxDB Line Protocol](/plugins/serializers/influx) 1. [InfluxDB Line Protocol](/plugins/serializers/influx)
1. [JSON](/plugins/serializers/json)
1. [Graphite](/plugins/serializers/graphite)
1. [SplunkMetric](/plugins/serializers/splunkmetric)
1. [Carbon2](/plugins/serializers/carbon2) 1. [Carbon2](/plugins/serializers/carbon2)
1. [Graphite](/plugins/serializers/graphite)
1. [JSON](/plugins/serializers/json)
1. [Prometheus](/plugins/serializers/prometheus)
1. [SplunkMetric](/plugins/serializers/splunkmetric)
1. [Wavefront](/plugins/serializers/wavefront) 1. [Wavefront](/plugins/serializers/wavefront)
You will be able to identify the plugins with support by the presence of a You will be able to identify the plugins with support by the presence of a

View File

@ -2,6 +2,7 @@ package internal
import ( import (
"crypto/subtle" "crypto/subtle"
"net"
"net/http" "net/http"
) )
@ -43,3 +44,49 @@ func (h *basicAuthHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
h.next.ServeHTTP(rw, req) h.next.ServeHTTP(rw, req)
} }
// IPRangeHandler returns a http handler that requires the remote address to be
// in the specified network.
func IPRangeHandler(network []*net.IPNet, onError ErrorFunc) func(h http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return &ipRangeHandler{
network: network,
onError: onError,
next: h,
}
}
}
type ipRangeHandler struct {
network []*net.IPNet
onError ErrorFunc
next http.Handler
}
func (h *ipRangeHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if len(h.network) == 0 {
h.next.ServeHTTP(rw, req)
return
}
remoteIPString, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
h.onError(rw, http.StatusForbidden)
return
}
remoteIP := net.ParseIP(remoteIPString)
if remoteIP == nil {
h.onError(rw, http.StatusForbidden)
return
}
for _, net := range h.network {
if net.Contains(remoteIP) {
h.next.ServeHTTP(rw, req)
return
}
}
h.onError(rw, http.StatusForbidden)
}

View File

@ -11,8 +11,14 @@ in Prometheus format.
## An array of urls to scrape metrics from. ## An array of urls to scrape metrics from.
urls = ["http://localhost:9100/metrics"] urls = ["http://localhost:9100/metrics"]
## Metric version (optional, default=1, supported values are 1 and 2) ## Metric version controls the mapping from Prometheus metrics into
# metric_version = 2 ## Telegraf metrics. When using the prometheus_client output, use the same
## value in both plugins to ensure metrics are round-tripped without
## modification.
##
## example: metric_version = 1; deprecated in 1.13
## metric_version = 2; recommended version
# metric_version = 1
## An array of Kubernetes services to scrape metrics from. ## An array of Kubernetes services to scrape metrics from.
# kubernetes_services = ["http://my-service-dns.my-namespace:9100/metrics"] # kubernetes_services = ["http://my-service-dns.my-namespace:9100/metrics"]

View File

@ -62,8 +62,14 @@ var sampleConfig = `
## An array of urls to scrape metrics from. ## An array of urls to scrape metrics from.
urls = ["http://localhost:9100/metrics"] urls = ["http://localhost:9100/metrics"]
## Metric version (optional, default=1, supported values are 1 and 2) ## Metric version controls the mapping from Prometheus metrics into
# metric_version = 2 ## Telegraf metrics. When using the prometheus_client output, use the same
## value in both plugins to ensure metrics are round-tripped without
## modification.
##
## example: metric_version = 1; deprecated in 1.13
## metric_version = 2; recommended version
# metric_version = 1
## Url tag name (tag containing scrapped url. optional, default is "url") ## Url tag name (tag containing scrapped url. optional, default is "url")
# url_tag = "scrapeUrl" # url_tag = "scrapeUrl"
@ -95,7 +101,7 @@ var sampleConfig = `
# username = "" # username = ""
# password = "" # password = ""
## Specify timeout duration for slower prometheus clients (default is 3s) ## Specify timeout duration for slower prometheus clients (default is 3s)
# response_timeout = "3s" # response_timeout = "3s"
## Optional TLS Config ## Optional TLS Config
@ -114,6 +120,13 @@ func (p *Prometheus) Description() string {
return "Read metrics from one or many prometheus clients" return "Read metrics from one or many prometheus clients"
} }
func (p *Prometheus) Init() error {
if p.MetricVersion != 2 {
p.Log.Warnf("Use of deprecated configuration: 'metric_version = 1'; please update to 'metric_version = 2'")
}
return nil
}
var ErrProtocolError = errors.New("prometheus protocol error") var ErrProtocolError = errors.New("prometheus protocol error")
func (p *Prometheus) AddressToURL(u *url.URL, address string) *url.URL { func (p *Prometheus) AddressToURL(u *url.URL, address string) *url.URL {
@ -311,7 +324,9 @@ func (p *Prometheus) gatherURL(u URLAndAddress, acc telegraf.Accumulator) error
tags := metric.Tags() tags := metric.Tags()
// strip user and password from URL // strip user and password from URL
u.OriginalURL.User = nil u.OriginalURL.User = nil
tags[p.URLTag] = u.OriginalURL.String() if p.URLTag != "" {
tags[p.URLTag] = u.OriginalURL.String()
}
if u.Address != "" { if u.Address != "" {
tags["address"] = u.Address tags["address"] = u.Address
} }

View File

@ -9,6 +9,11 @@ This plugin writes telegraf metrics to files
## Files to write to, "stdout" is a specially handled file. ## Files to write to, "stdout" is a specially handled file.
files = ["stdout", "/tmp/metrics.out"] files = ["stdout", "/tmp/metrics.out"]
## Use batch serialization format instead of line based delimiting. The
## batch format allows for the production of non line based output formats and
## may more effiently encode and write metrics.
# use_batch_format = false
## The file will be rotated after the time interval specified. When set ## The file will be rotated after the time interval specified. When set
## to 0 no time based rotation is performed. ## to 0 no time based rotation is performed.
# rotation_interval = "0h" # rotation_interval = "0h"

View File

@ -3,7 +3,6 @@ package file
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
@ -18,6 +17,8 @@ type File struct {
RotationInterval internal.Duration `toml:"rotation_interval"` RotationInterval internal.Duration `toml:"rotation_interval"`
RotationMaxSize internal.Size `toml:"rotation_max_size"` RotationMaxSize internal.Size `toml:"rotation_max_size"`
RotationMaxArchives int `toml:"rotation_max_archives"` RotationMaxArchives int `toml:"rotation_max_archives"`
UseBatchFormat bool `toml:"use_batch_format"`
Log telegraf.Logger `toml:"-"`
writer io.Writer writer io.Writer
closers []io.Closer closers []io.Closer
@ -28,6 +29,11 @@ var sampleConfig = `
## Files to write to, "stdout" is a specially handled file. ## Files to write to, "stdout" is a specially handled file.
files = ["stdout", "/tmp/metrics.out"] files = ["stdout", "/tmp/metrics.out"]
## Use batch serialization format instead of line based delimiting. The
## batch format allows for the production of non line based output formats and
## may more effiently encode metric groups.
# use_batch_format = false
## The file will be rotated after the time interval specified. When set ## The file will be rotated after the time interval specified. When set
## to 0 no time based rotation is performed. ## to 0 no time based rotation is performed.
# rotation_interval = "0d" # rotation_interval = "0d"
@ -98,15 +104,27 @@ func (f *File) Description() string {
func (f *File) Write(metrics []telegraf.Metric) error { func (f *File) Write(metrics []telegraf.Metric) error {
var writeErr error = nil var writeErr error = nil
for _, metric := range metrics { if f.UseBatchFormat {
b, err := f.serializer.Serialize(metric) octets, err := f.serializer.SerializeBatch(metrics)
if err != nil { if err != nil {
log.Printf("D! [outputs.file] Could not serialize metric: %v", err) f.Log.Errorf("Could not serialize metric: %v", err)
} }
_, err = f.writer.Write(b) _, err = f.writer.Write(octets)
if err != nil { if err != nil {
writeErr = fmt.Errorf("E! [outputs.file] failed to write message: %v", err) f.Log.Errorf("Error writing to file: %v", err)
}
} else {
for _, metric := range metrics {
b, err := f.serializer.Serialize(metric)
if err != nil {
f.Log.Debugf("Could not serialize metric: %v", err)
}
_, err = f.writer.Write(b)
if err != nil {
writeErr = fmt.Errorf("E! [outputs.file] failed to write message: %v", err)
}
} }
} }

View File

@ -1,6 +1,7 @@
# Prometheus Client Service Output Plugin # Prometheus Output Plugin
This plugin starts a [Prometheus](https://prometheus.io/) Client, it exposes all metrics on `/metrics` (default) to be polled by a Prometheus server. This plugin starts a [Prometheus](https://prometheus.io/) Client, it exposes
all metrics on `/metrics` (default) to be polled by a Prometheus server.
## Configuration ## Configuration
@ -10,6 +11,14 @@ This plugin starts a [Prometheus](https://prometheus.io/) Client, it exposes all
## Address to listen on. ## Address to listen on.
listen = ":9273" listen = ":9273"
## Metric version controls the mapping from Telegraf metrics into
## Prometheus format. When using the prometheus input, use the same value in
## both plugins to ensure metrics are round-tripped without modification.
##
## example: metric_version = 1; deprecated in 1.13
## metric_version = 2; recommended version
# metric_version = 1
## Use HTTP Basic Authentication. ## Use HTTP Basic Authentication.
# basic_username = "Foo" # basic_username = "Foo"
# basic_password = "Bar" # basic_password = "Bar"

View File

@ -1,18 +1,12 @@
package prometheus_client package prometheus
import ( import (
"context" "context"
"crypto/subtle"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"log"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"sort"
"strconv"
"strings"
"sync" "sync"
"time" "time"
@ -20,73 +14,30 @@ import (
"github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/internal"
tlsint "github.com/influxdata/telegraf/internal/tls" tlsint "github.com/influxdata/telegraf/internal/tls"
"github.com/influxdata/telegraf/plugins/outputs" "github.com/influxdata/telegraf/plugins/outputs"
"github.com/influxdata/telegraf/plugins/outputs/prometheus_client/v1"
"github.com/influxdata/telegraf/plugins/outputs/prometheus_client/v2"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
) )
var ( var (
invalidNameCharRE = regexp.MustCompile(`[^a-zA-Z0-9_:]`) defaultListen = ":9273"
validNameCharRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*`) defaultPath = "/metrics"
defaultExpirationInterval = internal.Duration{Duration: 60 * time.Second}
) )
// SampleID uniquely identifies a Sample
type SampleID string
// Sample represents the current value of a series.
type Sample struct {
// Labels are the Prometheus labels.
Labels map[string]string
// Value is the value in the Prometheus output. Only one of these will populated.
Value float64
HistogramValue map[float64]uint64
SummaryValue map[float64]float64
// Histograms and Summaries need a count and a sum
Count uint64
Sum float64
// Metric timestamp
Timestamp time.Time
// Expiration is the deadline that this Sample is valid until.
Expiration time.Time
}
// MetricFamily contains the data required to build valid prometheus Metrics.
type MetricFamily struct {
// Samples are the Sample belonging to this MetricFamily.
Samples map[SampleID]*Sample
// Need the telegraf ValueType because there isn't a Prometheus ValueType
// representing Histogram or Summary
TelegrafValueType telegraf.ValueType
// LabelSet is the label counts for all Samples.
LabelSet map[string]int
}
type PrometheusClient struct {
Listen string
BasicUsername string `toml:"basic_username"`
BasicPassword string `toml:"basic_password"`
IPRange []string `toml:"ip_range"`
ExpirationInterval internal.Duration `toml:"expiration_interval"`
Path string `toml:"path"`
CollectorsExclude []string `toml:"collectors_exclude"`
StringAsLabel bool `toml:"string_as_label"`
ExportTimestamp bool `toml:"export_timestamp"`
tlsint.ServerConfig
server *http.Server
url string
sync.Mutex
// fam is the non-expired MetricFamily by Prometheus metric name.
fam map[string]*MetricFamily
// now returns the current time.
now func() time.Time
}
var sampleConfig = ` var sampleConfig = `
## Address to listen on ## Address to listen on
listen = ":9273" listen = ":9273"
## Metric version controls the mapping from Telegraf metrics into
## Prometheus format. When using the prometheus input, use the same value in
## both plugins to ensure metrics are round-tripped without modification.
##
## example: metric_version = 1; deprecated in 1.13
## metric_version = 2; recommended version
# metric_version = 1
## Use HTTP Basic Authentication. ## Use HTTP Basic Authentication.
# basic_username = "Foo" # basic_username = "Foo"
# basic_password = "Bar" # basic_password = "Bar"
@ -121,46 +72,42 @@ var sampleConfig = `
# export_timestamp = false # export_timestamp = false
` `
func (p *PrometheusClient) auth(h http.Handler) http.Handler { type Collector interface {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Describe(ch chan<- *prometheus.Desc)
if p.BasicUsername != "" && p.BasicPassword != "" { Collect(ch chan<- prometheus.Metric)
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) Add(metrics []telegraf.Metric) error
username, password, ok := r.BasicAuth()
if !ok ||
subtle.ConstantTimeCompare([]byte(username), []byte(p.BasicUsername)) != 1 ||
subtle.ConstantTimeCompare([]byte(password), []byte(p.BasicPassword)) != 1 {
http.Error(w, "Not authorized", 401)
return
}
}
if len(p.IPRange) > 0 {
matched := false
remoteIPs, _, _ := net.SplitHostPort(r.RemoteAddr)
remoteIP := net.ParseIP(remoteIPs)
for _, iprange := range p.IPRange {
_, ipNet, err := net.ParseCIDR(iprange)
if err != nil {
http.Error(w, "Config Error in ip_range setting", 500)
return
}
if ipNet.Contains(remoteIP) {
matched = true
break
}
}
if !matched {
http.Error(w, "Not authorized", 401)
return
}
}
h.ServeHTTP(w, r)
})
} }
func (p *PrometheusClient) Connect() error { type PrometheusClient struct {
Listen string `toml:"listen"`
MetricVersion int `toml:"metric_version"`
BasicUsername string `toml:"basic_username"`
BasicPassword string `toml:"basic_password"`
IPRange []string `toml:"ip_range"`
ExpirationInterval internal.Duration `toml:"expiration_interval"`
Path string `toml:"path"`
CollectorsExclude []string `toml:"collectors_exclude"`
StringAsLabel bool `toml:"string_as_label"`
ExportTimestamp bool `toml:"export_timestamp"`
tlsint.ServerConfig
Log telegraf.Logger `toml:"-"`
server *http.Server
url *url.URL
collector Collector
wg sync.WaitGroup
}
func (p *PrometheusClient) Description() string {
return "Configuration for the Prometheus client to spawn"
}
func (p *PrometheusClient) SampleConfig() string {
return sampleConfig
}
func (p *PrometheusClient) Init() error {
defaultCollectors := map[string]bool{ defaultCollectors := map[string]bool{
"gocollector": true, "gocollector": true,
"process": true, "process": true,
@ -181,421 +128,137 @@ func (p *PrometheusClient) Connect() error {
} }
} }
err := registry.Register(p) switch p.MetricVersion {
if err != nil { default:
return err fallthrough
case 1:
p.Log.Warnf("Use of deprecated configuration: metric_version = 1; please update to metric_version = 2")
p.collector = v1.NewCollector(p.ExpirationInterval.Duration, p.StringAsLabel, p.Log)
err := registry.Register(p.collector)
if err != nil {
return err
}
case 2:
p.collector = v2.NewCollector(p.ExpirationInterval.Duration, p.StringAsLabel)
err := registry.Register(p.collector)
if err != nil {
return err
}
} }
if p.Listen == "" { ipRange := make([]*net.IPNet, 0, len(p.IPRange))
p.Listen = "localhost:9273" for _, cidr := range p.IPRange {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return fmt.Errorf("error parsing ip_range: %v", err)
}
ipRange = append(ipRange, ipNet)
} }
if p.Path == "" { authHandler := internal.AuthHandler(p.BasicUsername, p.BasicPassword, onAuthError)
p.Path = "/metrics" rangeHandler := internal.IPRangeHandler(ipRange, onError)
} promHandler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError})
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle(p.Path, p.auth(promhttp.HandlerFor( if p.Path == "" {
registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}))) p.Path = "/"
}
mux.Handle(p.Path, authHandler(rangeHandler(promHandler)))
tlsConfig, err := p.TLSConfig() tlsConfig, err := p.TLSConfig()
if err != nil { if err != nil {
return err return err
} }
p.server = &http.Server{ p.server = &http.Server{
Addr: p.Listen, Addr: p.Listen,
Handler: mux, Handler: mux,
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
} }
var listener net.Listener return nil
if tlsConfig != nil { }
listener, err = tls.Listen("tcp", p.Listen, tlsConfig)
func (p *PrometheusClient) listen() (net.Listener, error) {
if p.server.TLSConfig != nil {
return tls.Listen("tcp", p.Listen, p.server.TLSConfig)
} else { } else {
listener, err = net.Listen("tcp", p.Listen) return net.Listen("tcp", p.Listen)
} }
}
func (p *PrometheusClient) Connect() error {
listener, err := p.listen()
if err != nil { if err != nil {
return err return err
} }
p.url = createURL(tlsConfig, listener, p.Path) scheme := "http"
if p.server.TLSConfig != nil {
scheme = "https"
}
p.url = &url.URL{
Scheme: scheme,
Host: listener.Addr().String(),
Path: p.Path,
}
p.Log.Infof("Listening on %s", p.URL())
p.wg.Add(1)
go func() { go func() {
defer p.wg.Done()
err := p.server.Serve(listener) err := p.server.Serve(listener)
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed {
log.Printf("E! Error creating prometheus metric endpoint, err: %s\n", p.Log.Errorf("Server error: %v", err)
err.Error())
} }
}() }()
return nil return nil
} }
func onAuthError(rw http.ResponseWriter, code int) {
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(rw, http.StatusText(code), code)
}
func onError(rw http.ResponseWriter, code int) {
http.Error(rw, http.StatusText(code), code)
}
// Address returns the address the plugin is listening on. If not listening // Address returns the address the plugin is listening on. If not listening
// an empty string is returned. // an empty string is returned.
func (p *PrometheusClient) URL() string { func (p *PrometheusClient) URL() string {
return p.url if p.url != nil {
} return p.url.String()
func createURL(tlsConfig *tls.Config, listener net.Listener, path string) string {
u := url.URL{
Scheme: "http",
Host: listener.Addr().String(),
Path: path,
} }
return ""
if tlsConfig != nil {
u.Scheme = "https"
}
return u.String()
} }
func (p *PrometheusClient) Close() error { func (p *PrometheusClient) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
err := p.server.Shutdown(ctx) err := p.server.Shutdown(ctx)
prometheus.Unregister(p) p.wg.Wait()
p.url = "" p.url = nil
prometheus.Unregister(p.collector)
return err return err
} }
func (p *PrometheusClient) SampleConfig() string {
return sampleConfig
}
func (p *PrometheusClient) Description() string {
return "Configuration for the Prometheus client to spawn"
}
// Implements prometheus.Collector
func (p *PrometheusClient) Describe(ch chan<- *prometheus.Desc) {
prometheus.NewGauge(prometheus.GaugeOpts{Name: "Dummy", Help: "Dummy"}).Describe(ch)
}
// Expire removes Samples that have expired.
func (p *PrometheusClient) Expire() {
now := p.now()
for name, family := range p.fam {
for key, sample := range family.Samples {
if p.ExpirationInterval.Duration != 0 && now.After(sample.Expiration) {
for k := range sample.Labels {
family.LabelSet[k]--
}
delete(family.Samples, key)
if len(family.Samples) == 0 {
delete(p.fam, name)
}
}
}
}
}
// Collect implements prometheus.Collector
func (p *PrometheusClient) Collect(ch chan<- prometheus.Metric) {
p.Lock()
defer p.Unlock()
p.Expire()
for name, family := range p.fam {
// Get list of all labels on MetricFamily
var labelNames []string
for k, v := range family.LabelSet {
if v > 0 {
labelNames = append(labelNames, k)
}
}
desc := prometheus.NewDesc(name, "Telegraf collected metric", labelNames, nil)
for _, sample := range family.Samples {
// Get labels for this sample; unset labels will be set to the
// empty string
var labels []string
for _, label := range labelNames {
v := sample.Labels[label]
labels = append(labels, v)
}
var metric prometheus.Metric
var err error
switch family.TelegrafValueType {
case telegraf.Summary:
metric, err = prometheus.NewConstSummary(desc, sample.Count, sample.Sum, sample.SummaryValue, labels...)
case telegraf.Histogram:
metric, err = prometheus.NewConstHistogram(desc, sample.Count, sample.Sum, sample.HistogramValue, labels...)
default:
metric, err = prometheus.NewConstMetric(desc, getPromValueType(family.TelegrafValueType), sample.Value, labels...)
}
if err != nil {
log.Printf("E! Error creating prometheus metric, "+
"key: %s, labels: %v,\nerr: %s\n",
name, labels, err.Error())
continue
}
if p.ExportTimestamp {
metric = prometheus.NewMetricWithTimestamp(sample.Timestamp, metric)
}
ch <- metric
}
}
}
func sanitize(value string) string {
return invalidNameCharRE.ReplaceAllString(value, "_")
}
func isValidTagName(tag string) bool {
return validNameCharRE.MatchString(tag)
}
func getPromValueType(tt telegraf.ValueType) prometheus.ValueType {
switch tt {
case telegraf.Counter:
return prometheus.CounterValue
case telegraf.Gauge:
return prometheus.GaugeValue
default:
return prometheus.UntypedValue
}
}
// CreateSampleID creates a SampleID based on the tags of a telegraf.Metric.
func CreateSampleID(tags map[string]string) SampleID {
pairs := make([]string, 0, len(tags))
for k, v := range tags {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
}
sort.Strings(pairs)
return SampleID(strings.Join(pairs, ","))
}
func addSample(fam *MetricFamily, sample *Sample, sampleID SampleID) {
for k := range sample.Labels {
fam.LabelSet[k]++
}
fam.Samples[sampleID] = sample
}
func (p *PrometheusClient) addMetricFamily(point telegraf.Metric, sample *Sample, mname string, sampleID SampleID) {
var fam *MetricFamily
var ok bool
if fam, ok = p.fam[mname]; !ok {
fam = &MetricFamily{
Samples: make(map[SampleID]*Sample),
TelegrafValueType: point.Type(),
LabelSet: make(map[string]int),
}
p.fam[mname] = fam
}
addSample(fam, sample, sampleID)
}
// Sorted returns a copy of the metrics in time ascending order. A copy is
// made to avoid modifying the input metric slice since doing so is not
// allowed.
func sorted(metrics []telegraf.Metric) []telegraf.Metric {
batch := make([]telegraf.Metric, 0, len(metrics))
for i := len(metrics) - 1; i >= 0; i-- {
batch = append(batch, metrics[i])
}
sort.Slice(batch, func(i, j int) bool {
return batch[i].Time().Before(batch[j].Time())
})
return batch
}
func (p *PrometheusClient) Write(metrics []telegraf.Metric) error { func (p *PrometheusClient) Write(metrics []telegraf.Metric) error {
p.Lock() return p.collector.Add(metrics)
defer p.Unlock()
now := p.now()
for _, point := range sorted(metrics) {
tags := point.Tags()
sampleID := CreateSampleID(tags)
labels := make(map[string]string)
for k, v := range tags {
tName := sanitize(k)
if !isValidTagName(tName) {
continue
}
labels[tName] = v
}
// Prometheus doesn't have a string value type, so convert string
// fields to labels if enabled.
if p.StringAsLabel {
for fn, fv := range point.Fields() {
switch fv := fv.(type) {
case string:
tName := sanitize(fn)
if !isValidTagName(tName) {
continue
}
labels[tName] = fv
}
}
}
switch point.Type() {
case telegraf.Summary:
var mname string
var sum float64
var count uint64
summaryvalue := make(map[float64]float64)
for fn, fv := range point.Fields() {
var value float64
switch fv := fv.(type) {
case int64:
value = float64(fv)
case uint64:
value = float64(fv)
case float64:
value = fv
default:
continue
}
switch fn {
case "sum":
sum = value
case "count":
count = uint64(value)
default:
limit, err := strconv.ParseFloat(fn, 64)
if err == nil {
summaryvalue[limit] = value
}
}
}
sample := &Sample{
Labels: labels,
SummaryValue: summaryvalue,
Count: count,
Sum: sum,
Timestamp: point.Time(),
Expiration: now.Add(p.ExpirationInterval.Duration),
}
mname = sanitize(point.Name())
if !isValidTagName(mname) {
continue
}
p.addMetricFamily(point, sample, mname, sampleID)
case telegraf.Histogram:
var mname string
var sum float64
var count uint64
histogramvalue := make(map[float64]uint64)
for fn, fv := range point.Fields() {
var value float64
switch fv := fv.(type) {
case int64:
value = float64(fv)
case uint64:
value = float64(fv)
case float64:
value = fv
default:
continue
}
switch fn {
case "sum":
sum = value
case "count":
count = uint64(value)
default:
limit, err := strconv.ParseFloat(fn, 64)
if err == nil {
histogramvalue[limit] = uint64(value)
}
}
}
sample := &Sample{
Labels: labels,
HistogramValue: histogramvalue,
Count: count,
Sum: sum,
Timestamp: point.Time(),
Expiration: now.Add(p.ExpirationInterval.Duration),
}
mname = sanitize(point.Name())
if !isValidTagName(mname) {
continue
}
p.addMetricFamily(point, sample, mname, sampleID)
default:
for fn, fv := range point.Fields() {
// Ignore string and bool fields.
var value float64
switch fv := fv.(type) {
case int64:
value = float64(fv)
case uint64:
value = float64(fv)
case float64:
value = fv
default:
continue
}
sample := &Sample{
Labels: labels,
Value: value,
Timestamp: point.Time(),
Expiration: now.Add(p.ExpirationInterval.Duration),
}
// Special handling of value field; supports passthrough from
// the prometheus input.
var mname string
switch point.Type() {
case telegraf.Counter:
if fn == "counter" {
mname = sanitize(point.Name())
}
case telegraf.Gauge:
if fn == "gauge" {
mname = sanitize(point.Name())
}
}
if mname == "" {
if fn == "value" {
mname = sanitize(point.Name())
} else {
mname = sanitize(fmt.Sprintf("%s_%s", point.Name(), fn))
}
}
if !isValidTagName(mname) {
continue
}
p.addMetricFamily(point, sample, mname, sampleID)
}
}
}
return nil
} }
func init() { func init() {
outputs.Add("prometheus_client", func() telegraf.Output { outputs.Add("prometheus_client", func() telegraf.Output {
return &PrometheusClient{ return &PrometheusClient{
ExpirationInterval: internal.Duration{Duration: time.Second * 60}, Listen: defaultListen,
Path: defaultPath,
ExpirationInterval: defaultExpirationInterval,
StringAsLabel: true, StringAsLabel: true,
fam: make(map[string]*MetricFamily),
now: time.Now,
} }
}) })
} }

View File

@ -1,693 +1,304 @@
package prometheus_client package prometheus
import ( import (
"io/ioutil"
"net/http"
"strings"
"testing" "testing"
"time" "time"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/metric"
prometheus_input "github.com/influxdata/telegraf/plugins/inputs/prometheus"
"github.com/influxdata/telegraf/testutil" "github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func setUnixTime(client *PrometheusClient, sec int64) { func TestMetricVersion1(t *testing.T) {
client.now = func() time.Time { tests := []struct {
return time.Unix(sec, 0) name string
} output *PrometheusClient
} metrics []telegraf.Metric
expected []byte
// NewClient initializes a PrometheusClient.
func NewClient() *PrometheusClient {
return &PrometheusClient{
ExpirationInterval: internal.Duration{Duration: time.Second * 60},
StringAsLabel: true,
fam: make(map[string]*MetricFamily),
now: time.Now,
}
}
func TestWrite_Basic(t *testing.T) {
now := time.Now()
pt1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 0.0},
now)
var metrics = []telegraf.Metric{
pt1,
}
client := NewClient()
err = client.Write(metrics)
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, telegraf.Untyped, fam.TelegrafValueType)
require.Equal(t, map[string]int{}, fam.LabelSet)
sample, ok := fam.Samples[CreateSampleID(pt1.Tags())]
require.True(t, ok)
require.Equal(t, 0.0, sample.Value)
require.True(t, now.Before(sample.Expiration))
}
func TestWrite_IntField(t *testing.T) {
client := NewClient()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 42},
time.Now())
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
for _, v := range fam.Samples {
require.Equal(t, 42.0, v.Value)
}
}
func TestWrite_FieldNotValue(t *testing.T) {
client := NewClient()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"howdy": 0.0},
time.Now())
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
fam, ok := client.fam["foo_howdy"]
require.True(t, ok)
for _, v := range fam.Samples {
require.Equal(t, 0.0, v.Value)
}
}
func TestWrite_SkipNonNumberField(t *testing.T) {
client := NewClient()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": "howdy"},
time.Now())
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
_, ok := client.fam["foo"]
require.False(t, ok)
}
func TestWrite_Counters(t *testing.T) {
type args struct {
measurement string
tags map[string]string
fields map[string]interface{}
valueType telegraf.ValueType
}
var tests = []struct {
name string
args args
err error
metricName string
valueType telegraf.ValueType
}{ }{
{ {
name: "field named value is not added to metric name", name: "simple",
args: args{ output: &PrometheusClient{
measurement: "foo", Listen: ":0",
fields: map[string]interface{}{"value": 42}, MetricVersion: 1,
valueType: telegraf.Counter, CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: testutil.Logger{},
}, },
metricName: "foo", metrics: []telegraf.Metric{
valueType: telegraf.Counter, testutil.MustMetric(
"cpu",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
}, },
{ {
name: "field named counter is not added to metric name", name: "prometheus untyped",
args: args{ output: &PrometheusClient{
measurement: "foo", Listen: ":0",
fields: map[string]interface{}{"counter": 42}, MetricVersion: 1,
valueType: telegraf.Counter, CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: testutil.Logger{},
}, },
metricName: "foo", metrics: []telegraf.Metric{
valueType: telegraf.Counter, testutil.MustMetric(
"cpu_time_idle",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"value": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
}, },
{ {
name: "field with any other name is added to metric name", name: "prometheus counter",
args: args{ output: &PrometheusClient{
measurement: "foo", Listen: ":0",
fields: map[string]interface{}{"other": 42}, MetricVersion: 1,
valueType: telegraf.Counter, CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: testutil.Logger{},
}, },
metricName: "foo_other", metrics: []telegraf.Metric{
valueType: telegraf.Counter, testutil.MustMetric(
"cpu_time_idle",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"counter": 42.0,
},
time.Unix(0, 0),
telegraf.Counter,
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle counter
cpu_time_idle{host="example.org"} 42
`),
}, },
{ {
name: "uint64 fields are output", name: "prometheus gauge",
args: args{ output: &PrometheusClient{
measurement: "foo", Listen: ":0",
fields: map[string]interface{}{"value": uint64(42)}, MetricVersion: 1,
valueType: telegraf.Counter, CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: testutil.Logger{},
}, },
metricName: "foo", metrics: []telegraf.Metric{
valueType: telegraf.Counter, testutil.MustMetric(
"cpu_time_idle",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"gauge": 42.0,
},
time.Unix(0, 0),
telegraf.Gauge,
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "prometheus histogram",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: testutil.Logger{},
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"http_request_duration_seconds",
map[string]string{},
map[string]interface{}{
"sum": 53423,
"0.05": 24054,
"0.1": 33444,
"0.2": 100392,
"0.5": 129389,
"1": 133988,
"+Inf": 144320,
"count": 144320,
},
time.Unix(0, 0),
telegraf.Histogram,
),
},
expected: []byte(`
# HELP http_request_duration_seconds Telegraf collected metric
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 24054
http_request_duration_seconds_bucket{le="0.1"} 33444
http_request_duration_seconds_bucket{le="0.2"} 100392
http_request_duration_seconds_bucket{le="0.5"} 129389
http_request_duration_seconds_bucket{le="1"} 133988
http_request_duration_seconds_bucket{le="+Inf"} 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_count 144320
`),
},
{
name: "prometheus summary",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: testutil.Logger{},
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"rpc_duration_seconds",
map[string]string{},
map[string]interface{}{
"0.01": 3102,
"0.05": 3272,
"0.5": 4773,
"0.9": 9001,
"0.99": 76656,
"count": 2693,
"sum": 17560473,
},
time.Unix(0, 0),
telegraf.Summary,
),
},
expected: []byte(`
# HELP rpc_duration_seconds Telegraf collected metric
# TYPE rpc_duration_seconds summary
rpc_duration_seconds{quantile="0.01"} 3102
rpc_duration_seconds{quantile="0.05"} 3272
rpc_duration_seconds{quantile="0.5"} 4773
rpc_duration_seconds{quantile="0.9"} 9001
rpc_duration_seconds{quantile="0.99"} 76656
rpc_duration_seconds_sum 1.7560473e+07
rpc_duration_seconds_count 2693
`),
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
m, err := metric.New( err := tt.output.Init()
tt.args.measurement, require.NoError(t, err)
tt.args.tags,
tt.args.fields,
time.Now(),
tt.args.valueType,
)
client := NewClient()
err = client.Write([]telegraf.Metric{m})
require.Equal(t, tt.err, err)
fam, ok := client.fam[tt.metricName] err = tt.output.Connect()
require.True(t, ok) require.NoError(t, err)
require.Equal(t, tt.valueType, fam.TelegrafValueType)
defer func() {
err := tt.output.Close()
require.NoError(t, err)
}()
err = tt.output.Write(tt.metrics)
require.NoError(t, err)
resp, err := http.Get(tt.output.URL())
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t,
strings.TrimSpace(string(tt.expected)),
strings.TrimSpace(string(body)))
}) })
} }
} }
func TestWrite_Sanitize(t *testing.T) { func TestMetricVersion2(t *testing.T) {
client := NewClient() tests := []struct {
name string
p1, err := metric.New( output *PrometheusClient
"foo.bar:colon", metrics []telegraf.Metric
map[string]string{"tag-with-dash": "localhost.local"}, expected []byte
map[string]interface{}{"field-with-dash-and:colon": 42},
time.Now(),
telegraf.Counter)
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
fam, ok := client.fam["foo_bar:colon_field_with_dash_and:colon"]
require.True(t, ok)
require.Equal(t, map[string]int{"tag_with_dash": 1}, fam.LabelSet)
sample1, ok := fam.Samples[CreateSampleID(p1.Tags())]
require.True(t, ok)
require.Equal(t, map[string]string{
"tag_with_dash": "localhost.local"}, sample1.Labels)
}
func TestWrite_Gauge(t *testing.T) {
type args struct {
measurement string
tags map[string]string
fields map[string]interface{}
valueType telegraf.ValueType
}
var tests = []struct {
name string
args args
err error
metricName string
valueType telegraf.ValueType
}{ }{
{ {
name: "field named value is not added to metric name", name: "simple",
args: args{ output: &PrometheusClient{
measurement: "foo", Listen: ":0",
fields: map[string]interface{}{"value": 42}, MetricVersion: 2,
valueType: telegraf.Gauge, CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: testutil.Logger{},
}, },
metricName: "foo", metrics: []telegraf.Metric{
valueType: telegraf.Gauge, testutil.MustMetric(
}, "cpu",
{ map[string]string{
name: "field named gauge is not added to metric name", "host": "example.org",
args: args{ },
measurement: "foo", map[string]interface{}{
fields: map[string]interface{}{"gauge": 42}, "time_idle": 42.0,
valueType: telegraf.Gauge, },
time.Unix(0, 0),
),
}, },
metricName: "foo", expected: []byte(`
valueType: telegraf.Gauge, # HELP cpu_time_idle Telegraf collected metric
}, # TYPE cpu_time_idle untyped
{ cpu_time_idle{host="example.org"} 42
name: "field with any other name is added to metric name", `),
args: args{
measurement: "foo",
fields: map[string]interface{}{"other": 42},
valueType: telegraf.Gauge,
},
metricName: "foo_other",
valueType: telegraf.Gauge,
},
{
name: "uint64 fields are output",
args: args{
measurement: "foo",
fields: map[string]interface{}{"value": uint64(42)},
valueType: telegraf.Counter,
},
metricName: "foo",
valueType: telegraf.Counter,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
m, err := metric.New( err := tt.output.Init()
tt.args.measurement, require.NoError(t, err)
tt.args.tags,
tt.args.fields,
time.Now(),
tt.args.valueType,
)
client := NewClient()
err = client.Write([]telegraf.Metric{m})
require.Equal(t, tt.err, err)
fam, ok := client.fam[tt.metricName] err = tt.output.Connect()
require.True(t, ok) require.NoError(t, err)
require.Equal(t, tt.valueType, fam.TelegrafValueType)
defer func() {
err := tt.output.Close()
require.NoError(t, err)
}()
err = tt.output.Write(tt.metrics)
require.NoError(t, err)
resp, err := http.Get(tt.output.URL())
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t,
strings.TrimSpace(string(tt.expected)),
strings.TrimSpace(string(body)))
}) })
} }
} }
func TestWrite_Summary(t *testing.T) {
client := NewClient()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"sum": 84, "count": 42, "0": 2, "0.5": 3, "1": 4},
time.Now(),
telegraf.Summary)
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 1, len(fam.Samples))
sample1, ok := fam.Samples[CreateSampleID(p1.Tags())]
require.True(t, ok)
require.Equal(t, 84.0, sample1.Sum)
require.Equal(t, uint64(42), sample1.Count)
require.Equal(t, 3, len(sample1.SummaryValue))
}
func TestWrite_Histogram(t *testing.T) {
client := NewClient()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"sum": 84, "count": 42, "0": 2, "0.5": 3, "1": 4},
time.Now(),
telegraf.Histogram)
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 1, len(fam.Samples))
sample1, ok := fam.Samples[CreateSampleID(p1.Tags())]
require.True(t, ok)
require.Equal(t, 84.0, sample1.Sum)
require.Equal(t, uint64(42), sample1.Count)
require.Equal(t, 3, len(sample1.HistogramValue))
}
func TestWrite_MixedValueType(t *testing.T) {
now := time.Now()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 1.0},
now,
telegraf.Counter)
p2, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 2.0},
now,
telegraf.Gauge)
var metrics = []telegraf.Metric{p1, p2}
client := NewClient()
err = client.Write(metrics)
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 1, len(fam.Samples))
}
func TestWrite_MixedValueTypeUpgrade(t *testing.T) {
now := time.Now()
p1, err := metric.New(
"foo",
map[string]string{"a": "x"},
map[string]interface{}{"value": 1.0},
now,
telegraf.Untyped)
p2, err := metric.New(
"foo",
map[string]string{"a": "y"},
map[string]interface{}{"value": 2.0},
now,
telegraf.Gauge)
var metrics = []telegraf.Metric{p1, p2}
client := NewClient()
err = client.Write(metrics)
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 2, len(fam.Samples))
}
func TestWrite_MixedValueTypeDowngrade(t *testing.T) {
now := time.Now()
p1, err := metric.New(
"foo",
map[string]string{"a": "x"},
map[string]interface{}{"value": 1.0},
now,
telegraf.Gauge)
p2, err := metric.New(
"foo",
map[string]string{"a": "y"},
map[string]interface{}{"value": 2.0},
now,
telegraf.Untyped)
var metrics = []telegraf.Metric{p1, p2}
client := NewClient()
err = client.Write(metrics)
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 2, len(fam.Samples))
}
func TestWrite_Tags(t *testing.T) {
now := time.Now()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 1.0},
now)
p2, err := metric.New(
"foo",
map[string]string{"host": "localhost"},
map[string]interface{}{"value": 2.0},
now)
var metrics = []telegraf.Metric{p1, p2}
client := NewClient()
err = client.Write(metrics)
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, telegraf.Untyped, fam.TelegrafValueType)
require.Equal(t, map[string]int{"host": 1}, fam.LabelSet)
sample1, ok := fam.Samples[CreateSampleID(p1.Tags())]
require.True(t, ok)
require.Equal(t, 1.0, sample1.Value)
require.True(t, now.Before(sample1.Expiration))
sample2, ok := fam.Samples[CreateSampleID(p2.Tags())]
require.True(t, ok)
require.Equal(t, 2.0, sample2.Value)
require.True(t, now.Before(sample2.Expiration))
}
func TestWrite_StringFields(t *testing.T) {
now := time.Now()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 1.0, "status": "good"},
now,
telegraf.Counter)
p2, err := metric.New(
"bar",
make(map[string]string),
map[string]interface{}{"status": "needs numeric field"},
now,
telegraf.Gauge)
var metrics = []telegraf.Metric{p1, p2}
client := NewClient()
err = client.Write(metrics)
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 1, fam.LabelSet["status"])
fam, ok = client.fam["bar"]
require.False(t, ok)
}
func TestDoNotWrite_StringFields(t *testing.T) {
now := time.Now()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 1.0, "status": "good"},
now,
telegraf.Counter)
p2, err := metric.New(
"bar",
make(map[string]string),
map[string]interface{}{"status": "needs numeric field"},
now,
telegraf.Gauge)
var metrics = []telegraf.Metric{p1, p2}
client := &PrometheusClient{
ExpirationInterval: internal.Duration{Duration: time.Second * 60},
StringAsLabel: false,
fam: make(map[string]*MetricFamily),
now: time.Now,
}
err = client.Write(metrics)
require.NoError(t, err)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 0, fam.LabelSet["status"])
fam, ok = client.fam["bar"]
require.False(t, ok)
}
func TestExpire(t *testing.T) {
client := NewClient()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 1.0},
time.Now())
setUnixTime(client, 0)
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
p2, err := metric.New(
"bar",
make(map[string]string),
map[string]interface{}{"value": 2.0},
time.Now())
setUnixTime(client, 1)
err = client.Write([]telegraf.Metric{p2})
setUnixTime(client, 61)
require.Equal(t, 2, len(client.fam))
client.Expire()
require.Equal(t, 1, len(client.fam))
}
func TestExpire_TagsNoDecrement(t *testing.T) {
client := NewClient()
p1, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 1.0},
time.Now())
setUnixTime(client, 0)
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
p2, err := metric.New(
"foo",
map[string]string{"host": "localhost"},
map[string]interface{}{"value": 2.0},
time.Now())
setUnixTime(client, 1)
err = client.Write([]telegraf.Metric{p2})
setUnixTime(client, 61)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 2, len(fam.Samples))
client.Expire()
require.Equal(t, 1, len(fam.Samples))
require.Equal(t, map[string]int{"host": 1}, fam.LabelSet)
}
func TestExpire_TagsWithDecrement(t *testing.T) {
client := NewClient()
p1, err := metric.New(
"foo",
map[string]string{"host": "localhost"},
map[string]interface{}{"value": 1.0},
time.Now())
setUnixTime(client, 0)
err = client.Write([]telegraf.Metric{p1})
require.NoError(t, err)
p2, err := metric.New(
"foo",
make(map[string]string),
map[string]interface{}{"value": 2.0},
time.Now())
setUnixTime(client, 1)
err = client.Write([]telegraf.Metric{p2})
setUnixTime(client, 61)
fam, ok := client.fam["foo"]
require.True(t, ok)
require.Equal(t, 2, len(fam.Samples))
client.Expire()
require.Equal(t, 1, len(fam.Samples))
require.Equal(t, map[string]int{"host": 0}, fam.LabelSet)
}
var pTesting *PrometheusClient
func TestPrometheusWritePointEmptyTag(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
pClient, p, err := setupPrometheus()
require.NoError(t, err)
defer pClient.Close()
now := time.Now()
tags := make(map[string]string)
pt1, _ := metric.New(
"test_point_1",
tags,
map[string]interface{}{"value": 0.0},
now)
pt2, _ := metric.New(
"test_point_2",
tags,
map[string]interface{}{"value": 1.0},
now)
var metrics = []telegraf.Metric{
pt1,
pt2,
}
require.NoError(t, pClient.Write(metrics))
expected := []struct {
name string
value float64
tags map[string]string
}{
{"test_point_1", 0.0, tags},
{"test_point_2", 1.0, tags},
}
var acc testutil.Accumulator
require.NoError(t, p.Gather(&acc))
for _, e := range expected {
acc.AssertContainsFields(t, e.name,
map[string]interface{}{"value": e.value})
}
tags = make(map[string]string)
tags["testtag"] = "testvalue"
pt3, _ := metric.New(
"test_point_3",
tags,
map[string]interface{}{"value": 0.0},
now)
pt4, _ := metric.New(
"test_point_4",
tags,
map[string]interface{}{"value": 1.0},
now)
metrics = []telegraf.Metric{
pt3,
pt4,
}
require.NoError(t, pClient.Write(metrics))
expected2 := []struct {
name string
value float64
}{
{"test_point_3", 0.0},
{"test_point_4", 1.0},
}
require.NoError(t, p.Gather(&acc))
for _, e := range expected2 {
acc.AssertContainsFields(t, e.name,
map[string]interface{}{"value": e.value})
}
}
func setupPrometheus() (*PrometheusClient, *prometheus_input.Prometheus, error) {
if pTesting == nil {
pTesting = NewClient()
pTesting.Listen = "localhost:9127"
pTesting.Path = "/metrics"
err := pTesting.Connect()
if err != nil {
return nil, nil, err
}
} else {
pTesting.fam = make(map[string]*MetricFamily)
}
time.Sleep(time.Millisecond * 200)
p := &prometheus_input.Prometheus{
URLs: []string{"http://localhost:9127/metrics"},
}
return pTesting, p, nil
}

View File

@ -0,0 +1,391 @@
package v1
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/influxdata/telegraf"
"github.com/prometheus/client_golang/prometheus"
)
var (
invalidNameCharRE = regexp.MustCompile(`[^a-zA-Z0-9_:]`)
validNameCharRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*`)
)
// SampleID uniquely identifies a Sample
type SampleID string
// Sample represents the current value of a series.
type Sample struct {
// Labels are the Prometheus labels.
Labels map[string]string
// Value is the value in the Prometheus output. Only one of these will populated.
Value float64
HistogramValue map[float64]uint64
SummaryValue map[float64]float64
// Histograms and Summaries need a count and a sum
Count uint64
Sum float64
// Metric timestamp
Timestamp time.Time
// Expiration is the deadline that this Sample is valid until.
Expiration time.Time
}
// MetricFamily contains the data required to build valid prometheus Metrics.
type MetricFamily struct {
// Samples are the Sample belonging to this MetricFamily.
Samples map[SampleID]*Sample
// Need the telegraf ValueType because there isn't a Prometheus ValueType
// representing Histogram or Summary
TelegrafValueType telegraf.ValueType
// LabelSet is the label counts for all Samples.
LabelSet map[string]int
}
type Collector struct {
ExpirationInterval time.Duration
StringAsLabel bool
ExportTimestamp bool
Log telegraf.Logger
sync.Mutex
fam map[string]*MetricFamily
}
func NewCollector(expire time.Duration, stringsAsLabel bool, logger telegraf.Logger) *Collector {
return &Collector{
ExpirationInterval: expire,
StringAsLabel: stringsAsLabel,
Log: logger,
fam: make(map[string]*MetricFamily),
}
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
prometheus.NewGauge(prometheus.GaugeOpts{Name: "Dummy", Help: "Dummy"}).Describe(ch)
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
c.Lock()
defer c.Unlock()
c.Expire(time.Now(), c.ExpirationInterval)
for name, family := range c.fam {
// Get list of all labels on MetricFamily
var labelNames []string
for k, v := range family.LabelSet {
if v > 0 {
labelNames = append(labelNames, k)
}
}
desc := prometheus.NewDesc(name, "Telegraf collected metric", labelNames, nil)
for _, sample := range family.Samples {
// Get labels for this sample; unset labels will be set to the
// empty string
var labels []string
for _, label := range labelNames {
v := sample.Labels[label]
labels = append(labels, v)
}
var metric prometheus.Metric
var err error
switch family.TelegrafValueType {
case telegraf.Summary:
metric, err = prometheus.NewConstSummary(desc, sample.Count, sample.Sum, sample.SummaryValue, labels...)
case telegraf.Histogram:
metric, err = prometheus.NewConstHistogram(desc, sample.Count, sample.Sum, sample.HistogramValue, labels...)
default:
metric, err = prometheus.NewConstMetric(desc, getPromValueType(family.TelegrafValueType), sample.Value, labels...)
}
if err != nil {
c.Log.Errorf("Error creating prometheus metric: "+
"key: %s, labels: %v, err: %v",
name, labels, err)
continue
}
if c.ExportTimestamp {
metric = prometheus.NewMetricWithTimestamp(sample.Timestamp, metric)
}
ch <- metric
}
}
}
func sanitize(value string) string {
return invalidNameCharRE.ReplaceAllString(value, "_")
}
func isValidTagName(tag string) bool {
return validNameCharRE.MatchString(tag)
}
func getPromValueType(tt telegraf.ValueType) prometheus.ValueType {
switch tt {
case telegraf.Counter:
return prometheus.CounterValue
case telegraf.Gauge:
return prometheus.GaugeValue
default:
return prometheus.UntypedValue
}
}
// CreateSampleID creates a SampleID based on the tags of a telegraf.Metric.
func CreateSampleID(tags map[string]string) SampleID {
pairs := make([]string, 0, len(tags))
for k, v := range tags {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
}
sort.Strings(pairs)
return SampleID(strings.Join(pairs, ","))
}
func addSample(fam *MetricFamily, sample *Sample, sampleID SampleID) {
for k := range sample.Labels {
fam.LabelSet[k]++
}
fam.Samples[sampleID] = sample
}
func (c *Collector) addMetricFamily(point telegraf.Metric, sample *Sample, mname string, sampleID SampleID) {
var fam *MetricFamily
var ok bool
if fam, ok = c.fam[mname]; !ok {
fam = &MetricFamily{
Samples: make(map[SampleID]*Sample),
TelegrafValueType: point.Type(),
LabelSet: make(map[string]int),
}
c.fam[mname] = fam
}
addSample(fam, sample, sampleID)
}
// Sorted returns a copy of the metrics in time ascending order. A copy is
// made to avoid modifying the input metric slice since doing so is not
// allowed.
func sorted(metrics []telegraf.Metric) []telegraf.Metric {
batch := make([]telegraf.Metric, 0, len(metrics))
for i := len(metrics) - 1; i >= 0; i-- {
batch = append(batch, metrics[i])
}
sort.Slice(batch, func(i, j int) bool {
return batch[i].Time().Before(batch[j].Time())
})
return batch
}
func (c *Collector) Add(metrics []telegraf.Metric) error {
c.Lock()
defer c.Unlock()
now := time.Now()
for _, point := range sorted(metrics) {
tags := point.Tags()
sampleID := CreateSampleID(tags)
labels := make(map[string]string)
for k, v := range tags {
tName := sanitize(k)
if !isValidTagName(tName) {
continue
}
labels[tName] = v
}
// Prometheus doesn't have a string value type, so convert string
// fields to labels if enabled.
if c.StringAsLabel {
for fn, fv := range point.Fields() {
switch fv := fv.(type) {
case string:
tName := sanitize(fn)
if !isValidTagName(tName) {
continue
}
labels[tName] = fv
}
}
}
switch point.Type() {
case telegraf.Summary:
var mname string
var sum float64
var count uint64
summaryvalue := make(map[float64]float64)
for fn, fv := range point.Fields() {
var value float64
switch fv := fv.(type) {
case int64:
value = float64(fv)
case uint64:
value = float64(fv)
case float64:
value = fv
default:
continue
}
switch fn {
case "sum":
sum = value
case "count":
count = uint64(value)
default:
limit, err := strconv.ParseFloat(fn, 64)
if err == nil {
summaryvalue[limit] = value
}
}
}
sample := &Sample{
Labels: labels,
SummaryValue: summaryvalue,
Count: count,
Sum: sum,
Timestamp: point.Time(),
Expiration: now.Add(c.ExpirationInterval),
}
mname = sanitize(point.Name())
if !isValidTagName(mname) {
continue
}
c.addMetricFamily(point, sample, mname, sampleID)
case telegraf.Histogram:
var mname string
var sum float64
var count uint64
histogramvalue := make(map[float64]uint64)
for fn, fv := range point.Fields() {
var value float64
switch fv := fv.(type) {
case int64:
value = float64(fv)
case uint64:
value = float64(fv)
case float64:
value = fv
default:
continue
}
switch fn {
case "sum":
sum = value
case "count":
count = uint64(value)
default:
limit, err := strconv.ParseFloat(fn, 64)
if err == nil {
histogramvalue[limit] = uint64(value)
}
}
}
sample := &Sample{
Labels: labels,
HistogramValue: histogramvalue,
Count: count,
Sum: sum,
Timestamp: point.Time(),
Expiration: now.Add(c.ExpirationInterval),
}
mname = sanitize(point.Name())
if !isValidTagName(mname) {
continue
}
c.addMetricFamily(point, sample, mname, sampleID)
default:
for fn, fv := range point.Fields() {
// Ignore string and bool fields.
var value float64
switch fv := fv.(type) {
case int64:
value = float64(fv)
case uint64:
value = float64(fv)
case float64:
value = fv
default:
continue
}
sample := &Sample{
Labels: labels,
Value: value,
Timestamp: point.Time(),
Expiration: now.Add(c.ExpirationInterval),
}
// Special handling of value field; supports passthrough from
// the prometheus input.
var mname string
switch point.Type() {
case telegraf.Counter:
if fn == "counter" {
mname = sanitize(point.Name())
}
case telegraf.Gauge:
if fn == "gauge" {
mname = sanitize(point.Name())
}
}
if mname == "" {
if fn == "value" {
mname = sanitize(point.Name())
} else {
mname = sanitize(fmt.Sprintf("%s_%s", point.Name(), fn))
}
}
if !isValidTagName(mname) {
continue
}
c.addMetricFamily(point, sample, mname, sampleID)
}
}
}
return nil
}
func (c *Collector) Expire(now time.Time, age time.Duration) {
if age == 0 {
return
}
for name, family := range c.fam {
for key, sample := range family.Samples {
if age != 0 && now.After(sample.Expiration) {
for k := range sample.Labels {
family.LabelSet[k]--
}
delete(family.Samples, key)
if len(family.Samples) == 0 {
delete(c.fam, name)
}
}
}
}
}

View File

@ -0,0 +1,87 @@
package v2
import (
"sync"
"time"
"github.com/influxdata/telegraf"
serializer "github.com/influxdata/telegraf/plugins/serializers/prometheus"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
type Metric struct {
family *dto.MetricFamily
metric *dto.Metric
}
func (m *Metric) Desc() *prometheus.Desc {
labelNames := make([]string, 0, len(m.metric.Label))
for _, label := range m.metric.Label {
labelNames = append(labelNames, *label.Name)
}
desc := prometheus.NewDesc(*m.family.Name, *m.family.Help, labelNames, nil)
return desc
}
func (m *Metric) Write(out *dto.Metric) error {
out.Label = m.metric.Label
out.Counter = m.metric.Counter
out.Untyped = m.metric.Untyped
out.Gauge = m.metric.Gauge
out.Histogram = m.metric.Histogram
out.Summary = m.metric.Summary
out.TimestampMs = m.metric.TimestampMs
return nil
}
type Collector struct {
sync.Mutex
expireDuration time.Duration
coll *serializer.Collection
}
func NewCollector(expire time.Duration, stringsAsLabel bool) *Collector {
config := serializer.FormatConfig{}
if stringsAsLabel {
config.StringHandling = serializer.StringAsLabel
}
return &Collector{
expireDuration: expire,
coll: serializer.NewCollection(config),
}
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
// Sending no descriptor at all marks the Collector as "unchecked",
// i.e. no checks will be performed at registration time, and the
// Collector may yield any Metric it sees fit in its Collect method.
return
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
c.Lock()
defer c.Unlock()
for _, family := range c.coll.GetProto() {
for _, metric := range family.Metric {
ch <- &Metric{family: family, metric: metric}
}
}
}
func (c *Collector) Add(metrics []telegraf.Metric) error {
c.Lock()
defer c.Unlock()
for _, metric := range metrics {
c.coll.Add(metric)
}
if c.expireDuration != 0 {
c.coll.Expire(time.Now(), c.expireDuration)
}
return nil
}

View File

@ -0,0 +1,68 @@
# Prometheus
The `prometheus` data format converts metrics into the Prometheus text
exposition format. When used with the `prometheus` input, the input should be
use the `metric_version = 2` option in order to properly round trip metrics.
**Warning**: When generating histogram and summary types, output may
not be correct if the metric spans multiple batches. This issue can be
somewhat, but not fully, mitigated by using outputs that support writing in
"batch format". When using histogram and summary types, it is recommended to
use only the `prometheus_client` output.
## Configuration
```toml
[[outputs.file]]
files = ["stdout"]
use_batch_format = true
## Include the metric timestamp on each sample.
prometheus_export_timestamp = false
## Sort prometheus metric families and metric samples. Useful for
## debugging.
prometheus_sort_metrics = false
## Output string fields as metric labels; when false string fields are
## discarded.
prometheus_string_as_label = false
## Data format to output.
## Each data format has its own unique set of configuration options, read
## more about them here:
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md
data_format = "prometheus"
```
### Example
**Example Input**
```
cpu,cpu=cpu0 time_guest=8022.6,time_system=26145.98,time_user=92512.89 1574317740000000000
cpu,cpu=cpu1 time_guest=8097.88,time_system=25223.35,time_user=96519.58 1574317740000000000
cpu,cpu=cpu2 time_guest=7386.28,time_system=24870.37,time_user=95631.59 1574317740000000000
cpu,cpu=cpu3 time_guest=7434.19,time_system=24843.71,time_user=93753.88 1574317740000000000
```
**Example Output**
```
# HELP cpu_time_guest Telegraf collected metric
# TYPE cpu_time_guest counter
cpu_time_guest{cpu="cpu0"} 9582.54
cpu_time_guest{cpu="cpu1"} 9660.88
cpu_time_guest{cpu="cpu2"} 8946.45
cpu_time_guest{cpu="cpu3"} 9002.31
# HELP cpu_time_system Telegraf collected metric
# TYPE cpu_time_system counter
cpu_time_system{cpu="cpu0"} 28675.47
cpu_time_system{cpu="cpu1"} 27779.34
cpu_time_system{cpu="cpu2"} 27406.18
cpu_time_system{cpu="cpu3"} 27404.97
# HELP cpu_time_user Telegraf collected metric
# TYPE cpu_time_user counter
cpu_time_user{cpu="cpu0"} 99551.84
cpu_time_user{cpu="cpu1"} 103468.52
cpu_time_user{cpu="cpu2"} 102591.45
cpu_time_user{cpu="cpu3"} 100717.05
```

View File

@ -0,0 +1,464 @@
package prometheus
import (
"hash/fnv"
"sort"
"strconv"
"strings"
"time"
"github.com/gogo/protobuf/proto"
"github.com/influxdata/telegraf"
dto "github.com/prometheus/client_model/go"
)
const helpString = "Telegraf collected metric"
type MetricFamily struct {
Name string
Type telegraf.ValueType
}
type Metric struct {
Labels []LabelPair
Time time.Time
Scaler *Scaler
Histogram *Histogram
Summary *Summary
}
type LabelPair struct {
Name string
Value string
}
type Scaler struct {
Value float64
}
type Bucket struct {
Bound float64
Count uint64
}
type Quantile struct {
Quantile float64
Value float64
}
type Histogram struct {
Buckets []Bucket
Count uint64
Sum float64
}
type Summary struct {
Quantiles []Quantile
Count uint64
Sum float64
}
type MetricKey uint64
func MakeMetricKey(labels []LabelPair) MetricKey {
h := fnv.New64a()
for _, label := range labels {
h.Write([]byte(label.Name))
h.Write([]byte("\x00"))
h.Write([]byte(label.Value))
h.Write([]byte("\x00"))
}
return MetricKey(h.Sum64())
}
type Entry struct {
Family MetricFamily
Metrics map[MetricKey]*Metric
}
type Collection struct {
config FormatConfig
Entries map[MetricFamily]Entry
}
func NewCollection(config FormatConfig) *Collection {
cache := &Collection{
config: config,
Entries: make(map[MetricFamily]Entry),
}
return cache
}
func hasLabel(name string, labels []LabelPair) bool {
for _, label := range labels {
if name == label.Name {
return true
}
}
return false
}
func (c *Collection) createLabels(metric telegraf.Metric) []LabelPair {
labels := make([]LabelPair, 0, len(metric.TagList()))
for _, tag := range metric.TagList() {
// Ignore special tags for histogram and summary types.
switch metric.Type() {
case telegraf.Histogram:
if tag.Key == "le" {
continue
}
case telegraf.Summary:
if tag.Key == "quantile" {
continue
}
}
name, ok := SanitizeName(tag.Key)
if !ok {
continue
}
labels = append(labels, LabelPair{Name: name, Value: tag.Value})
}
if c.config.StringHandling != StringAsLabel {
return labels
}
addedFieldLabel := false
for _, field := range metric.FieldList() {
value, ok := field.Value.(string)
if !ok {
continue
}
name, ok := SanitizeName(field.Key)
if !ok {
continue
}
// If there is a tag with the same name as the string field, discard
// the field and use the tag instead.
if hasLabel(name, labels) {
continue
}
labels = append(labels, LabelPair{Name: name, Value: value})
addedFieldLabel = true
}
if addedFieldLabel {
sort.Slice(labels, func(i, j int) bool {
return labels[i].Name < labels[j].Name
})
}
return labels
}
func (c *Collection) Add(metric telegraf.Metric) {
labels := c.createLabels(metric)
for _, field := range metric.FieldList() {
metricName := MetricName(metric.Name(), field.Key, metric.Type())
metricName, ok := SanitizeName(metricName)
if !ok {
continue
}
family := MetricFamily{
Name: metricName,
Type: metric.Type(),
}
entry, ok := c.Entries[family]
if !ok {
entry = Entry{
Family: family,
Metrics: make(map[MetricKey]*Metric),
}
c.Entries[family] = entry
}
metricKey := MakeMetricKey(labels)
m, ok := entry.Metrics[metricKey]
if ok {
// A batch of metrics can contain multiple values for a single
// Prometheus sample. If this metric is older than the existing
// sample then we can skip over it.
if metric.Time().Before(m.Time) {
continue
}
}
switch metric.Type() {
case telegraf.Counter:
fallthrough
case telegraf.Gauge:
fallthrough
case telegraf.Untyped:
value, ok := SampleValue(field.Value)
if !ok {
continue
}
m = &Metric{
Labels: labels,
Time: metric.Time(),
Scaler: &Scaler{Value: value},
}
// what if already here
entry.Metrics[metricKey] = m
case telegraf.Histogram:
if m == nil {
m = &Metric{
Labels: labels,
Time: metric.Time(),
Histogram: &Histogram{},
}
}
switch {
case strings.HasSuffix(field.Key, "_bucket"):
le, ok := metric.GetTag("le")
if !ok {
continue
}
bound, err := strconv.ParseFloat(le, 64)
if err != nil {
continue
}
count, ok := SampleCount(field.Value)
if !ok {
continue
}
m.Histogram.Buckets = append(m.Histogram.Buckets, Bucket{
Bound: bound,
Count: count,
})
case strings.HasSuffix(field.Key, "_sum"):
sum, ok := SampleSum(field.Value)
if !ok {
continue
}
m.Histogram.Sum = sum
case strings.HasSuffix(field.Key, "_count"):
count, ok := SampleCount(field.Value)
if !ok {
continue
}
m.Histogram.Count = count
default:
continue
}
entry.Metrics[metricKey] = m
case telegraf.Summary:
if m == nil {
m = &Metric{
Labels: labels,
Time: metric.Time(),
Summary: &Summary{},
}
}
switch {
case strings.HasSuffix(field.Key, "_sum"):
sum, ok := SampleSum(field.Value)
if !ok {
continue
}
m.Summary.Sum = sum
case strings.HasSuffix(field.Key, "_count"):
count, ok := SampleCount(field.Value)
if !ok {
continue
}
m.Summary.Count = count
default:
quantileTag, ok := metric.GetTag("quantile")
if !ok {
continue
}
quantile, err := strconv.ParseFloat(quantileTag, 64)
if err != nil {
continue
}
value, ok := SampleValue(field.Value)
if !ok {
continue
}
m.Summary.Quantiles = append(m.Summary.Quantiles, Quantile{
Quantile: quantile,
Value: value,
})
}
entry.Metrics[metricKey] = m
}
}
}
func (c *Collection) Expire(now time.Time, age time.Duration) {
expireTime := now.Add(-age)
for _, entry := range c.Entries {
for key, metric := range entry.Metrics {
if metric.Time.Before(expireTime) {
delete(entry.Metrics, key)
if len(entry.Metrics) == 0 {
delete(c.Entries, entry.Family)
}
}
}
}
}
func (c *Collection) GetEntries(order MetricSortOrder) []Entry {
entries := make([]Entry, 0, len(c.Entries))
for _, entry := range c.Entries {
entries = append(entries, entry)
}
switch order {
case SortMetrics:
sort.Slice(entries, func(i, j int) bool {
lhs := entries[i].Family
rhs := entries[j].Family
if lhs.Name != rhs.Name {
return lhs.Name < rhs.Name
}
return lhs.Type < rhs.Type
})
}
return entries
}
func (c *Collection) GetMetrics(entry Entry, order MetricSortOrder) []*Metric {
metrics := make([]*Metric, 0, len(entry.Metrics))
for _, metric := range entry.Metrics {
metrics = append(metrics, metric)
}
switch order {
case SortMetrics:
sort.Slice(metrics, func(i, j int) bool {
lhs := metrics[i].Labels
rhs := metrics[j].Labels
if len(lhs) != len(rhs) {
return len(lhs) < len(rhs)
}
for index := range lhs {
l := lhs[index]
r := rhs[index]
if l.Name != r.Name {
return l.Name < r.Name
}
if l.Value != r.Value {
return l.Value < r.Value
}
}
return false
})
}
return metrics
}
func (c *Collection) GetProto() []*dto.MetricFamily {
result := make([]*dto.MetricFamily, 0, len(c.Entries))
for _, entry := range c.GetEntries(c.config.MetricSortOrder) {
mf := &dto.MetricFamily{
Name: proto.String(entry.Family.Name),
Help: proto.String(helpString),
Type: MetricType(entry.Family.Type),
}
for _, metric := range c.GetMetrics(entry, c.config.MetricSortOrder) {
l := make([]*dto.LabelPair, 0, len(metric.Labels))
for _, label := range metric.Labels {
l = append(l, &dto.LabelPair{
Name: proto.String(label.Name),
Value: proto.String(label.Value),
})
}
m := &dto.Metric{
Label: l,
}
if c.config.TimestampExport == ExportTimestamp {
m.TimestampMs = proto.Int64(metric.Time.UnixNano() / int64(time.Millisecond))
}
switch entry.Family.Type {
case telegraf.Gauge:
m.Gauge = &dto.Gauge{Value: proto.Float64(metric.Scaler.Value)}
case telegraf.Counter:
m.Counter = &dto.Counter{Value: proto.Float64(metric.Scaler.Value)}
case telegraf.Untyped:
m.Untyped = &dto.Untyped{Value: proto.Float64(metric.Scaler.Value)}
case telegraf.Histogram:
buckets := make([]*dto.Bucket, 0, len(metric.Histogram.Buckets))
for _, bucket := range metric.Histogram.Buckets {
buckets = append(buckets, &dto.Bucket{
UpperBound: proto.Float64(bucket.Bound),
CumulativeCount: proto.Uint64(bucket.Count),
})
}
if len(buckets) == 0 {
continue
}
m.Histogram = &dto.Histogram{
Bucket: buckets,
SampleCount: proto.Uint64(metric.Histogram.Count),
SampleSum: proto.Float64(metric.Histogram.Sum),
}
case telegraf.Summary:
quantiles := make([]*dto.Quantile, 0, len(metric.Summary.Quantiles))
for _, quantile := range metric.Summary.Quantiles {
quantiles = append(quantiles, &dto.Quantile{
Quantile: proto.Float64(quantile.Quantile),
Value: proto.Float64(quantile.Value),
})
}
if len(quantiles) == 0 {
continue
}
m.Summary = &dto.Summary{
Quantile: quantiles,
SampleCount: proto.Uint64(metric.Summary.Count),
SampleSum: proto.Float64(metric.Summary.Sum),
}
default:
panic("unknown telegraf.ValueType")
}
mf.Metric = append(mf.Metric, m)
}
if len(mf.Metric) != 0 {
result = append(result, mf)
}
}
return result
}

View File

@ -0,0 +1,116 @@
package prometheus
import (
"testing"
"time"
"github.com/gogo/protobuf/proto"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/testutil"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
)
func TestCollectionExpire(t *testing.T) {
tests := []struct {
name string
now time.Time
age time.Duration
metrics []telegraf.Metric
expected []*dto.MetricFamily
}{
{
name: "not expired",
now: time.Unix(1, 0),
age: 10 * time.Second,
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []*dto.MetricFamily{
{
Name: proto.String("cpu_time_idle"),
Help: proto.String(helpString),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{},
Untyped: &dto.Untyped{Value: proto.Float64(42.0)},
},
},
},
},
},
{
name: "expired single metric in metric family",
now: time.Unix(20, 0),
age: 10 * time.Second,
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []*dto.MetricFamily{},
},
{
name: "expired one metric in metric family",
now: time.Unix(20, 0),
age: 10 * time.Second,
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_guest": 42.0,
},
time.Unix(15, 0),
),
},
expected: []*dto.MetricFamily{
{
Name: proto.String("cpu_time_guest"),
Help: proto.String(helpString),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{},
Untyped: &dto.Untyped{Value: proto.Float64(42.0)},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewCollection(FormatConfig{})
for _, metric := range tt.metrics {
c.Add(metric)
}
c.Expire(tt.now, tt.age)
actual := c.GetProto()
require.Equal(t, tt.expected, actual)
})
}
}

View File

@ -0,0 +1,175 @@
package prometheus
import (
"strings"
"unicode"
"github.com/influxdata/telegraf"
dto "github.com/prometheus/client_model/go"
)
var FirstTable = &unicode.RangeTable{
R16: []unicode.Range16{
{0x0041, 0x005A, 1}, // A-Z
{0x005F, 0x005F, 1}, // _
{0x0061, 0x007A, 1}, // a-z
},
LatinOffset: 3,
}
var RestTable = &unicode.RangeTable{
R16: []unicode.Range16{
{0x0030, 0x0039, 1}, // 0-9
{0x0041, 0x005A, 1}, // A-Z
{0x005F, 0x005F, 1}, // _
{0x0061, 0x007A, 1}, // a-z
},
LatinOffset: 4,
}
func isValid(name string) bool {
if name == "" {
return false
}
for i, r := range name {
switch {
case i == 0:
if !unicode.In(r, FirstTable) {
return false
}
default:
if !unicode.In(r, RestTable) {
return false
}
}
}
return true
}
// SanitizeName check if the name is a valid Prometheus metric name and label
// name. If not, it attempts to replaces invalid runes with an underscore to
// create a valid name. Returns the metric name and true if the name is valid
// to use.
func SanitizeName(name string) (string, bool) {
if isValid(name) {
return name, true
}
var b strings.Builder
for i, r := range name {
switch {
case i == 0:
if unicode.In(r, FirstTable) {
b.WriteRune(r)
}
default:
if unicode.In(r, RestTable) {
b.WriteRune(r)
} else {
b.WriteString("_")
}
}
}
name = strings.Trim(b.String(), "_")
if name == "" {
return "", false
}
return name, true
}
// MetricName returns the Prometheus metric name.
func MetricName(measurement, fieldKey string, valueType telegraf.ValueType) string {
switch valueType {
case telegraf.Histogram, telegraf.Summary:
switch {
case strings.HasSuffix(fieldKey, "_bucket"):
fieldKey = strings.TrimSuffix(fieldKey, "_bucket")
case strings.HasSuffix(fieldKey, "_sum"):
fieldKey = strings.TrimSuffix(fieldKey, "_sum")
case strings.HasSuffix(fieldKey, "_count"):
fieldKey = strings.TrimSuffix(fieldKey, "_count")
}
}
if measurement == "prometheus" {
return fieldKey
}
return measurement + "_" + fieldKey
}
func MetricType(valueType telegraf.ValueType) *dto.MetricType {
switch valueType {
case telegraf.Counter:
return dto.MetricType_COUNTER.Enum()
case telegraf.Gauge:
return dto.MetricType_GAUGE.Enum()
case telegraf.Summary:
return dto.MetricType_SUMMARY.Enum()
case telegraf.Untyped:
return dto.MetricType_UNTYPED.Enum()
case telegraf.Histogram:
return dto.MetricType_HISTOGRAM.Enum()
default:
panic("unknown telegraf.ValueType")
}
}
// SampleValue converts a field value into a value suitable for a simple sample value.
func SampleValue(value interface{}) (float64, bool) {
switch v := value.(type) {
case float64:
return v, true
case int64:
return float64(v), true
case uint64:
return float64(v), true
case bool:
if v {
return 1.0, true
}
return 0.0, true
default:
return 0, false
}
}
// SampleCount converts a field value into a count suitable for a metric family
// of the Histogram or Summary type.
func SampleCount(value interface{}) (uint64, bool) {
switch v := value.(type) {
case float64:
if v < 0 {
return 0, false
}
return uint64(v), true
case int64:
if v < 0 {
return 0, false
}
return uint64(v), true
case uint64:
return v, true
default:
return 0, false
}
}
// SampleSum converts a field value into a sum suitable for a metric family
// of the Histogram or Summary type.
func SampleSum(value interface{}) (float64, bool) {
switch v := value.(type) {
case float64:
return v, true
case int64:
return float64(v), true
case uint64:
return float64(v), true
default:
return 0, false
}
}

View File

@ -0,0 +1,69 @@
package prometheus
import (
"bytes"
"github.com/influxdata/telegraf"
"github.com/prometheus/common/expfmt"
)
// TimestampExport controls if the output contains timestamps.
type TimestampExport int
const (
NoExportTimestamp TimestampExport = iota
ExportTimestamp
)
// MetricSortOrder controls if the output is sorted.
type MetricSortOrder int
const (
NoSortMetrics MetricSortOrder = iota
SortMetrics
)
// StringHandling defines how to process string fields.
type StringHandling int
const (
DiscardStrings StringHandling = iota
StringAsLabel
)
type FormatConfig struct {
TimestampExport TimestampExport
MetricSortOrder MetricSortOrder
StringHandling StringHandling
}
type Serializer struct {
config FormatConfig
}
func NewSerializer(config FormatConfig) (*Serializer, error) {
s := &Serializer{config: config}
return s, nil
}
func (s *Serializer) Serialize(metric telegraf.Metric) ([]byte, error) {
return s.SerializeBatch([]telegraf.Metric{metric})
}
func (s *Serializer) SerializeBatch(metrics []telegraf.Metric) ([]byte, error) {
coll := NewCollection(s.config)
for _, metric := range metrics {
coll.Add(metric)
}
var buf bytes.Buffer
for _, mf := range coll.GetProto() {
enc := expfmt.NewEncoder(&buf, expfmt.FmtText)
err := enc.Encode(mf)
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,589 @@
package prometheus
import (
"strings"
"testing"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/require"
)
func TestSerialize(t *testing.T) {
tests := []struct {
name string
config FormatConfig
metric telegraf.Metric
expected []byte
}{
{
name: "simple",
metric: testutil.MustMetric(
"cpu",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "prometheus input untyped",
metric: testutil.MustMetric(
"prometheus",
map[string]string{
"code": "400",
"method": "post",
},
map[string]interface{}{
"http_requests_total": 3.0,
},
time.Unix(0, 0),
telegraf.Untyped,
),
expected: []byte(`
# HELP http_requests_total Telegraf collected metric
# TYPE http_requests_total untyped
http_requests_total{code="400",method="post"} 3
`),
},
{
name: "prometheus input counter",
metric: testutil.MustMetric(
"prometheus",
map[string]string{
"code": "400",
"method": "post",
},
map[string]interface{}{
"http_requests_total": 3.0,
},
time.Unix(0, 0),
telegraf.Counter,
),
expected: []byte(`
# HELP http_requests_total Telegraf collected metric
# TYPE http_requests_total counter
http_requests_total{code="400",method="post"} 3
`),
},
{
name: "prometheus input gauge",
metric: testutil.MustMetric(
"prometheus",
map[string]string{
"code": "400",
"method": "post",
},
map[string]interface{}{
"http_requests_total": 3.0,
},
time.Unix(0, 0),
telegraf.Gauge,
),
expected: []byte(`
# HELP http_requests_total Telegraf collected metric
# TYPE http_requests_total gauge
http_requests_total{code="400",method="post"} 3
`),
},
{
name: "prometheus input histogram no buckets",
metric: testutil.MustMetric(
"prometheus",
map[string]string{},
map[string]interface{}{
"http_request_duration_seconds_sum": 53423,
"http_request_duration_seconds_count": 144320,
},
time.Unix(0, 0),
telegraf.Histogram,
),
expected: []byte(`
`),
},
{
name: "prometheus input histogram only bucket",
metric: testutil.MustMetric(
"prometheus",
map[string]string{
"le": "0.5",
},
map[string]interface{}{
"http_request_duration_seconds_bucket": 129389.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
expected: []byte(`
# HELP http_request_duration_seconds Telegraf collected metric
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.5"} 129389
http_request_duration_seconds_bucket{le="+Inf"} 0
http_request_duration_seconds_sum 0
http_request_duration_seconds_count 0
`),
},
{
name: "simple with timestamp",
config: FormatConfig{
TimestampExport: ExportTimestamp,
},
metric: testutil.MustMetric(
"cpu",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(1574279268, 0),
),
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42 1574279268000
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := NewSerializer(FormatConfig{
MetricSortOrder: SortMetrics,
TimestampExport: tt.config.TimestampExport,
StringHandling: tt.config.StringHandling,
})
require.NoError(t, err)
actual, err := s.Serialize(tt.metric)
require.NoError(t, err)
require.Equal(t, strings.TrimSpace(string(tt.expected)),
strings.TrimSpace(string(actual)))
})
}
}
func TestSerializeBatch(t *testing.T) {
tests := []struct {
name string
config FormatConfig
metrics []telegraf.Metric
expected []byte
}{
{
name: "simple",
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{
"host": "one.example.org",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
testutil.MustMetric(
"cpu",
map[string]string{
"host": "two.example.org",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="one.example.org"} 42
cpu_time_idle{host="two.example.org"} 42
`),
},
{
name: "multiple metric families",
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{
"host": "one.example.org",
},
map[string]interface{}{
"time_idle": 42.0,
"time_guest": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_guest Telegraf collected metric
# TYPE cpu_time_guest untyped
cpu_time_guest{host="one.example.org"} 42
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="one.example.org"} 42
`),
},
{
name: "histogram",
metrics: []telegraf.Metric{
testutil.MustMetric(
"prometheus",
map[string]string{},
map[string]interface{}{
"http_request_duration_seconds_sum": 53423,
"http_request_duration_seconds_count": 144320,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"prometheus",
map[string]string{"le": "0.05"},
map[string]interface{}{
"http_request_duration_seconds_bucket": 24054.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"prometheus",
map[string]string{"le": "0.1"},
map[string]interface{}{
"http_request_duration_seconds_bucket": 33444.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"prometheus",
map[string]string{"le": "0.2"},
map[string]interface{}{
"http_request_duration_seconds_bucket": 100392.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"prometheus",
map[string]string{"le": "0.5"},
map[string]interface{}{
"http_request_duration_seconds_bucket": 129389.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"prometheus",
map[string]string{"le": "1.0"},
map[string]interface{}{
"http_request_duration_seconds_bucket": 133988.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"prometheus",
map[string]string{"le": "+Inf"},
map[string]interface{}{
"http_request_duration_seconds_bucket": 144320.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
},
expected: []byte(`
# HELP http_request_duration_seconds Telegraf collected metric
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 24054
http_request_duration_seconds_bucket{le="0.1"} 33444
http_request_duration_seconds_bucket{le="0.2"} 100392
http_request_duration_seconds_bucket{le="0.5"} 129389
http_request_duration_seconds_bucket{le="1"} 133988
http_request_duration_seconds_bucket{le="+Inf"} 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_count 144320
`),
},
{
name: "",
metrics: []telegraf.Metric{
testutil.MustMetric(
"prometheus",
map[string]string{},
map[string]interface{}{
"rpc_duration_seconds_sum": 1.7560473e+07,
"rpc_duration_seconds_count": 2693,
},
time.Unix(0, 0),
telegraf.Summary,
),
testutil.MustMetric(
"prometheus",
map[string]string{"quantile": "0.01"},
map[string]interface{}{
"rpc_duration_seconds": 3102.0,
},
time.Unix(0, 0),
telegraf.Summary,
),
testutil.MustMetric(
"prometheus",
map[string]string{"quantile": "0.05"},
map[string]interface{}{
"rpc_duration_seconds": 3272.0,
},
time.Unix(0, 0),
telegraf.Summary,
),
testutil.MustMetric(
"prometheus",
map[string]string{"quantile": "0.5"},
map[string]interface{}{
"rpc_duration_seconds": 4773.0,
},
time.Unix(0, 0),
telegraf.Summary,
),
testutil.MustMetric(
"prometheus",
map[string]string{"quantile": "0.9"},
map[string]interface{}{
"rpc_duration_seconds": 9001.0,
},
time.Unix(0, 0),
telegraf.Summary,
),
testutil.MustMetric(
"prometheus",
map[string]string{"quantile": "0.99"},
map[string]interface{}{
"rpc_duration_seconds": 76656.0,
},
time.Unix(0, 0),
telegraf.Summary,
),
},
expected: []byte(`
# HELP rpc_duration_seconds Telegraf collected metric
# TYPE rpc_duration_seconds summary
rpc_duration_seconds{quantile="0.01"} 3102
rpc_duration_seconds{quantile="0.05"} 3272
rpc_duration_seconds{quantile="0.5"} 4773
rpc_duration_seconds{quantile="0.9"} 9001
rpc_duration_seconds{quantile="0.99"} 76656
rpc_duration_seconds_sum 1.7560473e+07
rpc_duration_seconds_count 2693
`),
},
{
name: "newer sample",
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 43.0,
},
time.Unix(1, 0),
),
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle 43
`),
},
{
name: "invalid label",
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{
"host-name": "example.org",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host_name="example.org"} 42
`),
},
{
name: "discard strings",
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
"cpu": "cpu0",
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle 42
`),
},
{
name: "string as label",
config: FormatConfig{
StringHandling: StringAsLabel,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
"cpu": "cpu0",
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{cpu="cpu0"} 42
`),
},
{
name: "string as label duplicate tag",
config: FormatConfig{
StringHandling: StringAsLabel,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu0",
},
map[string]interface{}{
"time_idle": 42.0,
"cpu": "cpu1",
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{cpu="cpu0"} 42
`),
},
{
name: "multiple fields grouping",
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu0",
},
map[string]interface{}{
"time_guest": 8106.04,
"time_system": 26271.4,
"time_user": 92904.33,
},
time.Unix(0, 0),
),
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu1",
},
map[string]interface{}{
"time_guest": 8181.63,
"time_system": 25351.49,
"time_user": 96912.57,
},
time.Unix(0, 0),
),
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu2",
},
map[string]interface{}{
"time_guest": 7470.04,
"time_system": 24998.43,
"time_user": 96034.08,
},
time.Unix(0, 0),
),
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu3",
},
map[string]interface{}{
"time_guest": 7517.95,
"time_system": 24970.82,
"time_user": 94148,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_guest Telegraf collected metric
# TYPE cpu_time_guest untyped
cpu_time_guest{cpu="cpu0"} 8106.04
cpu_time_guest{cpu="cpu1"} 8181.63
cpu_time_guest{cpu="cpu2"} 7470.04
cpu_time_guest{cpu="cpu3"} 7517.95
# HELP cpu_time_system Telegraf collected metric
# TYPE cpu_time_system untyped
cpu_time_system{cpu="cpu0"} 26271.4
cpu_time_system{cpu="cpu1"} 25351.49
cpu_time_system{cpu="cpu2"} 24998.43
cpu_time_system{cpu="cpu3"} 24970.82
# HELP cpu_time_user Telegraf collected metric
# TYPE cpu_time_user untyped
cpu_time_user{cpu="cpu0"} 92904.33
cpu_time_user{cpu="cpu1"} 96912.57
cpu_time_user{cpu="cpu2"} 96034.08
cpu_time_user{cpu="cpu3"} 94148
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := NewSerializer(FormatConfig{
MetricSortOrder: SortMetrics,
TimestampExport: tt.config.TimestampExport,
StringHandling: tt.config.StringHandling,
})
require.NoError(t, err)
actual, err := s.SerializeBatch(tt.metrics)
require.NoError(t, err)
require.Equal(t,
strings.TrimSpace(string(tt.expected)),
strings.TrimSpace(string(actual)))
})
}
}

View File

@ -10,6 +10,7 @@ import (
"github.com/influxdata/telegraf/plugins/serializers/influx" "github.com/influxdata/telegraf/plugins/serializers/influx"
"github.com/influxdata/telegraf/plugins/serializers/json" "github.com/influxdata/telegraf/plugins/serializers/json"
"github.com/influxdata/telegraf/plugins/serializers/nowmetric" "github.com/influxdata/telegraf/plugins/serializers/nowmetric"
"github.com/influxdata/telegraf/plugins/serializers/prometheus"
"github.com/influxdata/telegraf/plugins/serializers/splunkmetric" "github.com/influxdata/telegraf/plugins/serializers/splunkmetric"
"github.com/influxdata/telegraf/plugins/serializers/wavefront" "github.com/influxdata/telegraf/plugins/serializers/wavefront"
) )
@ -45,43 +46,54 @@ type Serializer interface {
// and can be used to instantiate _any_ of the serializers. // and can be used to instantiate _any_ of the serializers.
type Config struct { type Config struct {
// Dataformat can be one of the serializer types listed in NewSerializer. // Dataformat can be one of the serializer types listed in NewSerializer.
DataFormat string DataFormat string `toml:"data_format"`
// Support tags in graphite protocol // Support tags in graphite protocol
GraphiteTagSupport bool GraphiteTagSupport bool `toml:"graphite_tag_support"`
// Maximum line length in bytes; influx format only // Maximum line length in bytes; influx format only
InfluxMaxLineBytes int InfluxMaxLineBytes int `toml:"influx_max_line_bytes"`
// Sort field keys, set to true only when debugging as it less performant // Sort field keys, set to true only when debugging as it less performant
// than unsorted fields; influx format only // than unsorted fields; influx format only
InfluxSortFields bool InfluxSortFields bool `toml:"influx_sort_fields"`
// Support unsigned integer output; influx format only // Support unsigned integer output; influx format only
InfluxUintSupport bool InfluxUintSupport bool `toml:"influx_uint_support"`
// Prefix to add to all measurements, only supports Graphite // Prefix to add to all measurements, only supports Graphite
Prefix string Prefix string `toml:"prefix"`
// Template for converting telegraf metrics into Graphite // Template for converting telegraf metrics into Graphite
// only supports Graphite // only supports Graphite
Template string Template string `toml:"template"`
// Timestamp units to use for JSON formatted output // Timestamp units to use for JSON formatted output
TimestampUnits time.Duration TimestampUnits time.Duration `toml:"timestamp_units"`
// Include HEC routing fields for splunkmetric output // Include HEC routing fields for splunkmetric output
HecRouting bool HecRouting bool `toml:"hec_routing"`
// Enable Splunk MultiMetric output (Splunk 8.0+) // Enable Splunk MultiMetric output (Splunk 8.0+)
SplunkmetricMultiMetric bool SplunkmetricMultiMetric bool `toml:"splunkmetric_multi_metric"`
// Point tags to use as the source name for Wavefront (if none found, host will be used). // Point tags to use as the source name for Wavefront (if none found, host will be used).
WavefrontSourceOverride []string WavefrontSourceOverride []string `toml:"wavefront_source_override"`
// Use Strict rules to sanitize metric and tag names from invalid characters for Wavefront // Use Strict rules to sanitize metric and tag names from invalid characters for Wavefront
// When enabled forward slash (/) and comma (,) will be accepted // When enabled forward slash (/) and comma (,) will be accepted
WavefrontUseStrict bool WavefrontUseStrict bool `toml:"wavefront_use_strict"`
// Include the metric timestamp on each sample.
PrometheusExportTimestamp bool `toml:"prometheus_export_timestamp"`
// Sort prometheus metric families and metric samples. Useful for
// debugging.
PrometheusSortMetrics bool `toml:"prometheus_sort_metrics"`
// Output string fields as metric labels; when false string fields are
// discarded.
PrometheusStringAsLabel bool `toml:"prometheus_string_as_label"`
} }
// NewSerializer a Serializer interface based on the given config. // NewSerializer a Serializer interface based on the given config.
@ -103,12 +115,37 @@ func NewSerializer(config *Config) (Serializer, error) {
serializer, err = NewCarbon2Serializer() serializer, err = NewCarbon2Serializer()
case "wavefront": case "wavefront":
serializer, err = NewWavefrontSerializer(config.Prefix, config.WavefrontUseStrict, config.WavefrontSourceOverride) serializer, err = NewWavefrontSerializer(config.Prefix, config.WavefrontUseStrict, config.WavefrontSourceOverride)
case "prometheus":
serializer, err = NewPrometheusSerializer(config)
default: default:
err = fmt.Errorf("Invalid data format: %s", config.DataFormat) err = fmt.Errorf("Invalid data format: %s", config.DataFormat)
} }
return serializer, err return serializer, err
} }
func NewPrometheusSerializer(config *Config) (Serializer, error) {
exportTimestamp := prometheus.NoExportTimestamp
if config.PrometheusExportTimestamp {
exportTimestamp = prometheus.ExportTimestamp
}
sortMetrics := prometheus.NoSortMetrics
if config.PrometheusExportTimestamp {
sortMetrics = prometheus.SortMetrics
}
stringAsLabels := prometheus.DiscardStrings
if config.PrometheusStringAsLabel {
stringAsLabels = prometheus.StringAsLabel
}
return prometheus.NewSerializer(prometheus.FormatConfig{
TimestampExport: exportTimestamp,
MetricSortOrder: sortMetrics,
StringHandling: stringAsLabels,
})
}
func NewWavefrontSerializer(prefix string, useStrict bool, sourceOverride []string) (Serializer, error) { func NewWavefrontSerializer(prefix string, useStrict bool, sourceOverride []string) (Serializer, error) {
return wavefront.NewSerializer(prefix, useStrict, sourceOverride) return wavefront.NewSerializer(prefix, useStrict, sourceOverride)
} }