1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,99 @@
# Prometheus Output Plugin
This plugin starts a [Prometheus][prometheus] client and exposes the written
metrics on a `/metrics` endpoint by default. This endpoint can then be polled
by a Prometheus server.
⭐ Telegraf v0.2.1
🏷️ applications
💻 all
[prometheus]: https://prometheus.io
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Secret-store support
This plugin supports secrets from secret-stores for the `basic_password` option.
See the [secret-store documentation][SECRETSTORE] for more details on how
to use them.
[SECRETSTORE]: ../../../docs/CONFIGURATION.md#secret-store-secrets
## Configuration
```toml @sample.conf
# Configuration for the Prometheus client to spawn
[[outputs.prometheus_client]]
## Address to listen on.
## ex:
## listen = ":9273"
## listen = "vsock://:9273"
listen = ":9273"
## Maximum duration before timing out read of the request
# read_timeout = "10s"
## Maximum duration before timing out write of the response
# write_timeout = "10s"
## Metric version controls the mapping from Prometheus metrics into Telegraf metrics.
## See "Metric Format Configuration" in plugins/inputs/prometheus/README.md for details.
## Valid options: 1, 2
# metric_version = 1
## Use HTTP Basic Authentication.
# basic_username = "Foo"
# basic_password = "Bar"
## If set, the IP Ranges which are allowed to access metrics.
## ex: ip_range = ["192.168.0.0/24", "192.168.1.0/30"]
# ip_range = []
## Path to publish the metrics on.
# path = "/metrics"
## Expiration interval for each metric. 0 == no expiration
# expiration_interval = "60s"
## Collectors to enable, valid entries are "gocollector" and "process".
## If unset, both are enabled.
# collectors_exclude = ["gocollector", "process"]
## Send string metrics as Prometheus labels.
## Unless set to false all string metrics will be sent as labels.
# string_as_label = true
## If set, enable TLS with the given certificate.
# tls_cert = "/etc/ssl/telegraf.crt"
# tls_key = "/etc/ssl/telegraf.key"
## Set one or more allowed client CA certificate file names to
## enable mutually authenticated TLS connections
# tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"]
## Export metric collection time.
# export_timestamp = false
## Set custom headers for HTTP responses.
# http_headers = {"X-Special-Header" = "Special-Value"}
## Specify the metric type explicitly.
## This overrides the metric-type of the Telegraf metric. Globbing is allowed.
# [outputs.prometheus_client.metric_types]
# counter = []
# gauge = []
```
## Metrics
Prometheus metrics are produced in the same manner as the [prometheus
serializer][].
[prometheus serializer]: /plugins/serializers/prometheus/README.md#Metrics

View file

@ -0,0 +1,312 @@
//go:generate ../../../tools/readme_config_includer/generator
package prometheus_client
import (
"context"
"crypto/tls"
_ "embed"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/mdlayher/vsock"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
common_tls "github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/outputs"
"github.com/influxdata/telegraf/plugins/outputs/prometheus_client/v1"
"github.com/influxdata/telegraf/plugins/outputs/prometheus_client/v2"
serializers_prometheus "github.com/influxdata/telegraf/plugins/serializers/prometheus"
)
//go:embed sample.conf
var sampleConfig string
const (
defaultListen = ":9273"
defaultPath = "/metrics"
defaultExpirationInterval = config.Duration(60 * time.Second)
defaultReadTimeout = 10 * time.Second
defaultWriteTimeout = 10 * time.Second
)
type Collector interface {
Describe(ch chan<- *prometheus.Desc)
Collect(ch chan<- prometheus.Metric)
Add(metrics []telegraf.Metric) error
}
type PrometheusClient struct {
Listen string `toml:"listen"`
ReadTimeout config.Duration `toml:"read_timeout"`
WriteTimeout config.Duration `toml:"write_timeout"`
MetricVersion int `toml:"metric_version"`
BasicUsername string `toml:"basic_username"`
BasicPassword config.Secret `toml:"basic_password"`
IPRange []string `toml:"ip_range"`
ExpirationInterval config.Duration `toml:"expiration_interval"`
Path string `toml:"path"`
CollectorsExclude []string `toml:"collectors_exclude"`
StringAsLabel bool `toml:"string_as_label"`
ExportTimestamp bool `toml:"export_timestamp"`
TypeMappings serializers_prometheus.MetricTypes `toml:"metric_types"`
HTTPHeaders map[string]*config.Secret `toml:"http_headers"`
Log telegraf.Logger `toml:"-"`
common_tls.ServerConfig
server *http.Server
url *url.URL
collector Collector
wg sync.WaitGroup
}
func (*PrometheusClient) SampleConfig() string {
return sampleConfig
}
func (p *PrometheusClient) Init() error {
defaultCollectors := map[string]bool{
"gocollector": true,
"process": true,
}
for _, collector := range p.CollectorsExclude {
delete(defaultCollectors, collector)
}
registry := prometheus.NewRegistry()
for collector := range defaultCollectors {
switch collector {
case "gocollector":
err := registry.Register(collectors.NewGoCollector())
if err != nil {
return err
}
case "process":
err := registry.Register(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
if err != nil {
return err
}
default:
return fmt.Errorf("unrecognized collector %s", collector)
}
}
if err := p.TypeMappings.Init(); err != nil {
return err
}
switch p.MetricVersion {
default:
fallthrough
case 1:
p.collector = v1.NewCollector(
time.Duration(p.ExpirationInterval),
p.StringAsLabel,
p.ExportTimestamp,
p.TypeMappings,
p.Log,
)
err := registry.Register(p.collector)
if err != nil {
return err
}
case 2:
p.collector = v2.NewCollector(
time.Duration(p.ExpirationInterval),
p.StringAsLabel,
p.ExportTimestamp,
p.TypeMappings,
)
err := registry.Register(p.collector)
if err != nil {
return err
}
}
ipRange := make([]*net.IPNet, 0, len(p.IPRange))
for _, cidr := range p.IPRange {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return fmt.Errorf("error parsing ip_range: %w", err)
}
ipRange = append(ipRange, ipNet)
}
psecret, err := p.BasicPassword.Get()
if err != nil {
return err
}
password := psecret.String()
psecret.Destroy()
authHandler := internal.BasicAuthHandler(p.BasicUsername, password, "prometheus", onAuthError)
rangeHandler := internal.IPRangeHandler(ipRange, onError)
promHandler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError})
landingPageHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte("Telegraf Output Plugin: Prometheus Client "))
if err != nil {
p.Log.Errorf("Error occurred when writing HTTP reply: %v", err)
}
})
mux := http.NewServeMux()
if p.Path == "" {
p.Path = "/metrics"
}
mux.Handle(p.Path, p.headerHandler(authHandler(rangeHandler(promHandler))))
mux.Handle("/", p.headerHandler(authHandler(rangeHandler(landingPageHandler))))
tlsConfig, err := p.TLSConfig()
if err != nil {
return err
}
if p.ReadTimeout < config.Duration(time.Second) {
p.ReadTimeout = config.Duration(defaultReadTimeout)
}
if p.WriteTimeout < config.Duration(time.Second) {
p.WriteTimeout = config.Duration(defaultWriteTimeout)
}
p.server = &http.Server{
Addr: p.Listen,
Handler: mux,
TLSConfig: tlsConfig,
ReadTimeout: time.Duration(p.ReadTimeout),
WriteTimeout: time.Duration(p.WriteTimeout),
}
return nil
}
func (p *PrometheusClient) listenTCP(host string) (net.Listener, error) {
if p.server.TLSConfig != nil {
return tls.Listen("tcp", host, p.server.TLSConfig)
}
return net.Listen("tcp", host)
}
func listenVsock(host string) (net.Listener, error) {
_, portStr, err := net.SplitHostPort(host)
if err != nil {
return nil, err
}
port, err := strconv.ParseUint(portStr, 10, 32)
if err != nil {
return nil, err
}
return vsock.Listen(uint32(port), nil)
}
func (p *PrometheusClient) listen() (net.Listener, error) {
u, err := url.ParseRequestURI(p.Listen)
// fallback to legacy way
if err != nil {
return p.listenTCP(p.Listen)
}
switch strings.ToLower(u.Scheme) {
case "", "tcp", "http":
return p.listenTCP(u.Host)
case "vsock":
return listenVsock(u.Host)
default:
return p.listenTCP(u.Host)
}
}
func (p *PrometheusClient) Connect() error {
listener, err := p.listen()
if err != nil {
return err
}
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() {
defer p.wg.Done()
err := p.server.Serve(listener)
if err != nil && err != http.ErrServerClosed {
p.Log.Errorf("Server error: %v", err)
}
}()
return nil
}
func (p *PrometheusClient) headerHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for key, secret := range p.HTTPHeaders {
value, err := secret.Get()
if err == nil {
w.Header().Set(key, value.String())
}
}
next.ServeHTTP(w, r)
})
}
func onAuthError(_ http.ResponseWriter) {
}
func onError(rw http.ResponseWriter, code int) {
http.Error(rw, http.StatusText(code), code)
}
// URL returns the address the plugin is listening on. If not listening
// an empty string is returned.
func (p *PrometheusClient) URL() string {
if p.url != nil {
return p.url.String()
}
return ""
}
func (p *PrometheusClient) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := p.server.Shutdown(ctx)
p.wg.Wait()
p.url = nil
prometheus.Unregister(p.collector)
return err
}
func (p *PrometheusClient) Write(metrics []telegraf.Metric) error {
return p.collector.Add(metrics)
}
func init() {
outputs.Add("prometheus_client", func() telegraf.Output {
return &PrometheusClient{
Listen: defaultListen,
Path: defaultPath,
ExpirationInterval: defaultExpirationInterval,
StringAsLabel: true,
}
})
}

View file

@ -0,0 +1,123 @@
package prometheus_client
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/testutil"
)
func TestLandingPage(t *testing.T) {
output := PrometheusClient{
Listen: ":0",
CollectorsExclude: []string{"process"},
MetricVersion: 1,
Log: &testutil.Logger{Name: "outputs.prometheus_client"},
}
expected := "Telegraf Output Plugin: Prometheus Client"
require.NoError(t, output.Init())
require.NoError(t, output.Connect())
u, err := url.Parse(fmt.Sprintf("http://%s/", output.url.Host))
require.NoError(t, err)
resp, err := http.Get(u.String())
require.NoError(t, err)
defer resp.Body.Close()
actual, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, expected, strings.TrimSpace(string(actual)))
}
func TestFormatHeader(t *testing.T) {
tests := []struct {
name string
accept string
expected string
}{
{
name: "invalid accept ",
accept: "applications/json",
expected: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
},
{
name: "no accept header",
expected: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
},
{
name: "text no version",
accept: "text/plain",
expected: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
},
{
name: "text with version 0.0.4",
accept: "text/plain; version=0.0.4",
expected: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
},
{
name: "protobuf text format",
accept: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text",
expected: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores",
},
{
name: "protobuf compact text format",
accept: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text",
expected: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
},
{
name: "protobuf delimited format",
accept: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited",
expected: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores",
},
{
name: "multiple accept preferring protobuf",
accept: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited, text/plain",
expected: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores",
},
{
name: "multiple accept preferring text",
accept: "text/plain, application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited",
expected: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
},
}
// Setup the plugin
plugin := PrometheusClient{
Listen: ":0",
Log: testutil.Logger{Name: "outputs.prometheus_client"},
}
require.NoError(t, plugin.Init())
require.NoError(t, plugin.Connect())
// Get the plugin's address so we can send data to it
addr := fmt.Sprintf("http://%s/metrics", plugin.url.Host)
// Run the actual tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Construct a request with the given "Accept" header
req, err := http.NewRequest("GET", addr, nil)
require.NoError(t, err)
if tt.accept != "" {
req.Header.Add("Accept", tt.accept)
}
// Get the metrics
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Test the result
require.NotEmpty(t, resp.Body)
require.Equal(t, tt.expected, resp.Header.Get("Content-Type"))
})
}
}

View file

@ -0,0 +1,497 @@
package prometheus_client
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
inputs "github.com/influxdata/telegraf/plugins/inputs/prometheus"
"github.com/influxdata/telegraf/plugins/serializers/prometheus"
"github.com/influxdata/telegraf/testutil"
)
func TestMetricVersion1(t *testing.T) {
logger := testutil.Logger{Name: "outputs.prometheus_client"}
tests := []struct {
name string
output *PrometheusClient
metrics []telegraf.Metric
expected []byte
}{
{
name: "simple",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
metrics: []telegraf.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 untyped",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
metrics: []telegraf.Metric{
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: "prometheus counter",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
metrics: []telegraf.Metric{
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: "when export timestamp is true timestamp is present in the metric",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
ExportTimestamp: true,
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu_time_idle",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"counter": 42.0,
},
time.Unix(1257894000, 0),
telegraf.Counter,
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle counter
cpu_time_idle{host="example.org"} 42 1257894000000
`),
},
{
name: "replace characters when using string as label",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
StringAsLabel: true,
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu_time_idle",
map[string]string{},
map[string]interface{}{
"host:name": "example.org",
"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_name="example.org"} 42
`),
},
{
name: "prometheus gauge",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
metrics: []telegraf.Metric{
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: 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: 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
`),
},
{
name: "prometheus untyped forced to counter",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
TypeMappings: prometheus.MetricTypes{Counter: []string{"cpu_time_idle"}},
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu_time_idle",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"value": 42,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle counter
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "prometheus untyped forced to gauge",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 1,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
TypeMappings: prometheus.MetricTypes{Gauge: []string{"cpu_time_idle"}},
Log: logger,
},
metrics: []telegraf.Metric{
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 gauge
cpu_time_idle{host="example.org"} 42
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.NoError(t, tt.output.Init())
require.NoError(t, tt.output.Connect())
defer func() {
require.NoError(t, tt.output.Close())
}()
require.NoError(t, tt.output.Write(tt.metrics))
resp, err := http.Get(tt.output.URL())
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t,
strings.TrimSpace(string(tt.expected)),
strings.TrimSpace(string(body)))
})
}
}
func TestRoundTripMetricVersion1(t *testing.T) {
logger := testutil.Logger{Name: "outputs.prometheus_client"}
regxPattern := regexp.MustCompile(`.*prometheus_request_.*`)
tests := []struct {
name string
data []byte
}{
{
name: "untyped",
data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "counter",
data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle counter
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "gauge",
data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "multi",
data: []byte(`
# HELP cpu_time_guest Telegraf collected metric
# TYPE cpu_time_guest gauge
cpu_time_guest{host="one.example.org"} 42
cpu_time_guest{host="two.example.org"} 42
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="one.example.org"} 42
cpu_time_idle{host="two.example.org"} 42
`),
},
{
name: "histogram",
data: []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: "summary",
data: []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
`),
},
}
ts := httptest.NewServer(http.NotFoundHandler())
defer ts.Close()
address := fmt.Sprintf("http://%s", ts.Listener.Addr())
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
if _, err := w.Write(tt.data); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
})
input := &inputs.Prometheus{
Log: logger,
URLs: []string{address},
URLTag: "",
MetricVersion: 1,
}
err := input.Init()
require.NoError(t, err)
var acc testutil.Accumulator
err = input.Start(&acc)
require.NoError(t, err)
err = input.Gather(&acc)
require.NoError(t, err)
input.Stop()
metrics := acc.GetTelegrafMetrics()
output := &PrometheusClient{
Listen: "127.0.0.1:0",
Path: defaultPath,
MetricVersion: 1,
Log: logger,
CollectorsExclude: []string{"gocollector", "process"},
}
err = output.Init()
require.NoError(t, err)
err = output.Connect()
require.NoError(t, err)
defer func() {
err = output.Close()
require.NoError(t, err)
}()
err = output.Write(metrics)
require.NoError(t, err)
resp, err := http.Get(output.URL())
require.NoError(t, err)
defer resp.Body.Close()
actual, err := io.ReadAll(resp.Body)
require.NoError(t, err)
current := regxPattern.ReplaceAllLiteralString(string(actual), "")
require.Equal(t,
strings.TrimSpace(string(tt.data)),
strings.TrimSpace(current))
})
}
}

View file

@ -0,0 +1,528 @@
package prometheus_client
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
inputs "github.com/influxdata/telegraf/plugins/inputs/prometheus"
"github.com/influxdata/telegraf/plugins/serializers/prometheus"
"github.com/influxdata/telegraf/testutil"
)
func TestMetricVersion2(t *testing.T) {
logger := testutil.Logger{Name: "outputs.prometheus_client"}
tests := []struct {
name string
output *PrometheusClient
metrics []telegraf.Metric
expected []byte
}{
{
name: "untyped telegraf metric",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
metrics: []telegraf.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: "summary no quantiles",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
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,
),
},
expected: []byte(`
# HELP rpc_duration_seconds Telegraf collected metric
# TYPE rpc_duration_seconds summary
rpc_duration_seconds_sum 1.7560473e+07
rpc_duration_seconds_count 2693
`),
},
{
name: "when export timestamp is true timestamp is present in the metric",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
ExportTimestamp: true,
Log: logger,
},
metrics: []telegraf.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 0
`),
},
{
name: "strings as labels",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
StringAsLabel: true,
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
"host": "example.org",
},
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: "when strings as labels is false string fields are discarded",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
StringAsLabel: false,
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
"host": "example.org",
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle 42
`),
},
{
name: "untype prometheus metric",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"prometheus",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"cpu_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: "telegraf histogram",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu1",
},
map[string]interface{}{
"usage_idle_sum": 2000.0,
"usage_idle_count": 20.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu1",
"le": "0.0",
},
map[string]interface{}{
"usage_idle_bucket": 0.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu1",
"le": "50.0",
},
map[string]interface{}{
"usage_idle_bucket": 7.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu1",
"le": "100.0",
},
map[string]interface{}{
"usage_idle_bucket": 20.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu1",
"le": "+Inf",
},
map[string]interface{}{
"usage_idle_bucket": 20.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
},
expected: []byte(`
# HELP cpu_usage_idle Telegraf collected metric
# TYPE cpu_usage_idle histogram
cpu_usage_idle_bucket{cpu="cpu1",le="0"} 0
cpu_usage_idle_bucket{cpu="cpu1",le="50"} 7
cpu_usage_idle_bucket{cpu="cpu1",le="100"} 20
cpu_usage_idle_bucket{cpu="cpu1",le="+Inf"} 20
cpu_usage_idle_sum{cpu="cpu1"} 2000
cpu_usage_idle_count{cpu="cpu1"} 20
`),
},
{
name: "histogram no buckets",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{
"cpu": "cpu1",
},
map[string]interface{}{
"usage_idle_sum": 2000.0,
"usage_idle_count": 20.0,
},
time.Unix(0, 0),
telegraf.Histogram,
),
},
expected: []byte(`
# HELP cpu_usage_idle Telegraf collected metric
# TYPE cpu_usage_idle histogram
cpu_usage_idle_bucket{cpu="cpu1",le="+Inf"} 20
cpu_usage_idle_sum{cpu="cpu1"} 2000
cpu_usage_idle_count{cpu="cpu1"} 20
`),
},
{
name: "untyped forced to counter",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
TypeMappings: prometheus.MetricTypes{Counter: []string{"cpu_time_idle"}},
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"prometheus",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"cpu_time_idle": 42,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle counter
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "untyped forced to gauge",
output: &PrometheusClient{
Listen: ":0",
MetricVersion: 2,
CollectorsExclude: []string{"gocollector", "process"},
Path: "/metrics",
TypeMappings: prometheus.MetricTypes{Gauge: []string{"cpu_time_idle"}},
Log: logger,
},
metrics: []telegraf.Metric{
testutil.MustMetric(
"prometheus",
map[string]string{
"host": "example.org",
},
map[string]interface{}{
"cpu_time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="example.org"} 42
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.NoError(t, tt.output.Init())
require.NoError(t, tt.output.Connect())
defer func() {
require.NoError(t, tt.output.Close())
}()
require.NoError(t, tt.output.Write(tt.metrics))
resp, err := http.Get(tt.output.URL())
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t,
strings.TrimSpace(string(tt.expected)),
strings.TrimSpace(string(body)))
})
}
}
func TestRoundTripMetricVersion2(t *testing.T) {
logger := testutil.Logger{Name: "outputs.prometheus_client"}
regxPattern := regexp.MustCompile(`.*prometheus_request_.*`)
tests := []struct {
name string
data []byte
}{
{
name: "untyped",
data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "counter",
data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle counter
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "gauge",
data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="example.org"} 42
`),
},
{
name: "multi",
data: []byte(`
# HELP cpu_time_guest Telegraf collected metric
# TYPE cpu_time_guest gauge
cpu_time_guest{host="one.example.org"} 42
cpu_time_guest{host="two.example.org"} 42
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="one.example.org"} 42
cpu_time_idle{host="two.example.org"} 42
`),
},
{
name: "histogram",
data: []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: "summary",
data: []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
`),
},
}
ts := httptest.NewServer(http.NotFoundHandler())
defer ts.Close()
url := fmt.Sprintf("http://%s", ts.Listener.Addr())
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
if _, err := w.Write(tt.data); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
})
input := &inputs.Prometheus{
Log: logger,
URLs: []string{url},
URLTag: "",
MetricVersion: 2,
}
err := input.Init()
require.NoError(t, err)
var acc testutil.Accumulator
err = input.Start(&acc)
require.NoError(t, err)
err = input.Gather(&acc)
require.NoError(t, err)
input.Stop()
metrics := acc.GetTelegrafMetrics()
output := &PrometheusClient{
Listen: "127.0.0.1:0",
Path: defaultPath,
MetricVersion: 2,
Log: logger,
CollectorsExclude: []string{"gocollector", "process"},
}
err = output.Init()
require.NoError(t, err)
err = output.Connect()
require.NoError(t, err)
defer func() {
err = output.Close()
require.NoError(t, err)
}()
err = output.Write(metrics)
require.NoError(t, err)
resp, err := http.Get(output.URL())
require.NoError(t, err)
defer resp.Body.Close()
actual, err := io.ReadAll(resp.Body)
require.NoError(t, err)
current := regxPattern.ReplaceAllLiteralString(string(actual), "")
require.Equal(t,
strings.TrimSpace(string(tt.data)),
strings.TrimSpace(current))
})
}
}

View file

@ -0,0 +1,59 @@
# Configuration for the Prometheus client to spawn
[[outputs.prometheus_client]]
## Address to listen on.
## ex:
## listen = ":9273"
## listen = "vsock://:9273"
listen = ":9273"
## Maximum duration before timing out read of the request
# read_timeout = "10s"
## Maximum duration before timing out write of the response
# write_timeout = "10s"
## Metric version controls the mapping from Prometheus metrics into Telegraf metrics.
## See "Metric Format Configuration" in plugins/inputs/prometheus/README.md for details.
## Valid options: 1, 2
# metric_version = 1
## Use HTTP Basic Authentication.
# basic_username = "Foo"
# basic_password = "Bar"
## If set, the IP Ranges which are allowed to access metrics.
## ex: ip_range = ["192.168.0.0/24", "192.168.1.0/30"]
# ip_range = []
## Path to publish the metrics on.
# path = "/metrics"
## Expiration interval for each metric. 0 == no expiration
# expiration_interval = "60s"
## Collectors to enable, valid entries are "gocollector" and "process".
## If unset, both are enabled.
# collectors_exclude = ["gocollector", "process"]
## Send string metrics as Prometheus labels.
## Unless set to false all string metrics will be sent as labels.
# string_as_label = true
## If set, enable TLS with the given certificate.
# tls_cert = "/etc/ssl/telegraf.crt"
# tls_key = "/etc/ssl/telegraf.key"
## Set one or more allowed client CA certificate file names to
## enable mutually authenticated TLS connections
# tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"]
## Export metric collection time.
# export_timestamp = false
## Set custom headers for HTTP responses.
# http_headers = {"X-Special-Header" = "Special-Value"}
## Specify the metric type explicitly.
## This overrides the metric-type of the Telegraf metric. Globbing is allowed.
# [outputs.prometheus_client.metric_types]
# counter = []
# gauge = []

View file

@ -0,0 +1,424 @@
package v1
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/influxdata/telegraf"
serializers_prometheus "github.com/influxdata/telegraf/plugins/serializers/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
TypeMapping serializers_prometheus.MetricTypes
Log telegraf.Logger
sync.Mutex
fam map[string]*MetricFamily
expireTicker *time.Ticker
}
func NewCollector(expire time.Duration, stringsAsLabel, exportTimestamp bool, typeMapping serializers_prometheus.MetricTypes, log telegraf.Logger) *Collector {
c := &Collector{
ExpirationInterval: expire,
StringAsLabel: stringsAsLabel,
ExportTimestamp: exportTimestamp,
TypeMapping: typeMapping,
Log: log,
fam: make(map[string]*MetricFamily),
}
if c.ExpirationInterval != 0 {
c.expireTicker = time.NewTicker(c.ExpirationInterval)
go func() {
for {
<-c.expireTicker.C
c.Expire(time.Now())
}
}()
}
return c
}
func (*Collector) Describe(ch chan<- *prometheus.Desc) {
prometheus.NewGauge(prometheus.GaugeOpts{Name: "Dummy", Help: "Dummy"}).Describe(ch)
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
// Expire metrics, doing this on Collect ensure metrics are removed even if no
// new metrics are added to the output.
if c.ExpirationInterval != 0 {
c.Expire(time.Now())
}
c.Lock()
defer c.Unlock()
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 {
pointType := c.TypeMapping.DetermineType(mname, point)
fam = &MetricFamily{
Samples: make(map[SampleID]*Sample),
TelegrafValueType: pointType,
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.addMetrics(metrics)
// Expire metrics, doing this on Add ensure metrics are removed even if no
// new metrics are added to the output.
if c.ExpirationInterval != 0 {
c.Expire(time.Now())
}
return nil
}
func (c *Collector) addMetrics(metrics []telegraf.Metric) {
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 {
name, ok := serializers_prometheus.SanitizeLabelName(k)
if !ok {
continue
}
labels[name] = 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() {
sfv, ok := fv.(string)
if !ok {
continue
}
name, ok := serializers_prometheus.SanitizeLabelName(fn)
if !ok {
continue
}
labels[name] = sfv
}
}
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)
}
}
}
}
func (c *Collector) Expire(now time.Time) {
c.Lock()
defer c.Unlock()
for name, family := range c.fam {
for key, sample := range family.Samples {
if 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,98 @@
package v2
import (
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/influxdata/telegraf"
serializers_prometheus "github.com/influxdata/telegraf/plugins/serializers/prometheus"
)
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 *serializers_prometheus.Collection
}
func NewCollector(expire time.Duration, stringsAsLabel, exportTimestamp bool, typeMapping serializers_prometheus.MetricTypes) *Collector {
cfg := serializers_prometheus.FormatConfig{
StringAsLabel: stringsAsLabel,
ExportTimestamp: exportTimestamp,
TypeMappings: typeMapping,
}
return &Collector{
expireDuration: expire,
coll: serializers_prometheus.NewCollection(cfg),
}
}
func (*Collector) Describe(_ 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.
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
c.Lock()
defer c.Unlock()
// Expire metrics, doing this on Collect ensure metrics are removed even if no
// new metrics are added to the output.
if c.expireDuration != 0 {
c.coll.Expire(time.Now(), c.expireDuration)
}
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, time.Now())
}
// Expire metrics, doing this on Add ensure metrics are removed even if no
// one is querying the data.
if c.expireDuration != 0 {
c.coll.Expire(time.Now(), c.expireDuration)
}
return nil
}