Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
45
plugins/serializers/prometheusremotewrite/README.md
Normal file
45
plugins/serializers/prometheusremotewrite/README.md
Normal 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.
|
|
@ -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{}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue