diff --git a/CHANGELOG.md b/CHANGELOG.md index 7094c5248cb..78cd642217d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Changed + +- Update `github.com/prometheus/common` to v0.62.0., which changes the `NameValidationScheme` to `NoEscaping`. This allows metrics names to keep original delimiters (e.g. `.`), rather than replacing with underscores. This is controlled by the `Content-Type` header, or can be reverted by setting `NameValidationScheme` to `LegacyValidation` in `github.com/prometheus/common/model` (#6198) + ### Added - Add `ValueFromAttribute` and `KeyValueFromAttribute` in `go.opentelemetry.io/otel/log`. (#6180) diff --git a/exporters/prometheus/config_test.go b/exporters/prometheus/config_test.go index d0432cabada..26b0bfbdf10 100644 --- a/exporters/prometheus/config_test.go +++ b/exporters/prometheus/config_test.go @@ -133,7 +133,7 @@ func TestNewConfig(t *testing.T) { }, wantConfig: config{ registerer: prometheus.DefaultRegisterer, - namespace: "test_", + namespace: "test/_", }, }, } diff --git a/exporters/prometheus/exporter.go b/exporters/prometheus/exporter.go index 50c95a16f7f..a8677e93a26 100644 --- a/exporters/prometheus/exporter.go +++ b/exporters/prometheus/exporter.go @@ -96,7 +96,7 @@ type collector struct { // prometheus counters MUST have a _total suffix by default: // https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/compatibility/prometheus_and_openmetrics.md -const counterSuffix = "_total" +const counterSuffix = "total" // New returns a Prometheus Exporter. func New(opts ...Option) (*Exporter, error) { @@ -368,38 +368,38 @@ func createScopeInfoMetric(scope instrumentation.Scope) (prometheus.Metric, erro var unitSuffixes = map[string]string{ // Time - "d": "_days", - "h": "_hours", - "min": "_minutes", - "s": "_seconds", - "ms": "_milliseconds", - "us": "_microseconds", - "ns": "_nanoseconds", + "d": "days", + "h": "hours", + "min": "minutes", + "s": "seconds", + "ms": "milliseconds", + "us": "microseconds", + "ns": "nanoseconds", // Bytes - "By": "_bytes", - "KiBy": "_kibibytes", - "MiBy": "_mebibytes", - "GiBy": "_gibibytes", - "TiBy": "_tibibytes", - "KBy": "_kilobytes", - "MBy": "_megabytes", - "GBy": "_gigabytes", - "TBy": "_terabytes", + "By": "bytes", + "KiBy": "kibibytes", + "MiBy": "mebibytes", + "GiBy": "gibibytes", + "TiBy": "tibibytes", + "KBy": "kilobytes", + "MBy": "megabytes", + "GBy": "gigabytes", + "TBy": "terabytes", // SI - "m": "_meters", - "V": "_volts", - "A": "_amperes", - "J": "_joules", - "W": "_watts", - "g": "_grams", + "m": "meters", + "V": "volts", + "A": "amperes", + "J": "joules", + "W": "watts", + "g": "grams", // Misc - "Cel": "_celsius", - "Hz": "_hertz", - "1": "_ratio", - "%": "_percent", + "Cel": "celsius", + "Hz": "hertz", + "1": "ratio", + "%": "percent", } // getName returns the sanitized name, prefixed with the namespace and suffixed with unit. @@ -414,19 +414,31 @@ func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string { // Remove the _total suffix here, as we will re-add the total suffix // later, and it needs to come after the unit suffix. name = strings.TrimSuffix(name, counterSuffix) + // If the last character is an underscore, or would be converted to an underscore, trim it from the name. + // an underscore will be added back in later. + if convertsToUnderscore(rune(name[len(name)-1])) { + name = name[:len(name)-1] + } } if c.namespace != "" { name = c.namespace + name } if suffix, ok := unitSuffixes[m.Unit]; ok && !c.withoutUnits && !strings.HasSuffix(name, suffix) { - name += suffix + name += "_" + suffix } if addCounterSuffix { - name += counterSuffix + name += "_" + counterSuffix } return name } +// convertsToUnderscore returns true if the character would be converted to an +// underscore when the escaping scheme is underscore escaping. This is meant to +// capture any character that should be considered a "delimiter". +func convertsToUnderscore(b rune) bool { + return !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == ':' || (b >= '0' && b <= '9')) +} + func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType { switch v := m.Data.(type) { case metricdata.Histogram[int64], metricdata.Histogram[float64]: diff --git a/exporters/prometheus/exporter_test.go b/exporters/prometheus/exporter_test.go index 8718a217289..f40da6f7f31 100644 --- a/exporters/prometheus/exporter_test.go +++ b/exporters/prometheus/exporter_test.go @@ -35,7 +35,7 @@ func TestPrometheusExporter(t *testing.T) { recordMetrics func(ctx context.Context, meter otelmetric.Meter) options []Option expectedFile string - enableUTF8 bool + disableUTF8 bool }{ { name: "counter", @@ -195,6 +195,7 @@ func TestPrometheusExporter(t *testing.T) { { name: "sanitized attributes to labels", expectedFile: "testdata/sanitized_labels.txt", + disableUTF8: true, options: []Option{WithoutUnits()}, recordMetrics: func(ctx context.Context, meter otelmetric.Meter) { opt := otelmetric.WithAttributes( @@ -404,7 +405,6 @@ func TestPrometheusExporter(t *testing.T) { { name: "counter utf-8", expectedFile: "testdata/counter_utf8.txt", - enableUTF8: true, recordMetrics: func(ctx context.Context, meter otelmetric.Meter) { opt := otelmetric.WithAttributes( attribute.Key("A.G").String("B"), @@ -471,11 +471,11 @@ func TestPrometheusExporter(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - if tc.enableUTF8 { - model.NameValidationScheme = model.UTF8Validation + if tc.disableUTF8 { + model.NameValidationScheme = model.LegacyValidation defer func() { // Reset to defaults - model.NameValidationScheme = model.LegacyValidation + model.NameValidationScheme = model.UTF8Validation }() } ctx := context.Background()