Skip to content

Commit

Permalink
wip: allow utf8
Browse files Browse the repository at this point in the history
Signed-off-by: Arthur Silva Sens <[email protected]>
  • Loading branch information
ArthurSens committed Sep 28, 2024
1 parent 507ec47 commit 16ce50a
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 108 deletions.
5 changes: 5 additions & 0 deletions exporter/prometheusexporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ Given the example, metrics will be available at `https://1.2.3.4:1234/metrics`.

OpenTelemetry metric names and attributes are normalized to be compliant with Prometheus naming rules. [Details on this normalization process are described in the Prometheus translator module](../../pkg/translator/prometheus/).

Prometheus 2.55.0 introduced support for UTF-8 characters behind the feature-flag `utf8-names`. Prometheus 3.0.0 and later accept UTF-8 by default. This means that name and attribute normalization is not required if you're using those versions.
To allow UTF-8 characters to be exposed without normalization, start the collector with the feature gate: `--feature-gates=exporter.prometheus.allow_utf8`.

The scraper must include `scaping=allow-utf-8` in the `Accept` header for UTF-8 characters to be exposed.

## Setting resource attributes as metric labels

By default, resource attributes are added to a special metric called `target_info`. To select and group by metrics by resource attributes, you [need to do join on `target_info`](https://prometheus.io/docs/prometheus/latest/querying/operators/#many-to-one-and-one-to-many-vector-matches). For example, to select metrics with `k8s_namespace_name` attribute equal to `my-namespace`:
Expand Down
9 changes: 6 additions & 3 deletions exporter/prometheusexporter/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type collector struct {
addMetricSuffixes bool
namespace string
constLabels prometheus.Labels

translator prometheustranslator.Translator
}

func newCollector(config *Config, logger *zap.Logger) *collector {
Expand All @@ -40,6 +42,7 @@ func newCollector(config *Config, logger *zap.Logger) *collector {
sendTimestamps: config.SendTimestamps,
constLabels: config.ConstLabels,
addMetricSuffixes: config.AddMetricSuffixes,
translator: prometheustranslator.NewTranslator(),
}
}

Expand Down Expand Up @@ -110,7 +113,7 @@ func (c *collector) getMetricMetadata(metric pmetric.Metric, attributes pcommon.
values := make([]string, 0, attributes.Len()+2)

attributes.Range(func(k string, v pcommon.Value) bool {
keys = append(keys, prometheustranslator.NormalizeLabel(k))
keys = append(keys, c.translator.TranslateAttribute(k))
values = append(values, v.AsString())
return true
})
Expand All @@ -125,7 +128,7 @@ func (c *collector) getMetricMetadata(metric pmetric.Metric, attributes pcommon.
}

return prometheus.NewDesc(
prometheustranslator.BuildCompliantName(metric, c.namespace, c.addMetricSuffixes),
c.translator.TranslateMetric(metric, c.namespace, c.addMetricSuffixes),
metric.Description(),
keys,
c.constLabels,
Expand Down Expand Up @@ -327,7 +330,7 @@ func (c *collector) createTargetInfoMetrics(resourceAttrs []pcommon.Map) ([]prom
})

attributes.Range(func(k string, v pcommon.Value) bool {
finalKey := prometheustranslator.NormalizeLabel(k)
finalKey := c.translator.TranslateAttribute(k)
if existingVal, ok := labels[finalKey]; ok {
labels[finalKey] = existingVal + ";" + v.AsString()
} else {
Expand Down
1 change: 1 addition & 0 deletions exporter/prometheusexporter/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func newPrometheusExporter(config *Config, set exporter.Settings) (*prometheusEx
collector := newCollector(config, set.Logger)
registry := prometheus.NewRegistry()
_ = registry.Register(collector)

return &prometheusExporter{
config: *config,
name: set.ID.String(),
Expand Down
39 changes: 39 additions & 0 deletions pkg/translator/prometheus/feature_gates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus"

import (
"github.com/prometheus/common/model"
"go.opentelemetry.io/collector/featuregate"
)

var (
dropSanitizationGate = featuregate.GlobalRegistry().MustRegister(
"pkg.translator.prometheus.PermissiveLabelSanitization",
featuregate.StageAlpha,
featuregate.WithRegisterDescription("Controls whether to change labels starting with '_' to 'key_'."),
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/8950"),
)

normalizeNameGate = featuregate.GlobalRegistry().MustRegister(
"pkg.translator.prometheus.NormalizeName",
featuregate.StageBeta,
featuregate.WithRegisterDescription("Controls whether metrics names are automatically normalized to follow Prometheus naming convention"),
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/8950"),
)

allowUTF8FeatureGate = featuregate.GlobalRegistry().MustRegister(
"pkg.translator.prometheus.allowUTF8",
featuregate.StageAlpha,
featuregate.WithRegisterDescription("When enabled, metric names and labels will not be normalized to the traditional Prometheus naming rules, fundamentally allowing UTF-8 characters."),
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/35459"),
featuregate.WithRegisterFromVersion("v0.110.0"),
)
)

func init() {
if allowUTF8FeatureGate.IsEnabled() {
model.NameValidationScheme = model.UTF8Validation
}
}
8 changes: 5 additions & 3 deletions pkg/translator/prometheus/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22.0

require (
github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.110.0
github.com/prometheus/common v0.59.1
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/collector/featuregate v1.16.0
go.opentelemetry.io/collector/pdata v1.16.0
Expand All @@ -19,10 +20,11 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
google.golang.org/grpc v1.66.2 // indirect
google.golang.org/protobuf v1.34.2 // indirect
Expand Down
16 changes: 10 additions & 6 deletions pkg/translator/prometheus/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 0 additions & 53 deletions pkg/translator/prometheus/normalize_label.go

This file was deleted.

35 changes: 0 additions & 35 deletions pkg/translator/prometheus/normalize_label_test.go

This file was deleted.

8 changes: 0 additions & 8 deletions pkg/translator/prometheus/normalize_name.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"strings"
"unicode"

"go.opentelemetry.io/collector/featuregate"
"go.opentelemetry.io/collector/pdata/pmetric"
)

Expand Down Expand Up @@ -65,13 +64,6 @@ var perUnitMap = map[string]string{
"y": "year",
}

var normalizeNameGate = featuregate.GlobalRegistry().MustRegister(
"pkg.translator.prometheus.NormalizeName",
featuregate.StageBeta,
featuregate.WithRegisterDescription("Controls whether metrics names are automatically normalized to follow Prometheus naming convention"),
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/8950"),
)

// BuildCompliantName builds a Prometheus-compliant metric name for the specified metric
//
// Metric name is prefixed with specified namespace and underscore (if any).
Expand Down
104 changes: 104 additions & 0 deletions pkg/translator/prometheus/translator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus"

import (
"strings"
"unicode"

"go.opentelemetry.io/collector/pdata/pmetric"
)

type Translator interface {
TranslateAttribute(string) string
TranslateMetric(metric pmetric.Metric, namespace string, addMetricSuffixes bool) string
}

type UTF8AllowedTranslator struct{}

func (UTF8AllowedTranslator) TranslateAttribute(attribute string) string {
return addKeyPrefixIfNeeded(attribute)
}

func (UTF8AllowedTranslator) TranslateMetric(metric pmetric.Metric, namespace string, addMetricSuffixes bool) string {
tokens := make([]string, 0)
if namespace != "" {
tokens = append(tokens, namespace)
}
tokens = append(tokens, metric.Name())

if addMetricSuffixes && normalizeNameGate.IsEnabled() {
// Split unit at the '/' if any
unitTokens := strings.SplitN(metric.Unit(), "/", 2)

if len(unitTokens) > 0 {
mainUnitOtel := strings.TrimSpace(unitTokens[0])
if mainUnitOtel != "" && !strings.ContainsAny(mainUnitOtel, "{}") {
tokens = append(tokens, unitMapGetOrDefault(mainUnitOtel))
}

// Per unit
// Append if not blank and doesn't contain '{}'
if len(unitTokens) > 1 && unitTokens[1] != "" {
perUnitOtel := strings.TrimSpace(unitTokens[1])
if perUnitOtel != "" && !strings.ContainsAny(perUnitOtel, "{}") {
tokens = append(tokens, perUnitMapGetOrDefault(perUnitOtel))
}
}

}
}

metricName := strings.Join(tokens, ".")

// Append _total for Counters
if normalizeNameGate.IsEnabled() && metric.Type() == pmetric.MetricTypeSum && metric.Sum().IsMonotonic() {
metricName += "_total"
}
return metricName
}

type ClassicTranslator struct{}

func (ClassicTranslator) TranslateAttribute(attribute string) string {
// Trivial case
if len(attribute) == 0 {
return attribute
}

// Replace all non-alphanumeric runes with underscores
attribute = strings.Map(sanitizeRune, attribute)
attribute = addKeyPrefixIfNeeded(attribute)

return attribute
}

// Return '_' for anything non-alphanumeric
func sanitizeRune(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return r
}
return '_'
}

func addKeyPrefixIfNeeded(attribute string) string {
// If label starts with a number, prepend with "key_"
if unicode.IsDigit(rune(attribute[0])) {
attribute = "key_" + attribute
} else if strings.HasPrefix(attribute, "_") && !strings.HasPrefix(attribute, "__") && !dropSanitizationGate.IsEnabled() {
attribute = "key" + attribute
}
return attribute
}

func (ClassicTranslator) TranslateMetric(metric pmetric.Metric, namespace string, addMetricSuffixes bool) string {
return BuildCompliantName(metric, namespace, addMetricSuffixes)
}

func NewTranslator() Translator {
if allowUTF8FeatureGate.IsEnabled() {
return UTF8AllowedTranslator{}
}
return ClassicTranslator{}
}
Loading

0 comments on commit 16ce50a

Please sign in to comment.