diff --git a/exporter/prometheusremotewriteexporter/config.go b/exporter/prometheusremotewriteexporter/config.go index 9744d87a331..5fea5770f1d 100644 --- a/exporter/prometheusremotewriteexporter/config.go +++ b/exporter/prometheusremotewriteexporter/config.go @@ -1,4 +1,4 @@ -// Copyright 2020 The OpenTelemetry Authors +// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/exporter/prometheusremotewriteexporter/config_test.go b/exporter/prometheusremotewriteexporter/config_test.go index a25327955ce..0fc421927f1 100644 --- a/exporter/prometheusremotewriteexporter/config_test.go +++ b/exporter/prometheusremotewriteexporter/config_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 The OpenTelemetry Authors +// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/exporter/prometheusremotewriteexporter/exporter.go b/exporter/prometheusremotewriteexporter/exporter.go index f6099a9762a..c396a4f6593 100644 --- a/exporter/prometheusremotewriteexporter/exporter.go +++ b/exporter/prometheusremotewriteexporter/exporter.go @@ -1,4 +1,4 @@ -// Copyright 2020 The OpenTelemetry Authors +// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/exporter/prometheusremotewriteexporter/exporter_test.go b/exporter/prometheusremotewriteexporter/exporter_test.go index 5334f0cc56d..8473a3b2c48 100644 --- a/exporter/prometheusremotewriteexporter/exporter_test.go +++ b/exporter/prometheusremotewriteexporter/exporter_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 The OpenTelemetry Authors +// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/exporter/prometheusremotewriteexporter/factory.go b/exporter/prometheusremotewriteexporter/factory.go index 5cc0efb0ddd..324558296c5 100644 --- a/exporter/prometheusremotewriteexporter/factory.go +++ b/exporter/prometheusremotewriteexporter/factory.go @@ -1,4 +1,4 @@ -// Copyright 2020 The OpenTelemetry Authors +// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -66,11 +66,7 @@ func createMetricsExporter(_ context.Context, _ component.ExporterCreateParams, exporterhelper.WithShutdown(prwe.shutdown), ) - if err != nil { - return nil, err - } - - return prwexp, nil + return prwexp, err } func createDefaultConfig() configmodels.Exporter { diff --git a/exporter/prometheusremotewriteexporter/factory_test.go b/exporter/prometheusremotewriteexporter/factory_test.go index 65c4ac656c3..8688c878439 100644 --- a/exporter/prometheusremotewriteexporter/factory_test.go +++ b/exporter/prometheusremotewriteexporter/factory_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 The OpenTelemetry Authors +// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/exporter/prometheusremotewriteexporter/helper.go b/exporter/prometheusremotewriteexporter/helper.go new file mode 100644 index 00000000000..46589e53ed3 --- /dev/null +++ b/exporter/prometheusremotewriteexporter/helper.go @@ -0,0 +1,210 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheusremotewriteexporter + +import ( + "log" + "sort" + "strings" + "unicode" + + "github.com/prometheus/prometheus/prompb" + + common "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/common/v1" + otlp "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/metrics/v1" +) + +const ( + totalStr = "total" + delimeter = "_" + keyStr = "key" +) + +// ByLabelName enables the usage of sort.Sort() with a slice of labels +type ByLabelName []prompb.Label + +func (a ByLabelName) Len() int { return len(a) } +func (a ByLabelName) Less(i, j int) bool { return a[i].Name < a[j].Name } +func (a ByLabelName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +// validateMetrics returns a bool representing whether the metric has a valid type and temporality combination. +func validateMetrics(desc *otlp.MetricDescriptor) bool { + + if desc == nil { + return false + } + + switch desc.GetType() { + case otlp.MetricDescriptor_MONOTONIC_DOUBLE, otlp.MetricDescriptor_MONOTONIC_INT64, + otlp.MetricDescriptor_HISTOGRAM, otlp.MetricDescriptor_SUMMARY: + return desc.GetTemporality() == otlp.MetricDescriptor_CUMULATIVE + case otlp.MetricDescriptor_INT64, otlp.MetricDescriptor_DOUBLE: + return true + } + + return false +} + +// addSample finds a TimeSeries in tsMap that corresponds to the label set labels, and add sample to the TimeSeries; it +// creates a new TimeSeries in the map if not found. tsMap is unmodified if either of its parameters is nil. +func addSample(tsMap map[string]*prompb.TimeSeries, sample *prompb.Sample, labels []prompb.Label, + ty otlp.MetricDescriptor_Type) { + + if sample == nil || labels == nil || tsMap == nil { + return + } + + sig := timeSeriesSignature(ty, &labels) + ts, ok := tsMap[sig] + + if ok { + ts.Samples = append(ts.Samples, *sample) + } else { + newTs := &prompb.TimeSeries{ + Labels: labels, + Samples: []prompb.Sample{*sample}, + } + tsMap[sig] = newTs + } +} + +// timeSeries return a string signature in the form of: +// TYPE-label1-value1- ... -labelN-valueN +// the label slice should not contain duplicate label names; this method sorts the slice by label name before creating +// the signature. +func timeSeriesSignature(t otlp.MetricDescriptor_Type, labels *[]prompb.Label) string { + b := strings.Builder{} + b.WriteString(t.String()) + + sort.Sort(ByLabelName(*labels)) + + for _, lb := range *labels { + b.WriteString("-") + b.WriteString(lb.GetName()) + b.WriteString("-") + b.WriteString(lb.GetValue()) + } + + return b.String() +} + +// createLabelSet creates a slice of Cortex Label with OTLP labels and paris of string values. +// Unpaired string value is ignored. String pairs overwrites OTLP labels if collision happens, and the overwrite is +// logged. Resultant label names are sanitized. +func createLabelSet(labels []*common.StringKeyValue, extras ...string) []prompb.Label { + + // map ensures no duplicate label name + l := map[string]prompb.Label{} + + for _, lb := range labels { + l[lb.Key] = prompb.Label{ + Name: sanitize(lb.Key), + Value: lb.Value, + } + } + + for i := 0; i < len(extras); i += 2 { + if i+1 >= len(extras) { + break + } + _, found := l[extras[i]] + if found { + log.Println("label " + extras[i] + " is overwritten. Check if Prometheus reserved labels are used.") + } + // internal labels should be maintained + name := extras[i] + if !(len(name) > 4 && name[:2] == "__" && name[len(name)-2:] == "__") { + name = sanitize(name) + } + l[extras[i]] = prompb.Label{ + Name: name, + Value: extras[i+1], + } + } + + s := make([]prompb.Label, 0, len(l)) + + for _, lb := range l { + s = append(s, lb) + } + + return s +} + +// getPromMetricName creates a Prometheus metric name by attaching namespace prefix, and _total suffix for Monotonic +// metrics. +func getPromMetricName(desc *otlp.MetricDescriptor, ns string) string { + + if desc == nil { + return "" + } + // whether _total suffix should be applied + isCounter := desc.Type == otlp.MetricDescriptor_MONOTONIC_INT64 || + desc.Type == otlp.MetricDescriptor_MONOTONIC_DOUBLE + + b := strings.Builder{} + + b.WriteString(ns) + + if b.Len() > 0 { + b.WriteString(delimeter) + } + b.WriteString(desc.GetName()) + + // Including units makes two metrics with the same name and label set belong to two different TimeSeries if the + // units are different. + /* + if b.Len() > 0 && len(desc.GetUnit()) > 0{ + fmt.Fprintf(&b, delimeter) + fmt.Fprintf(&b, desc.GetUnit()) + } + */ + + if b.Len() > 0 && isCounter { + b.WriteString(delimeter) + b.WriteString(totalStr) + } + return sanitize(b.String()) +} + +// copied from prometheus-go-metric-exporter +// sanitize replaces non-alphanumeric characters with underscores in s. +func sanitize(s string) string { + if len(s) == 0 { + return s + } + + // Note: No length limit for label keys because Prometheus doesn't + // define a length limit, thus we should NOT be truncating label keys. + // See https://github.com/orijtech/prometheus-go-metrics-exporter/issues/4. + s = strings.Map(sanitizeRune, s) + if unicode.IsDigit(rune(s[0])) { + s = keyStr + delimeter + s + } + if s[0] == '_' { + s = keyStr + s + } + return s +} + +// copied from prometheus-go-metric-exporter +// sanitizeRune converts anything that is not a letter or digit to an underscore +func sanitizeRune(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + return r + } + // Everything else turns into an underscore + return '_' +} diff --git a/exporter/prometheusremotewriteexporter/helper_test.go b/exporter/prometheusremotewriteexporter/helper_test.go new file mode 100644 index 00000000000..c43dd41c089 --- /dev/null +++ b/exporter/prometheusremotewriteexporter/helper_test.go @@ -0,0 +1,282 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheusremotewriteexporter + +import ( + "strconv" + "testing" + + "github.com/prometheus/prometheus/prompb" + "github.com/stretchr/testify/assert" + + common "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/common/v1" + otlp "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/metrics/v1" +) + +// Test_validateMetrics checks validateMetrics return true if a type and temporality combination is valid, false +// otherwise. +func Test_validateMetrics(t *testing.T) { + // define a single test + type combTest struct { + name string + desc *otlp.MetricDescriptor + want bool + } + + tests := []combTest{} + + // append true cases + for i := range validCombinations { + name := "valid_" + strconv.Itoa(i) + desc := getDescriptor(name, i, validCombinations) + tests = append(tests, combTest{ + name, + desc, + true, + }) + } + // append false cases + for i := range invalidCombinations { + name := "invalid_" + strconv.Itoa(i) + desc := getDescriptor(name, i, invalidCombinations) + tests = append(tests, combTest{ + name, + desc, + false, + }) + } + // append nil case + tests = append(tests, combTest{"invalid_nil", nil, false}) + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := validateMetrics(tt.desc) + assert.Equal(t, tt.want, got) + }) + } +} + +// Test_addSample checks addSample updates the map it receives correctly based on the sample and Label +// set it receives. +// Test cases are two samples belonging to the same TimeSeries, two samples belong to different TimeSeries, and nil +// case. +func Test_addSample(t *testing.T) { + type testCase struct { + desc otlp.MetricDescriptor_Type + sample prompb.Sample + labels []prompb.Label + } + + tests := []struct { + name string + orig map[string]*prompb.TimeSeries + testCase []testCase + want map[string]*prompb.TimeSeries + }{ + { + "two_points_same_ts_same_metric", + map[string]*prompb.TimeSeries{}, + []testCase{ + {otlp.MetricDescriptor_INT64, + getSample(float64(intVal1), msTime1), + promLbs1, + }, + { + otlp.MetricDescriptor_INT64, + getSample(float64(intVal2), msTime2), + promLbs1, + }, + }, + twoPointsSameTs, + }, + { + "two_points_different_ts_same_metric", + map[string]*prompb.TimeSeries{}, + []testCase{ + {otlp.MetricDescriptor_INT64, + getSample(float64(intVal1), msTime1), + promLbs1, + }, + {otlp.MetricDescriptor_INT64, + getSample(float64(intVal1), msTime2), + promLbs2, + }, + }, + twoPointsDifferentTs, + }, + } + t.Run("nil_case", func(t *testing.T) { + tsMap := map[string]*prompb.TimeSeries{} + addSample(tsMap, nil, nil, 0) + assert.Exactly(t, tsMap, map[string]*prompb.TimeSeries{}) + }) + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addSample(tt.orig, &tt.testCase[0].sample, tt.testCase[0].labels, tt.testCase[0].desc) + addSample(tt.orig, &tt.testCase[1].sample, tt.testCase[1].labels, tt.testCase[1].desc) + assert.Exactly(t, tt.want, tt.orig) + }) + } +} + +// Test_timeSeries checks timeSeriesSignature returns consistent and unique signatures for a distinct label set and +// metric type combination. +func Test_timeSeriesSignature(t *testing.T) { + tests := []struct { + name string + lbs []prompb.Label + desc otlp.MetricDescriptor_Type + want string + }{ + { + "int64_signature", + promLbs1, + otlp.MetricDescriptor_INT64, + typeInt64 + lb1Sig, + }, + { + "histogram_signature", + promLbs2, + otlp.MetricDescriptor_HISTOGRAM, + typeHistogram + lb2Sig, + }, + { + "unordered_signature", + getPromLabels(label22, value22, label21, value21), + otlp.MetricDescriptor_HISTOGRAM, + typeHistogram + lb2Sig, + }, + // descriptor type cannot be nil, as checked by validateMetrics + { + "nil_case", + nil, + otlp.MetricDescriptor_HISTOGRAM, + typeHistogram, + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.EqualValues(t, tt.want, timeSeriesSignature(tt.desc, &tt.lbs)) + }) + } +} + +// Test_createLabelSet checks resultant label names are sanitized and label in extra overrides label in labels if +// collision happens. It does not check whether labels are not sorted +func Test_createLabelSet(t *testing.T) { + tests := []struct { + name string + orig []*common.StringKeyValue + extras []string + want []prompb.Label + }{ + { + "labels_clean", + lbs1, + []string{label31, value31, label32, value32}, + getPromLabels(label11, value11, label12, value12, label31, value31, label32, value32), + }, + { + "labels_duplicate_in_extras", + lbs1, + []string{label11, value31}, + getPromLabels(label11, value31, label12, value12), + }, + { + "labels_dirty", + lbs1Dirty, + []string{label31 + dirty1, value31, label32, value32}, + getPromLabels(label11+"_", value11, "key_"+label12, value12, label31+"_", value31, label32, value32), + }, + { + "no_original_case", + nil, + []string{label31, value31, label32, value32}, + getPromLabels(label31, value31, label32, value32), + }, + { + "empty_extra_case", + lbs1, + []string{"", ""}, + getPromLabels(label11, value11, label12, value12, "", ""), + }, + { + "single_left_over_case", + lbs1, + []string{label31, value31, label32}, + getPromLabels(label11, value11, label12, value12, label31, value31), + }, + } + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.want, createLabelSet(tt.orig, tt.extras...)) + }) + } +} + +// Tes_getPromMetricName checks if OTLP metric names are converted to Cortex metric names correctly. +// Test cases are empty namespace, monotonic metrics that require a total suffix, and metric names that contains +// invalid characters. +func Test_getPromMetricName(t *testing.T) { + tests := []struct { + name string + desc *otlp.MetricDescriptor + ns string + want string + }{ + { + "nil_case", + nil, + ns1, + "", + }, + { + "normal_case", + getDescriptor(name1, histogramComb, validCombinations), + ns1, + "test_ns_valid_single_int_point", + }, + { + "empty_namespace", + getDescriptor(name1, summaryComb, validCombinations), + "", + "valid_single_int_point", + }, + { + "total_suffix", + getDescriptor(name1, monotonicInt64Comb, validCombinations), + ns1, + "test_ns_valid_single_int_point_total", + }, + { + "dirty_string", + getDescriptor(name1+dirty1, monotonicInt64Comb, validCombinations), + "7" + ns1, + "key_7test_ns_valid_single_int_point__total", + }, + } + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, getPromMetricName(tt.desc, tt.ns)) + }) + } + +} diff --git a/exporter/prometheusremotewriteexporter/testutil_test.go b/exporter/prometheusremotewriteexporter/testutil_test.go new file mode 100644 index 00000000000..4fc1b7c9454 --- /dev/null +++ b/exporter/prometheusremotewriteexporter/testutil_test.go @@ -0,0 +1,164 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheusremotewriteexporter + +import ( + "time" + + "github.com/prometheus/prometheus/prompb" + + commonpb "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/common/v1" + otlp "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/metrics/v1" +) + +type combination struct { + ty otlp.MetricDescriptor_Type + temp otlp.MetricDescriptor_Temporality +} + +var ( + time1 = uint64(time.Now().UnixNano()) + time2 = uint64(time.Date(1970, 1, 0, 0, 0, 0, 0, time.UTC).UnixNano()) + msTime1 = int64(time1 / uint64(int64(time.Millisecond)/int64(time.Nanosecond))) + msTime2 = int64(time2 / uint64(int64(time.Millisecond)/int64(time.Nanosecond))) + + typeInt64 = "INT64" + + typeHistogram = "HISTOGRAM" + + label11 = "test_label11" + value11 = "test_value11" + label12 = "test_label12" + value12 = "test_value12" + label21 = "test_label21" + value21 = "test_value21" + label22 = "test_label22" + value22 = "test_value22" + label31 = "test_label31" + value31 = "test_value31" + label32 = "test_label32" + value32 = "test_value32" + dirty1 = "%" + dirty2 = "?" + + intVal1 int64 = 1 + intVal2 int64 = 2 + + lbs1 = getLabels(label11, value11, label12, value12) + lbs1Dirty = getLabels(label11+dirty1, value11, dirty2+label12, value12) + + promLbs1 = getPromLabels(label11, value11, label12, value12) + promLbs2 = getPromLabels(label21, value21, label22, value22) + + lb1Sig = "-" + label11 + "-" + value11 + "-" + label12 + "-" + value12 + lb2Sig = "-" + label21 + "-" + value21 + "-" + label22 + "-" + value22 + ns1 = "test_ns" + name1 = "valid_single_int_point" + + monotonicInt64Comb = 0 + histogramComb = 2 + summaryComb = 3 + validCombinations = []combination{ + {otlp.MetricDescriptor_MONOTONIC_INT64, otlp.MetricDescriptor_CUMULATIVE}, + {otlp.MetricDescriptor_MONOTONIC_DOUBLE, otlp.MetricDescriptor_CUMULATIVE}, + {otlp.MetricDescriptor_HISTOGRAM, otlp.MetricDescriptor_CUMULATIVE}, + {otlp.MetricDescriptor_SUMMARY, otlp.MetricDescriptor_CUMULATIVE}, + {otlp.MetricDescriptor_INT64, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_DOUBLE, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_INT64, otlp.MetricDescriptor_INSTANTANEOUS}, + {otlp.MetricDescriptor_DOUBLE, otlp.MetricDescriptor_INSTANTANEOUS}, + {otlp.MetricDescriptor_INT64, otlp.MetricDescriptor_CUMULATIVE}, + {otlp.MetricDescriptor_DOUBLE, otlp.MetricDescriptor_CUMULATIVE}, + } + invalidCombinations = []combination{ + {otlp.MetricDescriptor_MONOTONIC_INT64, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_MONOTONIC_DOUBLE, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_HISTOGRAM, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_SUMMARY, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_MONOTONIC_INT64, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_MONOTONIC_DOUBLE, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_HISTOGRAM, otlp.MetricDescriptor_DELTA}, + {otlp.MetricDescriptor_SUMMARY, otlp.MetricDescriptor_DELTA}, + {ty: otlp.MetricDescriptor_INVALID_TYPE}, + {temp: otlp.MetricDescriptor_INVALID_TEMPORALITY}, + {}, + } + twoPointsSameTs = map[string]*prompb.TimeSeries{ + typeInt64 + "-" + label11 + "-" + value11 + "-" + label12 + "-" + value12: getTimeSeries(getPromLabels(label11, value11, label12, value12), + getSample(float64(intVal1), msTime1), + getSample(float64(intVal2), msTime2)), + } + twoPointsDifferentTs = map[string]*prompb.TimeSeries{ + typeInt64 + "-" + label11 + "-" + value11 + "-" + label12 + "-" + value12: getTimeSeries(getPromLabels(label11, value11, label12, value12), + getSample(float64(intVal1), msTime1)), + typeInt64 + "-" + label21 + "-" + value21 + "-" + label22 + "-" + value22: getTimeSeries(getPromLabels(label21, value21, label22, value22), + getSample(float64(intVal1), msTime2)), + } +) + +// OTLP metrics +// labels must come in pairs +func getLabels(labels ...string) []*commonpb.StringKeyValue { + var set []*commonpb.StringKeyValue + for i := 0; i < len(labels); i += 2 { + set = append(set, &commonpb.StringKeyValue{ + Key: labels[i], + Value: labels[i+1], + }) + } + return set +} + +func getDescriptor(name string, i int, comb []combination) *otlp.MetricDescriptor { + return &otlp.MetricDescriptor{ + Name: name, + Description: "", + Unit: "", + Type: comb[i].ty, + Temporality: comb[i].temp, + } +} + +// Prometheus TimeSeries +func getPromLabels(lbs ...string) []prompb.Label { + pbLbs := prompb.Labels{ + Labels: []prompb.Label{}, + } + for i := 0; i < len(lbs); i += 2 { + pbLbs.Labels = append(pbLbs.Labels, getLabel(lbs[i], lbs[i+1])) + } + return pbLbs.Labels +} + +func getLabel(name string, value string) prompb.Label { + return prompb.Label{ + Name: name, + Value: value, + } +} + +func getSample(v float64, t int64) prompb.Sample { + return prompb.Sample{ + Value: v, + Timestamp: t, + } +} + +func getTimeSeries(labels []prompb.Label, samples ...prompb.Sample) *prompb.TimeSeries { + return &prompb.TimeSeries{ + Labels: labels, + Samples: samples, + } +}