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,45 @@
# Prometheus remote write
The `prometheusremotewrite` data format converts metrics into the Prometheus protobuf
exposition format.
**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.http]]
## URL is the address to send metrics to
url = "https://cortex/api/prom/push"
## Optional TLS Config
tls_ca = "/etc/telegraf/ca.pem"
tls_cert = "/etc/telegraf/cert.pem"
tls_key = "/etc/telegraf/key.pem"
## Data format to output.
data_format = "prometheusremotewrite"
[outputs.http.headers]
Content-Type = "application/x-protobuf"
Content-Encoding = "snappy"
X-Prometheus-Remote-Write-Version = "0.1.0"
```
### Metrics
A Prometheus metric is created for each integer, float, boolean or unsigned
field. Boolean values are converted to *1.0* for true and *0.0* for false.
The Prometheus metric names are produced by joining the measurement name with
the field key. In the special case where the measurement name is `prometheus`
it is not included in the final metric name.
Prometheus labels are produced for each tag.
**Note:** String fields are ignored and do not produce Prometheus metrics.
Set **log_level** to `trace` to see all serialization issues.

View file

@ -0,0 +1,357 @@
package prometheusremotewrite
import (
"bytes"
"fmt"
"hash/fnv"
"sort"
"strconv"
"strings"
"time"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/prompb"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/serializers"
"github.com/influxdata/telegraf/plugins/serializers/prometheus"
)
type MetricKey uint64
type Serializer struct {
SortMetrics bool `toml:"prometheus_sort_metrics"`
StringAsLabel bool `toml:"prometheus_string_as_label"`
Log telegraf.Logger `toml:"-"`
}
func (s *Serializer) Serialize(metric telegraf.Metric) ([]byte, error) {
return s.SerializeBatch([]telegraf.Metric{metric})
}
func (s *Serializer) SerializeBatch(metrics []telegraf.Metric) ([]byte, error) {
var lastErr error
// traceAndKeepErr logs on Trace level every passed error.
// with each call it updates lastErr, so it can be logged later with higher level.
traceAndKeepErr := func(format string, a ...any) {
lastErr = fmt.Errorf(format, a...)
s.Log.Trace(lastErr)
}
var buf bytes.Buffer
var entries = make(map[MetricKey]prompb.TimeSeries)
var labels = make([]prompb.Label, 0)
for _, metric := range metrics {
labels = s.appendCommonLabels(labels[:0], metric)
var metrickey MetricKey
var promts prompb.TimeSeries
for _, field := range metric.FieldList() {
rawName := prometheus.MetricName(metric.Name(), field.Key, metric.Type())
metricName, ok := prometheus.SanitizeMetricName(rawName)
if !ok {
traceAndKeepErr("failed to parse metric name %q", rawName)
continue
}
switch metric.Type() {
case telegraf.Counter:
fallthrough
case telegraf.Gauge:
fallthrough
case telegraf.Untyped:
value, ok := prometheus.SampleValue(field.Value)
if !ok {
traceAndKeepErr("failed to parse %q: bad sample value %#v", metricName, field.Value)
continue
}
metrickey, promts = getPromTS(metricName, labels, value, metric.Time())
case telegraf.Histogram:
switch {
case strings.HasSuffix(field.Key, "_bucket"):
// if bucket only, init sum, count, inf
metrickeysum, promtssum := getPromTS(metricName+"_sum", labels, float64(0), metric.Time())
if _, ok = entries[metrickeysum]; !ok {
entries[metrickeysum] = promtssum
}
metrickeycount, promtscount := getPromTS(metricName+"_count", labels, float64(0), metric.Time())
if _, ok = entries[metrickeycount]; !ok {
entries[metrickeycount] = promtscount
}
extraLabel := prompb.Label{
Name: "le",
Value: "+Inf",
}
metrickeyinf, promtsinf := getPromTS(metricName+"_bucket", labels, float64(0), metric.Time(), extraLabel)
if _, ok = entries[metrickeyinf]; !ok {
entries[metrickeyinf] = promtsinf
}
le, ok := metric.GetTag("le")
if !ok {
traceAndKeepErr("failed to parse %q: can't find `le` label", metricName)
continue
}
bound, err := strconv.ParseFloat(le, 64)
if err != nil {
traceAndKeepErr("failed to parse %q: can't parse %q value: %w", metricName, le, err)
continue
}
count, ok := prometheus.SampleCount(field.Value)
if !ok {
traceAndKeepErr("failed to parse %q: bad sample value %#v", metricName, field.Value)
continue
}
extraLabel = prompb.Label{
Name: "le",
Value: fmt.Sprint(bound),
}
metrickey, promts = getPromTS(metricName+"_bucket", labels, float64(count), metric.Time(), extraLabel)
case strings.HasSuffix(field.Key, "_sum"):
sum, ok := prometheus.SampleSum(field.Value)
if !ok {
traceAndKeepErr("failed to parse %q: bad sample value %#v", metricName, field.Value)
continue
}
metrickey, promts = getPromTS(metricName+"_sum", labels, sum, metric.Time())
case strings.HasSuffix(field.Key, "_count"):
count, ok := prometheus.SampleCount(field.Value)
if !ok {
traceAndKeepErr("failed to parse %q: bad sample value %#v", metricName, field.Value)
continue
}
// if no bucket generate +Inf entry
extraLabel := prompb.Label{
Name: "le",
Value: "+Inf",
}
metrickeyinf, promtsinf := getPromTS(metricName+"_bucket", labels, float64(count), metric.Time(), extraLabel)
if minf, ok := entries[metrickeyinf]; !ok || minf.Samples[0].Value == 0 {
entries[metrickeyinf] = promtsinf
}
metrickey, promts = getPromTS(metricName+"_count", labels, float64(count), metric.Time())
default:
traceAndKeepErr("failed to parse %q: series %q should have `_count`, `_sum` or `_bucket` suffix", metricName, field.Key)
continue
}
case telegraf.Summary:
switch {
case strings.HasSuffix(field.Key, "_sum"):
sum, ok := prometheus.SampleSum(field.Value)
if !ok {
traceAndKeepErr("failed to parse %q: bad sample value %#v", metricName, field.Value)
continue
}
metrickey, promts = getPromTS(metricName+"_sum", labels, sum, metric.Time())
case strings.HasSuffix(field.Key, "_count"):
count, ok := prometheus.SampleCount(field.Value)
if !ok {
traceAndKeepErr("failed to parse %q: bad sample value %#v", metricName, field.Value)
continue
}
metrickey, promts = getPromTS(metricName+"_count", labels, float64(count), metric.Time())
default:
quantileTag, ok := metric.GetTag("quantile")
if !ok {
traceAndKeepErr("failed to parse %q: can't find `quantile` label", metricName)
continue
}
quantile, err := strconv.ParseFloat(quantileTag, 64)
if err != nil {
traceAndKeepErr("failed to parse %q: can't parse %q value: %w", metricName, quantileTag, err)
continue
}
value, ok := prometheus.SampleValue(field.Value)
if !ok {
traceAndKeepErr("failed to parse %q: bad sample value %#v", metricName, field.Value)
continue
}
extraLabel := prompb.Label{
Name: "quantile",
Value: fmt.Sprint(quantile),
}
metrickey, promts = getPromTS(metricName, labels, value, metric.Time(), extraLabel)
}
default:
return nil, fmt.Errorf("unknown type %v", metric.Type())
}
// 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.
m, ok := entries[metrickey]
if ok {
if metric.Time().Before(time.Unix(0, m.Samples[0].Timestamp*1_000_000)) {
traceAndKeepErr("metric %q has samples with timestamp %v older than already registered before", metric.Name(), metric.Time())
continue
}
}
entries[metrickey] = promts
}
}
if lastErr != nil {
// log only the last recorded error in the batch, as it could have many errors and logging each one
// could be too verbose. The following log line still provides enough info for user to act on.
s.Log.Warnf("some series were dropped, %d series left to send; last recorded error: %v", len(entries), lastErr)
}
var promTS = make([]prompb.TimeSeries, len(entries))
var i int
for _, promts := range entries {
promTS[i] = promts
i++
}
if s.SortMetrics {
sort.Slice(promTS, func(i, j int) bool {
lhs := promTS[i].Labels
rhs := promTS[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
})
}
pb := &prompb.WriteRequest{Timeseries: promTS}
data, err := pb.Marshal()
if err != nil {
return nil, fmt.Errorf("unable to marshal protobuf: %w", err)
}
encoded := snappy.Encode(nil, data)
buf.Write(encoded)
return buf.Bytes(), nil
}
func hasLabel(name string, labels []prompb.Label) bool {
for _, label := range labels {
if name == label.Name {
return true
}
}
return false
}
func (s *Serializer) appendCommonLabels(labels []prompb.Label, metric telegraf.Metric) []prompb.Label {
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 := prometheus.SanitizeLabelName(tag.Key)
if !ok {
continue
}
// remove tags with empty values
if tag.Value == "" {
continue
}
labels = append(labels, prompb.Label{Name: name, Value: tag.Value})
}
if !s.StringAsLabel {
return labels
}
for _, field := range metric.FieldList() {
value, ok := field.Value.(string)
if !ok {
continue
}
name, ok := prometheus.SanitizeLabelName(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, prompb.Label{Name: name, Value: value})
}
return labels
}
func MakeMetricKey(labels []prompb.Label) 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())
}
func getPromTS(name string, labels []prompb.Label, value float64, ts time.Time, extraLabels ...prompb.Label) (MetricKey, prompb.TimeSeries) {
labelscopy := make([]prompb.Label, len(labels), len(labels)+1)
copy(labelscopy, labels)
sample := []prompb.Sample{{
// Timestamp is int milliseconds for remote write.
Timestamp: ts.UnixNano() / int64(time.Millisecond),
Value: value,
}}
labelscopy = append(labelscopy, extraLabels...)
labelscopy = append(labelscopy, prompb.Label{
Name: "__name__",
Value: name,
})
// we sort the labels since Prometheus TSDB does not like out of order labels
sort.Sort(sortableLabels(labelscopy))
return MakeMetricKey(labelscopy), prompb.TimeSeries{Labels: labelscopy, Samples: sample}
}
type sortableLabels []prompb.Label
func (sl sortableLabels) Len() int { return len(sl) }
func (sl sortableLabels) Less(i, j int) bool {
return sl[i].Name < sl[j].Name
}
func (sl sortableLabels) Swap(i, j int) {
sl[i], sl[j] = sl[j], sl[i]
}
func init() {
serializers.Add("prometheusremotewrite",
func() telegraf.Serializer {
return &Serializer{}
},
)
}

View file

@ -0,0 +1,829 @@
package prometheusremotewrite
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/golang/snappy"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/prompb"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/serializers"
"github.com/influxdata/telegraf/testutil"
)
func BenchmarkRemoteWrite(b *testing.B) {
batch := make([]telegraf.Metric, 1000)
for i := range batch {
batch[i] = testutil.MustMetric(
"cpu",
map[string]string{
"host": "example.org",
"C": "D",
"A": "B",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
)
}
s := &Serializer{Log: &testutil.CaptureLogger{}}
for n := 0; n < b.N; n++ {
//nolint:errcheck // Benchmarking so skip the error check to avoid the unnecessary operations
s.SerializeBatch(batch)
}
}
func TestRemoteWriteSerialize(t *testing.T) {
tests := []struct {
name string
metric telegraf.Metric
expected []byte
}{
// the only way that we can produce an empty metric name is if the
// metric is called "prometheus" and has no fields.
{
name: "empty name is skipped",
metric: testutil.MustMetric(
"prometheus",
map[string]string{
"host": "example.org",
},
map[string]interface{}{},
time.Unix(0, 0),
),
expected: []byte(``),
},
{
name: "empty labels are skipped",
metric: testutil.MustMetric(
"cpu",
map[string]string{
"": "example.org",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
expected: []byte(`
cpu_time_idle 42
`),
},
{
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(`
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(`
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(`
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(`
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(`
http_request_duration_seconds_count 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_bucket{le="+Inf"} 144320
`),
},
{
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(`
http_request_duration_seconds_count 0
http_request_duration_seconds_sum 0
http_request_duration_seconds_bucket{le="+Inf"} 0
http_request_duration_seconds_bucket{le="0.5"} 129389
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Serializer{
Log: &testutil.CaptureLogger{},
SortMetrics: true,
}
data, err := s.Serialize(tt.metric)
require.NoError(t, err)
actual, err := prompbToText(data)
require.NoError(t, err)
require.Equal(t, strings.TrimSpace(string(tt.expected)),
strings.TrimSpace(string(actual)))
})
}
}
func TestRemoteWriteSerializeNegative(t *testing.T) {
clog := &testutil.CaptureLogger{}
s := &Serializer{Log: clog}
assert := func(msg string, err error) {
t.Helper()
require.NoError(t, err)
warnings := clog.Warnings()
require.NotEmpty(t, warnings, "expected non-empty last message")
lastMsg := warnings[len(warnings)-1]
require.Contains(t, lastMsg, msg, "unexpected log message")
// reset logger so it can be reused again
clog.Clear()
}
m := testutil.MustMetric("@@!!", nil, map[string]interface{}{"!!": "@@"}, time.Unix(0, 0))
_, err := s.Serialize(m)
assert("failed to parse metric name \"@@!!_!!\"", err)
m = testutil.MustMetric("prometheus", nil,
map[string]interface{}{
"http_requests_total": "asd",
},
time.Unix(0, 0),
)
_, err = s.Serialize(m)
assert("bad sample", err)
m = testutil.MustMetric(
"prometheus",
map[string]string{
"le": "0.5",
},
map[string]interface{}{
"http_request_duration_seconds_bucket": "asd",
},
time.Unix(0, 0),
telegraf.Histogram,
)
_, err = s.Serialize(m)
assert("bad sample", err)
m = testutil.MustMetric(
"prometheus",
map[string]string{
"code": "400",
"method": "post",
},
map[string]interface{}{
"http_requests_total": 3.0,
"http_requests_errors_total": "3.0",
},
time.Unix(0, 0),
telegraf.Gauge,
)
_, err = s.Serialize(m)
assert("bad sample", err)
m = testutil.MustMetric(
"prometheus",
map[string]string{"quantile": "0.01a"},
map[string]interface{}{
"rpc_duration_seconds": 3102.0,
},
time.Unix(0, 0),
telegraf.Summary,
)
_, err = s.Serialize(m)
assert("failed to parse", err)
}
func TestRemoteWriteSerializeBatch(t *testing.T) {
tests := []struct {
name string
metrics []telegraf.Metric
stringAsLabel bool
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(`
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(`
cpu_time_guest{host="one.example.org"} 42
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(`
http_request_duration_seconds_count 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_bucket{le="+Inf"} 144320
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
`),
},
{
name: "summary with quantile",
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(`
rpc_duration_seconds_count 2693
rpc_duration_seconds_sum 17560473
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
`),
},
{
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(`
cpu_time_idle 43
`),
},
{
name: "colons are not replaced in metric name from measurement",
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu::xyzzy",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
cpu::xyzzy_time_idle 42
`),
},
{
name: "colons are not replaced in metric name from field",
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time:idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
cpu_time:idle 42
`),
},
{
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(`
cpu_time_idle{host_name="example.org"} 42
`),
},
{
name: "colons are replaced in label name",
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(`
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(`
cpu_time_idle 42
`),
},
{
name: "string as label",
stringAsLabel: true,
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"time_idle": 42.0,
"cpu": "cpu0",
},
time.Unix(0, 0),
),
},
expected: []byte(`
cpu_time_idle{cpu="cpu0"} 42
`),
},
{
name: "string as label duplicate tag",
stringAsLabel: true,
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(`
cpu_time_idle{cpu="cpu0"} 42
`),
},
{
name: "replace characters when using string as label",
stringAsLabel: true,
metrics: []telegraf.Metric{
testutil.MustMetric(
"cpu",
map[string]string{},
map[string]interface{}{
"host:name": "example.org",
"time_idle": 42.0,
},
time.Unix(1574279268, 0),
),
},
expected: []byte(`
cpu_time_idle{host_name="example.org"} 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(`
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
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
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
`),
},
{
name: "summary with no quantile",
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(`
rpc_duration_seconds_count 2693
rpc_duration_seconds_sum 17560473
`),
},
{
name: "empty label string value",
stringAsLabel: true,
metrics: []telegraf.Metric{
testutil.MustMetric(
"prometheus",
map[string]string{
"cpu": "",
},
map[string]interface{}{
"time_idle": 42.0,
},
time.Unix(0, 0),
),
},
expected: []byte(`
time_idle 42
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Serializer{
Log: &testutil.CaptureLogger{},
SortMetrics: true,
StringAsLabel: tt.stringAsLabel,
}
data, err := s.SerializeBatch(tt.metrics)
require.NoError(t, err)
actual, err := prompbToText(data)
require.NoError(t, err)
require.Equal(t,
strings.TrimSpace(string(tt.expected)),
strings.TrimSpace(string(actual)))
})
}
}
func prompbToText(data []byte) ([]byte, error) {
var buf = bytes.Buffer{}
protobuff, err := snappy.Decode(nil, data)
if err != nil {
return nil, err
}
var req prompb.WriteRequest
err = req.Unmarshal(protobuff)
if err != nil {
return nil, err
}
samples := protoToSamples(&req)
for _, sample := range samples {
buf.WriteString(fmt.Sprintf("%s %s\n", sample.Metric.String(), sample.Value.String()))
}
return buf.Bytes(), nil
}
func protoToSamples(req *prompb.WriteRequest) model.Samples {
var samples model.Samples
for _, ts := range req.Timeseries {
metric := make(model.Metric, len(ts.Labels))
for _, l := range ts.Labels {
metric[model.LabelName(l.Name)] = model.LabelValue(l.Value)
}
for _, s := range ts.Samples {
samples = append(samples, &model.Sample{
Metric: metric,
Value: model.SampleValue(s.Value),
Timestamp: model.Time(s.Timestamp),
})
}
}
return samples
}
func BenchmarkSerialize(b *testing.B) {
s := &Serializer{Log: &testutil.CaptureLogger{}}
metrics := serializers.BenchmarkMetrics(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := s.Serialize(metrics[i%len(metrics)])
require.NoError(b, err)
}
}
func BenchmarkSerializeBatch(b *testing.B) {
s := &Serializer{Log: &testutil.CaptureLogger{}}
m := serializers.BenchmarkMetrics(b)
metrics := m[:]
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := s.SerializeBatch(metrics)
require.NoError(b, err)
}
}