From 39c7b784047ba42236ab66d9d963f193a81ee47b Mon Sep 17 00:00:00 2001 From: Dominik Rosiek Date: Mon, 22 Apr 2024 12:01:54 +0200 Subject: [PATCH 1/3] feat: fix prometheus formatter Signed-off-by: Dominik Rosiek --- ...siek-sumologicexporter-prom-formatter.yaml | 27 ++ exporter/sumologicexporter/exporter.go | 5 +- exporter/sumologicexporter/exporter_test.go | 8 +- .../sumologicexporter/prometheus_formatter.go | 248 ++++++++++++------ .../prometheus_formatter_test.go | 214 +++++++++------ exporter/sumologicexporter/sender.go | 2 +- exporter/sumologicexporter/sender_test.go | 11 +- 7 files changed, 343 insertions(+), 172 deletions(-) create mode 100644 .chloggen/drosiek-sumologicexporter-prom-formatter.yaml diff --git a/.chloggen/drosiek-sumologicexporter-prom-formatter.yaml b/.chloggen/drosiek-sumologicexporter-prom-formatter.yaml new file mode 100644 index 000000000000..5d062614b2aa --- /dev/null +++ b/.chloggen/drosiek-sumologicexporter-prom-formatter.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: bug_fix + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: sumologicexporter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "do not replace `.` with `_` for prometheus format" + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/exporter/sumologicexporter/exporter.go b/exporter/sumologicexporter/exporter.go index c36bceb73431..a2e7209677da 100644 --- a/exporter/sumologicexporter/exporter.go +++ b/exporter/sumologicexporter/exporter.go @@ -90,7 +90,10 @@ func initExporter(cfg *Config, settings component.TelemetrySettings) (*sumologic return nil, err } - pf := newPrometheusFormatter() + pf, err := newPrometheusFormatter() + if err != nil { + return nil, err + } gf := newGraphiteFormatter(cfg.GraphiteTemplate) diff --git a/exporter/sumologicexporter/exporter_test.go b/exporter/sumologicexporter/exporter_test.go index cf54375fd6f3..77a2f28d9c7b 100644 --- a/exporter/sumologicexporter/exporter_test.go +++ b/exporter/sumologicexporter/exporter_test.go @@ -231,7 +231,7 @@ func TestAllMetricsSuccess(t *testing.T) { test := prepareSenderTest(t, []func(w http.ResponseWriter, req *http.Request){ func(_ http.ResponseWriter, req *http.Request) { body := extractBody(t, req) - expected := `test_metric_data{test="test_value",test2="second_value"} 14500 1605534165000 + expected := `test.metric.data{test="test_value",test2="second_value"} 14500 1605534165000 gauge_metric_name{foo="bar",remote_name="156920",url="http://example_url"} 124 1608124661166 gauge_metric_name{foo="bar",remote_name="156955",url="http://another_url"} 245 1608124662166` assert.Equal(t, expected, body) @@ -256,7 +256,7 @@ func TestAllMetricsFailed(t *testing.T) { w.WriteHeader(500) body := extractBody(t, req) - expected := `test_metric_data{test="test_value",test2="second_value"} 14500 1605534165000 + expected := `test.metric.data{test="test_value",test2="second_value"} 14500 1605534165000 gauge_metric_name{foo="bar",remote_name="156920",url="http://example_url"} 124 1608124661166 gauge_metric_name{foo="bar",remote_name="156955",url="http://another_url"} 245 1608124662166` assert.Equal(t, expected, body) @@ -285,7 +285,7 @@ func TestMetricsPartiallyFailed(t *testing.T) { w.WriteHeader(500) body := extractBody(t, req) - expected := `test_metric_data{test="test_value",test2="second_value"} 14500 1605534165000` + expected := `test.metric.data{test="test_value",test2="second_value"} 14500 1605534165000` assert.Equal(t, expected, body) assert.Equal(t, "application/vnd.sumologic.prometheus", req.Header.Get("Content-Type")) }, @@ -343,7 +343,7 @@ func TestMetricsDifferentMetadata(t *testing.T) { w.WriteHeader(500) body := extractBody(t, req) - expected := `test_metric_data{test="test_value",test2="second_value",key1="value1"} 14500 1605534165000` + expected := `test.metric.data{test="test_value",test2="second_value",key1="value1"} 14500 1605534165000` assert.Equal(t, expected, body) assert.Equal(t, "application/vnd.sumologic.prometheus", req.Header.Get("Content-Type")) }, diff --git a/exporter/sumologicexporter/prometheus_formatter.go b/exporter/sumologicexporter/prometheus_formatter.go index bde966fd0e2e..347cb223ba57 100644 --- a/exporter/sumologicexporter/prometheus_formatter.go +++ b/exporter/sumologicexporter/prometheus_formatter.go @@ -31,64 +31,129 @@ const ( prometheusInfValue string = "+Inf" ) -func newPrometheusFormatter() prometheusFormatter { - sanitNameRegex := regexp.MustCompile(`[^0-9a-zA-Z]`) +func newPrometheusFormatter() (prometheusFormatter, error) { + sanitNameRegex, err := regexp.Compile(`[^0-9a-zA-Z\./_:\-]`) + if err != nil { + return prometheusFormatter{}, err + } return prometheusFormatter{ sanitNameRegex: sanitNameRegex, - replacer: strings.NewReplacer(`\`, `\\`, `"`, `\"`), - } + // `\`, `"` and `\n` should be escaped, everything else should be left as-is + // see: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#line-format + replacer: strings.NewReplacer(`\`, `\\`, `"`, `\"`, "\n", `\n`), + }, nil } // PrometheusLabels returns all attributes as sanitized prometheus labels string func (f *prometheusFormatter) tags2String(attr pcommon.Map, labels pcommon.Map) prometheusTags { + attrsPlusLabelsLen := attr.Len() + labels.Len() + if attrsPlusLabelsLen == 0 { + return "" + } + mergedAttributes := pcommon.NewMap() + mergedAttributes.EnsureCapacity(attrsPlusLabelsLen) + attr.CopyTo(mergedAttributes) labels.Range(func(k string, v pcommon.Value) bool { - mergedAttributes.PutStr(k, v.Str()) + mergedAttributes.PutStr(k, v.AsString()) return true }) length := mergedAttributes.Len() - if length == 0 { - return "" - } - returnValue := make([]string, 0, length) mergedAttributes.Range(func(k string, v pcommon.Value) bool { + key := f.sanitizeKeyBytes([]byte(k)) + value := f.sanitizeValue(v.AsString()) + returnValue = append( returnValue, - fmt.Sprintf( - `%s="%s"`, - f.sanitizeKey(k), - f.sanitizeValue(v.AsString()), - ), + formatKeyValuePair(key, value), ) return true }) - return prometheusTags(fmt.Sprintf("{%s}", strings.Join(returnValue, ","))) + return prometheusTags(stringsJoinAndSurround(returnValue, ",", "{", "}")) +} + +func formatKeyValuePair(key []byte, value string) string { + const ( + quoteSign = `"` + equalSign = `=` + ) + + // Use strings.Builder and not fmt.Sprintf as it uses significantly less + // allocations. + sb := strings.Builder{} + // We preallocate space for key, value, equal sign and quotes. + sb.Grow(len(key) + len(equalSign) + 2*len(quoteSign) + len(value)) + sb.Write(key) + sb.WriteString(equalSign) + sb.WriteString(quoteSign) + sb.WriteString(value) + sb.WriteString(quoteSign) + return sb.String() } -// sanitizeKey returns sanitized key string by replacing -// all non-alphanumeric chars with `_` -func (f *prometheusFormatter) sanitizeKey(s string) string { - return f.sanitNameRegex.ReplaceAllString(s, "_") +// stringsJoinAndSurround joins the strings in s slice using the separator adds front +// to the front of the resulting string and back at the end. +// +// This has a benefit over using the strings.Join() of using just one strings.Buidler +// instance and hence using less allocations to produce the final string. +func stringsJoinAndSurround(s []string, separator, front, back string) string { + switch len(s) { + case 0: + return "" + case 1: + var b strings.Builder + b.Grow(len(s[0]) + len(front) + len(back)) + b.WriteString(front) + b.WriteString(s[0]) + b.WriteString(back) + return b.String() + } + + // Count the total strings summarized length for the preallocation. + n := len(front) + len(s[0]) + for i := 1; i < len(s); i++ { + n += len(separator) + len(s[i]) + } + n += len(back) + + var b strings.Builder + // We preallocate space for all the entires in the provided slice together with + // the separator as well as the surrounding characters. + b.Grow(n) + b.WriteString(front) + b.WriteString(s[0]) + for _, s := range s[1:] { + b.WriteString(separator) + b.WriteString(s) + } + b.WriteString(back) + return b.String() +} + +// sanitizeKeyBytes returns sanitized key byte slice by replacing +// all non-allowed chars with `_` +func (f *prometheusFormatter) sanitizeKeyBytes(s []byte) []byte { + return f.sanitNameRegex.ReplaceAll(s, []byte{'_'}) } // sanitizeKey returns sanitized value string performing the following substitutions: // `/` -> `//` // `"` -> `\"` -// `\n` -> `\n` +// "\n" -> `\n` func (f *prometheusFormatter) sanitizeValue(s string) string { - return strings.ReplaceAll(f.replacer.Replace(s), `\\n`, `\n`) + return f.replacer.Replace(s) } // doubleLine builds metric based on the given arguments where value is float64 func (f *prometheusFormatter) doubleLine(name string, attributes prometheusTags, value float64, timestamp pcommon.Timestamp) string { return fmt.Sprintf( "%s%s %g %d", - f.sanitizeKey(name), + f.sanitizeKeyBytes([]byte(name)), attributes, value, timestamp/pcommon.Timestamp(time.Millisecond), @@ -99,7 +164,7 @@ func (f *prometheusFormatter) doubleLine(name string, attributes prometheusTags, func (f *prometheusFormatter) intLine(name string, attributes prometheusTags, value int64, timestamp pcommon.Timestamp) string { return fmt.Sprintf( "%s%s %d %d", - f.sanitizeKey(name), + f.sanitizeKeyBytes([]byte(name)), attributes, value, timestamp/pcommon.Timestamp(time.Millisecond), @@ -110,7 +175,7 @@ func (f *prometheusFormatter) intLine(name string, attributes prometheusTags, va func (f *prometheusFormatter) uintLine(name string, attributes prometheusTags, value uint64, timestamp pcommon.Timestamp) string { return fmt.Sprintf( "%s%s %d %d", - f.sanitizeKey(name), + f.sanitizeKeyBytes([]byte(name)), attributes, value, timestamp/pcommon.Timestamp(time.Millisecond), @@ -168,17 +233,35 @@ func (f *prometheusFormatter) countMetric(name string) string { return fmt.Sprintf("%s_count", name) } +// bucketMetric returns _bucket suffixed metric name +func (f *prometheusFormatter) bucketMetric(name string) string { + return fmt.Sprintf("%s_bucket", name) +} + +// mergeAttributes gets two pcommon.Maps and returns new which contains values from both of them +func (f *prometheusFormatter) mergeAttributes(attributes pcommon.Map, additionalAttributes pcommon.Map) pcommon.Map { + mergedAttributes := pcommon.NewMap() + mergedAttributes.EnsureCapacity(attributes.Len() + additionalAttributes.Len()) + + attributes.CopyTo(mergedAttributes) + additionalAttributes.Range(func(k string, v pcommon.Value) bool { + v.CopyTo(mergedAttributes.PutEmpty(k)) + return true + }) + return mergedAttributes +} + // doubleGauge2Strings converts DoubleGauge record to a list of strings (one per dataPoint) -func (f *prometheusFormatter) gauge2Strings(record metricPair) []string { - dps := record.metric.Gauge().DataPoints() +func (f *prometheusFormatter) gauge2Strings(metric pmetric.Metric, attributes pcommon.Map) []string { + dps := metric.Gauge().DataPoints() lines := make([]string, 0, dps.Len()) for i := 0; i < dps.Len(); i++ { dp := dps.At(i) line := f.numberDataPointValueLine( - record.metric.Name(), + metric.Name(), dp, - record.attributes, + attributes, ) lines = append(lines, line) } @@ -187,16 +270,16 @@ func (f *prometheusFormatter) gauge2Strings(record metricPair) []string { } // doubleSum2Strings converts Sum record to a list of strings (one per dataPoint) -func (f *prometheusFormatter) sum2Strings(record metricPair) []string { - dps := record.metric.Sum().DataPoints() +func (f *prometheusFormatter) sum2Strings(metric pmetric.Metric, attributes pcommon.Map) []string { + dps := metric.Sum().DataPoints() lines := make([]string, 0, dps.Len()) for i := 0; i < dps.Len(); i++ { dp := dps.At(i) line := f.numberDataPointValueLine( - record.metric.Name(), + metric.Name(), dp, - record.attributes, + attributes, ) lines = append(lines, line) } @@ -206,40 +289,40 @@ func (f *prometheusFormatter) sum2Strings(record metricPair) []string { // summary2Strings converts Summary record to a list of strings // n+2 where n is number of quantiles and 2 stands for sum and count metrics per each data point -func (f *prometheusFormatter) summary2Strings(record metricPair) []string { - dps := record.metric.Summary().DataPoints() +func (f *prometheusFormatter) summary2Strings(metric pmetric.Metric, attributes pcommon.Map) []string { + dps := metric.Summary().DataPoints() var lines []string for i := 0; i < dps.Len(); i++ { dp := dps.At(i) qs := dp.QuantileValues() + additionalAttributes := pcommon.NewMap() for i := 0; i < qs.Len(); i++ { q := qs.At(i) - newAttr := pcommon.NewMap() - record.attributes.CopyTo(newAttr) - newAttr.PutDouble(prometheusQuantileTag, q.Quantile()) + additionalAttributes.PutDouble(prometheusQuantileTag, q.Quantile()) + line := f.doubleValueLine( - record.metric.Name(), + metric.Name(), q.Value(), dp, - newAttr, + f.mergeAttributes(attributes, additionalAttributes), ) lines = append(lines, line) } line := f.doubleValueLine( - f.sumMetric(record.metric.Name()), + f.sumMetric(metric.Name()), dp.Sum(), dp, - record.attributes, + attributes, ) lines = append(lines, line) line = f.uintValueLine( - f.countMetric(record.metric.Name()), + f.countMetric(metric.Name()), dp.Count(), dp, - record.attributes, + attributes, ) lines = append(lines, line) } @@ -248,59 +331,61 @@ func (f *prometheusFormatter) summary2Strings(record metricPair) []string { // histogram2Strings converts Histogram record to a list of strings, // (n+1) where n is number of bounds plus two for sum and count per each data point -func (f *prometheusFormatter) histogram2Strings(record metricPair) []string { - dps := record.metric.Histogram().DataPoints() +func (f *prometheusFormatter) histogram2Strings(metric pmetric.Metric, attributes pcommon.Map) []string { + dps := metric.Histogram().DataPoints() var lines []string for i := 0; i < dps.Len(); i++ { dp := dps.At(i) explicitBounds := dp.ExplicitBounds() - if explicitBounds.Len() == 0 { - continue - } var cumulative uint64 + additionalAttributes := pcommon.NewMap() + for i := 0; i < explicitBounds.Len(); i++ { + bound := explicitBounds.At(i) cumulative += dp.BucketCounts().At(i) - newAttr := pcommon.NewMap() - record.attributes.CopyTo(newAttr) - newAttr.PutDouble(prometheusLeTag, explicitBounds.At(i)) + additionalAttributes.PutDouble(prometheusLeTag, bound) line := f.uintValueLine( - record.metric.Name(), + f.bucketMetric(metric.Name()), cumulative, dp, - newAttr, + f.mergeAttributes(attributes, additionalAttributes), + ) + lines = append(lines, line) + } + var line string + + // according to the spec, it's valid to have no buckets at all + if dp.BucketCounts().Len() > 0 { + cumulative += dp.BucketCounts().At(explicitBounds.Len()) + additionalAttributes.PutStr(prometheusLeTag, prometheusInfValue) + line = f.uintValueLine( + f.bucketMetric(metric.Name()), + cumulative, + dp, + f.mergeAttributes(attributes, additionalAttributes), ) lines = append(lines, line) } - cumulative += dp.BucketCounts().At(explicitBounds.Len()) - newAttr := pcommon.NewMap() - record.attributes.CopyTo(newAttr) - newAttr.PutStr(prometheusLeTag, prometheusInfValue) - line := f.uintValueLine( - record.metric.Name(), - cumulative, - dp, - newAttr, - ) - lines = append(lines, line) - - line = f.doubleValueLine( - f.sumMetric(record.metric.Name()), - dp.Sum(), - dp, - record.attributes, - ) - lines = append(lines, line) + if dp.HasSum() { + line = f.doubleValueLine( + f.sumMetric(metric.Name()), + dp.Sum(), + dp, + attributes, + ) + lines = append(lines, line) + } line = f.uintValueLine( - f.countMetric(record.metric.Name()), + f.countMetric(metric.Name()), dp.Count(), dp, - record.attributes, + attributes, ) lines = append(lines, line) } @@ -309,19 +394,18 @@ func (f *prometheusFormatter) histogram2Strings(record metricPair) []string { } // metric2String returns stringified metricPair -func (f *prometheusFormatter) metric2String(record metricPair) string { +func (f *prometheusFormatter) metric2String(metric pmetric.Metric, attributes pcommon.Map) string { var lines []string - //exhaustive:enforce - switch record.metric.Type() { + + switch metric.Type() { case pmetric.MetricTypeGauge: - lines = f.gauge2Strings(record) + lines = f.gauge2Strings(metric, attributes) case pmetric.MetricTypeSum: - lines = f.sum2Strings(record) + lines = f.sum2Strings(metric, attributes) case pmetric.MetricTypeSummary: - lines = f.summary2Strings(record) + lines = f.summary2Strings(metric, attributes) case pmetric.MetricTypeHistogram: - lines = f.histogram2Strings(record) - case pmetric.MetricTypeExponentialHistogram: + lines = f.histogram2Strings(metric, attributes) } return strings.Join(lines, "\n") } diff --git a/exporter/sumologicexporter/prometheus_formatter_test.go b/exporter/sumologicexporter/prometheus_formatter_test.go index 98a44fc34c92..fec8566a9afa 100644 --- a/exporter/sumologicexporter/prometheus_formatter_test.go +++ b/exporter/sumologicexporter/prometheus_formatter_test.go @@ -1,33 +1,39 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package sumologicexporter +package sumologicexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/sumologicexporter" import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" ) func TestSanitizeKey(t *testing.T) { - f := newPrometheusFormatter() + f, err := newPrometheusFormatter() + require.NoError(t, err) - key := "&^*123-abc-ABC!?" - expected := "___123_abc_ABC__" - assert.Equal(t, expected, f.sanitizeKey(key)) + key := "&^*123-abc-ABC!./?_:\n\r" + expected := "___123-abc-ABC_./__:__" + assert.EqualValues(t, expected, f.sanitizeKeyBytes([]byte(key))) } func TestSanitizeValue(t *testing.T) { - f := newPrometheusFormatter() + f, err := newPrometheusFormatter() + require.NoError(t, err) - value := `&^*123-abc-ABC!?"\\n` - expected := `&^*123-abc-ABC!?\"\\\n` + // `\`, `"` and `\n` should be escaped, everything else should be left as-is + // see: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#line-format + value := `&^*123-abc-ABC!?./"\` + "\n\r" + expected := `&^*123-abc-ABC!?./\"\\\n` + "\r" assert.Equal(t, expected, f.sanitizeValue(value)) } func TestTags2StringNoLabels(t *testing.T) { - f := newPrometheusFormatter() + f, err := newPrometheusFormatter() + require.NoError(t, err) mp := exampleIntMetric() mp.attributes.Clear() @@ -35,86 +41,82 @@ func TestTags2StringNoLabels(t *testing.T) { } func TestTags2String(t *testing.T) { - f := newPrometheusFormatter() + f, err := newPrometheusFormatter() + require.NoError(t, err) mp := exampleIntMetric() + mp.attributes.PutInt("int", 200) + + labels := pcommon.NewMap() + labels.PutInt("l_int", 200) + labels.PutStr("l_str", "two") + assert.Equal( t, - prometheusTags(`{test="test_value",test2="second_value"}`), - f.tags2String(mp.attributes, pcommon.NewMap()), + prometheusTags(`{test="test_value",test2="second_value",int="200",l_int="200",l_str="two"}`), + f.tags2String(mp.attributes, labels), ) } func TestTags2StringNoAttributes(t *testing.T) { - f := newPrometheusFormatter() + f, err := newPrometheusFormatter() + require.NoError(t, err) + + mp := exampleIntMetric() + mp.attributes.Clear() assert.Equal(t, prometheusTags(""), f.tags2String(pcommon.NewMap(), pcommon.NewMap())) } -func TestPrometheusMetricTypeIntGauge(t *testing.T) { - f := newPrometheusFormatter() - metric := exampleIntGaugeMetric() +func TestPrometheusMetricDataTypeIntGauge(t *testing.T) { + f, err := newPrometheusFormatter() + require.NoError(t, err) + mp := exampleIntGaugeMetric() - result := f.metric2String(metric) + result := f.metric2String(mp.metric, mp.attributes) expected := `gauge_metric_name{foo="bar",remote_name="156920",url="http://example_url"} 124 1608124661166 gauge_metric_name{foo="bar",remote_name="156955",url="http://another_url"} 245 1608124662166` assert.Equal(t, expected, result) - - metric = buildExampleIntGaugeMetric(false) - result = f.metric2String(metric) - expected = "" - assert.Equal(t, expected, result) } -func TestPrometheusMetricTypeDoubleGauge(t *testing.T) { - f := newPrometheusFormatter() - metric := exampleDoubleGaugeMetric() +func TestPrometheusMetricDataTypeDoubleGauge(t *testing.T) { + f, err := newPrometheusFormatter() + require.NoError(t, err) + mp := exampleDoubleGaugeMetric() - result := f.metric2String(metric) + result := f.metric2String(mp.metric, mp.attributes) expected := `gauge_metric_name_double_test{foo="bar",local_name="156720",endpoint="http://example_url"} 33.4 1608124661169 gauge_metric_name_double_test{foo="bar",local_name="156155",endpoint="http://another_url"} 56.8 1608124662186` assert.Equal(t, expected, result) - - metric = buildExampleDoubleGaugeMetric(false) - result = f.metric2String(metric) - expected = "" - assert.Equal(t, expected, result) } -func TestPrometheusMetricTypeIntSum(t *testing.T) { - f := newPrometheusFormatter() - metric := exampleIntSumMetric() +func TestPrometheusMetricDataTypeIntSum(t *testing.T) { + f, err := newPrometheusFormatter() + require.NoError(t, err) + mp := exampleIntSumMetric() - result := f.metric2String(metric) + result := f.metric2String(mp.metric, mp.attributes) expected := `sum_metric_int_test{foo="bar",name="156720",address="http://example_url"} 45 1608124444169 sum_metric_int_test{foo="bar",name="156155",address="http://another_url"} 1238 1608124699186` assert.Equal(t, expected, result) - - metric = buildExampleIntSumMetric(false) - result = f.metric2String(metric) - expected = "" - assert.Equal(t, expected, result) } -func TestPrometheusMetricTypeDoubleSum(t *testing.T) { - f := newPrometheusFormatter() - metric := exampleDoubleSumMetric() +func TestPrometheusMetricDataTypeDoubleSum(t *testing.T) { + f, err := newPrometheusFormatter() + require.NoError(t, err) + mp := exampleDoubleSumMetric() - result := f.metric2String(metric) + result := f.metric2String(mp.metric, mp.attributes) expected := `sum_metric_double_test{foo="bar",pod_name="lorem",namespace="default"} 45.6 1618124444169 sum_metric_double_test{foo="bar",pod_name="opsum",namespace="kube-config"} 1238.1 1608424699186` assert.Equal(t, expected, result) - - metric = buildExampleDoubleSumMetric(false) - result = f.metric2String(metric) - expected = "" - assert.Equal(t, expected, result) } -func TestPrometheusMetricTypeSummary(t *testing.T) { - f := newPrometheusFormatter() - metric := exampleSummaryMetric() +func TestPrometheusMetricDataTypeSummary(t *testing.T) { + f, err := newPrometheusFormatter() + require.NoError(t, err) + mp := exampleSummaryMetric() - result := f.metric2String(metric) + result := f.metric2String(mp.metric, mp.attributes) expected := `summary_metric_double_test{foo="bar",quantile="0.6",pod_name="dolor",namespace="sumologic"} 0.7 1618124444169 summary_metric_double_test{foo="bar",quantile="2.6",pod_name="dolor",namespace="sumologic"} 4 1618124444169 summary_metric_double_test_sum{foo="bar",pod_name="dolor",namespace="sumologic"} 45.6 1618124444169 @@ -122,39 +124,93 @@ summary_metric_double_test_count{foo="bar",pod_name="dolor",namespace="sumologic summary_metric_double_test_sum{foo="bar",pod_name="sit",namespace="main"} 1238.1 1608424699186 summary_metric_double_test_count{foo="bar",pod_name="sit",namespace="main"} 7 1608424699186` assert.Equal(t, expected, result) - - metric = buildExampleSummaryMetric(false) - result = f.metric2String(metric) - expected = "" - assert.Equal(t, expected, result) } -func TestPrometheusMetricTypeHistogram(t *testing.T) { - f := newPrometheusFormatter() - metric := exampleHistogramMetric() - - result := f.metric2String(metric) - expected := `histogram_metric_double_test{bar="foo",le="0.1",container="dolor",branch="sumologic"} 0 1618124444169 -histogram_metric_double_test{bar="foo",le="0.2",container="dolor",branch="sumologic"} 12 1618124444169 -histogram_metric_double_test{bar="foo",le="0.5",container="dolor",branch="sumologic"} 19 1618124444169 -histogram_metric_double_test{bar="foo",le="0.8",container="dolor",branch="sumologic"} 24 1618124444169 -histogram_metric_double_test{bar="foo",le="1",container="dolor",branch="sumologic"} 32 1618124444169 -histogram_metric_double_test{bar="foo",le="+Inf",container="dolor",branch="sumologic"} 45 1618124444169 +func TestPrometheusMetricDataTypeHistogram(t *testing.T) { + f, err := newPrometheusFormatter() + require.NoError(t, err) + mp := exampleHistogramMetric() + + result := f.metric2String(mp.metric, mp.attributes) + expected := `histogram_metric_double_test_bucket{bar="foo",le="0.1",container="dolor",branch="sumologic"} 0 1618124444169 +histogram_metric_double_test_bucket{bar="foo",le="0.2",container="dolor",branch="sumologic"} 12 1618124444169 +histogram_metric_double_test_bucket{bar="foo",le="0.5",container="dolor",branch="sumologic"} 19 1618124444169 +histogram_metric_double_test_bucket{bar="foo",le="0.8",container="dolor",branch="sumologic"} 24 1618124444169 +histogram_metric_double_test_bucket{bar="foo",le="1",container="dolor",branch="sumologic"} 32 1618124444169 +histogram_metric_double_test_bucket{bar="foo",le="+Inf",container="dolor",branch="sumologic"} 45 1618124444169 histogram_metric_double_test_sum{bar="foo",container="dolor",branch="sumologic"} 45.6 1618124444169 histogram_metric_double_test_count{bar="foo",container="dolor",branch="sumologic"} 7 1618124444169 -histogram_metric_double_test{bar="foo",le="0.1",container="sit",branch="main"} 0 1608424699186 -histogram_metric_double_test{bar="foo",le="0.2",container="sit",branch="main"} 10 1608424699186 -histogram_metric_double_test{bar="foo",le="0.5",container="sit",branch="main"} 11 1608424699186 -histogram_metric_double_test{bar="foo",le="0.8",container="sit",branch="main"} 12 1608424699186 -histogram_metric_double_test{bar="foo",le="1",container="sit",branch="main"} 16 1608424699186 -histogram_metric_double_test{bar="foo",le="+Inf",container="sit",branch="main"} 22 1608424699186 +histogram_metric_double_test_bucket{bar="foo",le="0.1",container="sit",branch="main"} 0 1608424699186 +histogram_metric_double_test_bucket{bar="foo",le="0.2",container="sit",branch="main"} 10 1608424699186 +histogram_metric_double_test_bucket{bar="foo",le="0.5",container="sit",branch="main"} 11 1608424699186 +histogram_metric_double_test_bucket{bar="foo",le="0.8",container="sit",branch="main"} 12 1608424699186 +histogram_metric_double_test_bucket{bar="foo",le="1",container="sit",branch="main"} 16 1608424699186 +histogram_metric_double_test_bucket{bar="foo",le="+Inf",container="sit",branch="main"} 22 1608424699186 histogram_metric_double_test_sum{bar="foo",container="sit",branch="main"} 54.1 1608424699186 histogram_metric_double_test_count{bar="foo",container="sit",branch="main"} 98 1608424699186` assert.Equal(t, expected, result) +} - metric = buildExampleHistogramMetric(false) - result = f.metric2String(metric) - expected = "" +func TestEmptyPrometheusMetrics(t *testing.T) { + type testCase struct { + name string + metricFunc func(fillData bool) metricPair + expected string + } + + tests := []testCase{ + { + name: "empty int gauge", + metricFunc: buildExampleIntGaugeMetric, + expected: "", + }, + { + name: "empty double gauge", + metricFunc: buildExampleDoubleGaugeMetric, + expected: "", + }, + { + name: "empty int sum", + metricFunc: buildExampleIntSumMetric, + expected: "", + }, + { + name: "empty double sum", + metricFunc: buildExampleDoubleSumMetric, + expected: "", + }, + { + name: "empty summary", + metricFunc: buildExampleSummaryMetric, + expected: "", + }, + { + name: "histogram with one datapoint, no sum or buckets", + metricFunc: buildExampleHistogramMetric, + expected: `histogram_metric_double_test_count{bar="foo"} 0 0`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := newPrometheusFormatter() + require.NoError(t, err) + + mp := tt.metricFunc(false) + result := f.metric2String(mp.metric, mp.attributes) + assert.Equal(t, tt.expected, result) + }) + } +} - assert.Equal(t, expected, result) +func Benchmark_PrometheusFormatter_Metric2String(b *testing.B) { + f, err := newPrometheusFormatter() + require.NoError(b, err) + + mp := buildExampleHistogramMetric(true) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = f.metric2String(mp.metric, mp.attributes) + } } diff --git a/exporter/sumologicexporter/sender.go b/exporter/sumologicexporter/sender.go index 3a7c3de61211..ccd2a8625785 100644 --- a/exporter/sumologicexporter/sender.go +++ b/exporter/sumologicexporter/sender.go @@ -272,7 +272,7 @@ func (s *sender) sendMetrics(ctx context.Context, flds fields) ([]metricPair, er switch s.config.MetricFormat { case PrometheusFormat: - formattedLine = s.prometheusFormatter.metric2String(record) + formattedLine = s.prometheusFormatter.metric2String(record.metric, record.attributes) case Carbon2Format: formattedLine = carbon2Metric2String(record) case GraphiteFormat: diff --git a/exporter/sumologicexporter/sender_test.go b/exporter/sumologicexporter/sender_test.go index c8b2f532cf03..8a8c4a5d68ab 100644 --- a/exporter/sumologicexporter/sender_test.go +++ b/exporter/sumologicexporter/sender_test.go @@ -60,7 +60,8 @@ func prepareSenderTest(t *testing.T, cb []func(w http.ResponseWriter, req *http. c, err := newCompressor(NoCompression) require.NoError(t, err) - pf := newPrometheusFormatter() + pf, err := newPrometheusFormatter() + require.NoError(t, err) gf := newGraphiteFormatter(DefaultGraphiteTemplate) @@ -607,7 +608,7 @@ func TestSendMetrics(t *testing.T) { test := prepareSenderTest(t, []func(w http.ResponseWriter, req *http.Request){ func(_ http.ResponseWriter, req *http.Request) { body := extractBody(t, req) - expected := `test_metric_data{test="test_value",test2="second_value"} 14500 1605534165000 + expected := `test.metric.data{test="test_value",test2="second_value"} 14500 1605534165000 gauge_metric_name{foo="bar",remote_name="156920",url="http://example_url"} 124 1608124661166 gauge_metric_name{foo="bar",remote_name="156955",url="http://another_url"} 245 1608124662166` assert.Equal(t, expected, body) @@ -634,7 +635,7 @@ func TestSendMetricsSplit(t *testing.T) { test := prepareSenderTest(t, []func(w http.ResponseWriter, req *http.Request){ func(_ http.ResponseWriter, req *http.Request) { body := extractBody(t, req) - expected := `test_metric_data{test="test_value",test2="second_value"} 14500 1605534165000` + expected := `test.metric.data{test="test_value",test2="second_value"} 14500 1605534165000` assert.Equal(t, expected, body) }, func(_ http.ResponseWriter, req *http.Request) { @@ -662,7 +663,7 @@ func TestSendMetricsSplitFailedOne(t *testing.T) { w.WriteHeader(500) body := extractBody(t, req) - expected := `test_metric_data{test="test_value",test2="second_value"} 14500 1605534165000` + expected := `test.metric.data{test="test_value",test2="second_value"} 14500 1605534165000` assert.Equal(t, expected, body) }, func(_ http.ResponseWriter, req *http.Request) { @@ -691,7 +692,7 @@ func TestSendMetricsSplitFailedAll(t *testing.T) { w.WriteHeader(500) body := extractBody(t, req) - expected := `test_metric_data{test="test_value",test2="second_value"} 14500 1605534165000` + expected := `test.metric.data{test="test_value",test2="second_value"} 14500 1605534165000` assert.Equal(t, expected, body) }, func(w http.ResponseWriter, req *http.Request) { From 4f9c3cbf0d70f7e6f7f2206cf8de484fd4d63574 Mon Sep 17 00:00:00 2001 From: Dominik Rosiek Date: Mon, 22 Apr 2024 12:14:36 +0200 Subject: [PATCH 2/3] chore: make lint fixes Signed-off-by: Dominik Rosiek --- exporter/sumologicexporter/exporter.go | 5 +-- .../sumologicexporter/prometheus_formatter.go | 9 ++--- .../prometheus_formatter_test.go | 40 ++++++------------- exporter/sumologicexporter/sender_test.go | 3 +- 4 files changed, 18 insertions(+), 39 deletions(-) diff --git a/exporter/sumologicexporter/exporter.go b/exporter/sumologicexporter/exporter.go index a2e7209677da..c36bceb73431 100644 --- a/exporter/sumologicexporter/exporter.go +++ b/exporter/sumologicexporter/exporter.go @@ -90,10 +90,7 @@ func initExporter(cfg *Config, settings component.TelemetrySettings) (*sumologic return nil, err } - pf, err := newPrometheusFormatter() - if err != nil { - return nil, err - } + pf := newPrometheusFormatter() gf := newGraphiteFormatter(cfg.GraphiteTemplate) diff --git a/exporter/sumologicexporter/prometheus_formatter.go b/exporter/sumologicexporter/prometheus_formatter.go index 347cb223ba57..bf779503576d 100644 --- a/exporter/sumologicexporter/prometheus_formatter.go +++ b/exporter/sumologicexporter/prometheus_formatter.go @@ -31,18 +31,15 @@ const ( prometheusInfValue string = "+Inf" ) -func newPrometheusFormatter() (prometheusFormatter, error) { - sanitNameRegex, err := regexp.Compile(`[^0-9a-zA-Z\./_:\-]`) - if err != nil { - return prometheusFormatter{}, err - } +func newPrometheusFormatter() prometheusFormatter { + sanitNameRegex := regexp.MustCompile(`[^0-9a-zA-Z\./_:\-]`) return prometheusFormatter{ sanitNameRegex: sanitNameRegex, // `\`, `"` and `\n` should be escaped, everything else should be left as-is // see: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#line-format replacer: strings.NewReplacer(`\`, `\\`, `"`, `\"`, "\n", `\n`), - }, nil + } } // PrometheusLabels returns all attributes as sanitized prometheus labels string diff --git a/exporter/sumologicexporter/prometheus_formatter_test.go b/exporter/sumologicexporter/prometheus_formatter_test.go index fec8566a9afa..3a01bb711355 100644 --- a/exporter/sumologicexporter/prometheus_formatter_test.go +++ b/exporter/sumologicexporter/prometheus_formatter_test.go @@ -7,13 +7,11 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" ) func TestSanitizeKey(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() key := "&^*123-abc-ABC!./?_:\n\r" expected := "___123-abc-ABC_./__:__" @@ -21,8 +19,7 @@ func TestSanitizeKey(t *testing.T) { } func TestSanitizeValue(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() // `\`, `"` and `\n` should be escaped, everything else should be left as-is // see: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#line-format @@ -32,8 +29,7 @@ func TestSanitizeValue(t *testing.T) { } func TestTags2StringNoLabels(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleIntMetric() mp.attributes.Clear() @@ -41,8 +37,7 @@ func TestTags2StringNoLabels(t *testing.T) { } func TestTags2String(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleIntMetric() mp.attributes.PutInt("int", 200) @@ -59,8 +54,7 @@ func TestTags2String(t *testing.T) { } func TestTags2StringNoAttributes(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleIntMetric() mp.attributes.Clear() @@ -68,8 +62,7 @@ func TestTags2StringNoAttributes(t *testing.T) { } func TestPrometheusMetricDataTypeIntGauge(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleIntGaugeMetric() result := f.metric2String(mp.metric, mp.attributes) @@ -79,8 +72,7 @@ gauge_metric_name{foo="bar",remote_name="156955",url="http://another_url"} 245 1 } func TestPrometheusMetricDataTypeDoubleGauge(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleDoubleGaugeMetric() result := f.metric2String(mp.metric, mp.attributes) @@ -90,8 +82,7 @@ gauge_metric_name_double_test{foo="bar",local_name="156155",endpoint="http://ano } func TestPrometheusMetricDataTypeIntSum(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleIntSumMetric() result := f.metric2String(mp.metric, mp.attributes) @@ -101,8 +92,7 @@ sum_metric_int_test{foo="bar",name="156155",address="http://another_url"} 1238 1 } func TestPrometheusMetricDataTypeDoubleSum(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleDoubleSumMetric() result := f.metric2String(mp.metric, mp.attributes) @@ -112,8 +102,7 @@ sum_metric_double_test{foo="bar",pod_name="opsum",namespace="kube-config"} 1238. } func TestPrometheusMetricDataTypeSummary(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleSummaryMetric() result := f.metric2String(mp.metric, mp.attributes) @@ -127,8 +116,7 @@ summary_metric_double_test_count{foo="bar",pod_name="sit",namespace="main"} 7 16 } func TestPrometheusMetricDataTypeHistogram(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := exampleHistogramMetric() result := f.metric2String(mp.metric, mp.attributes) @@ -193,8 +181,7 @@ func TestEmptyPrometheusMetrics(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f, err := newPrometheusFormatter() - require.NoError(t, err) + f := newPrometheusFormatter() mp := tt.metricFunc(false) result := f.metric2String(mp.metric, mp.attributes) @@ -204,8 +191,7 @@ func TestEmptyPrometheusMetrics(t *testing.T) { } func Benchmark_PrometheusFormatter_Metric2String(b *testing.B) { - f, err := newPrometheusFormatter() - require.NoError(b, err) + f := newPrometheusFormatter() mp := buildExampleHistogramMetric(true) diff --git a/exporter/sumologicexporter/sender_test.go b/exporter/sumologicexporter/sender_test.go index 8a8c4a5d68ab..c278659cb5be 100644 --- a/exporter/sumologicexporter/sender_test.go +++ b/exporter/sumologicexporter/sender_test.go @@ -60,8 +60,7 @@ func prepareSenderTest(t *testing.T, cb []func(w http.ResponseWriter, req *http. c, err := newCompressor(NoCompression) require.NoError(t, err) - pf, err := newPrometheusFormatter() - require.NoError(t, err) + pf := newPrometheusFormatter() gf := newGraphiteFormatter(DefaultGraphiteTemplate) From 6210b8d8b240842f7c57aa20d5a12c6e6dc60a3c Mon Sep 17 00:00:00 2001 From: Dominik Rosiek <58699848+sumo-drosiek@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:35:30 +0200 Subject: [PATCH 3/3] Update .chloggen/drosiek-sumologicexporter-prom-formatter.yaml --- .chloggen/drosiek-sumologicexporter-prom-formatter.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chloggen/drosiek-sumologicexporter-prom-formatter.yaml b/.chloggen/drosiek-sumologicexporter-prom-formatter.yaml index 5d062614b2aa..02d01f47898d 100644 --- a/.chloggen/drosiek-sumologicexporter-prom-formatter.yaml +++ b/.chloggen/drosiek-sumologicexporter-prom-formatter.yaml @@ -10,7 +10,7 @@ component: sumologicexporter note: "do not replace `.` with `_` for prometheus format" # Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. -issues: [] +issues: [31479] # (Optional) One or more lines of additional information to render under the primary note. # These lines will be padded with 2 spaces and then inserted directly into the document.