Skip to content

Commit

Permalink
pkg/translator/prometheus: Allow not translating UTF8-characters
Browse files Browse the repository at this point in the history
Signed-off-by: Arthur Silva Sens <[email protected]>
  • Loading branch information
ArthurSens committed Oct 3, 2024
1 parent 2797fa0 commit e9f0df5
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 79 deletions.
27 changes: 27 additions & 0 deletions .chloggen/allowutf-prometheustranslator.yaml
Original file line number Diff line number Diff line change
@@ -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: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: pkg/translator/prometheus

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Allow UTF-8 characters in Prometheus metric names and labels.

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [35459]

# (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, api]
4 changes: 2 additions & 2 deletions exporter/googlemanagedprometheusexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ require (
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.20.4 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.59.1 // indirect
github.com/prometheus/common v0.60.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/shirou/gopsutil/v4 v4.24.8 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
Expand Down Expand Up @@ -135,7 +135,7 @@ require (
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions exporter/googlemanagedprometheusexporter/go.sum

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

4 changes: 4 additions & 0 deletions exporter/prometheusexporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ 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=pkg.translator.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
7 changes: 7 additions & 0 deletions exporter/prometheusexporter/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import (

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/model"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/pdata/pmetric"

prometheustranslator "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus"
)

type prometheusExporter struct {
Expand All @@ -30,6 +33,10 @@ type prometheusExporter struct {
var errBlankPrometheusAddress = errors.New("expecting a non-blank address to run the Prometheus metrics handler")

func newPrometheusExporter(config *Config, set exporter.Settings) (*prometheusExporter, error) {
if prometheustranslator.AllowUTF8FeatureGate.IsEnabled() {
model.NameValidationScheme = model.UTF8Validation
}

addr := strings.TrimSpace(config.Endpoint)
if strings.TrimSpace(config.Endpoint) == "" {
return nil, errBlankPrometheusAddress
Expand Down
2 changes: 2 additions & 0 deletions exporter/prometheusremotewriteexporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ To enable it run collector with enabled feature gate `exporter.prometheusremotew

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=pkg.translator.prometheus.allow_utf8`.

## 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
5 changes: 5 additions & 0 deletions exporter/prometheusremotewriteexporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/prompb"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/confighttp"
Expand Down Expand Up @@ -88,6 +89,10 @@ func newPRWTelemetry(set exporter.Settings) (prwTelemetry, error) {

// newPRWExporter initializes a new prwExporter instance and sets fields accordingly.
func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
if prometheustranslator.AllowUTF8FeatureGate.IsEnabled() {
model.NameValidationScheme = model.UTF8Validation
}

sanitizedLabels, err := validateAndSanitizeExternalLabels(cfg)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion exporter/prometheusremotewriteexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.110.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.110.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite v0.110.0
github.com/prometheus/common v0.60.0
github.com/prometheus/prometheus v0.54.1
github.com/stretchr/testify v1.9.0
github.com/tidwall/wal v1.1.7
Expand Down Expand Up @@ -55,7 +56,6 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/tidwall/gjson v1.10.2 // indirect
github.com/tidwall/match v1.1.1 // indirect
Expand Down
53 changes: 53 additions & 0 deletions pkg/translator/prometheus/feature_gates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

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

import (
"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. Attention: if 'pkg.translator.prometheus.allowUTF8' is enabled, UTF-8 characters will not be normalized."),
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/8950"),
)

// AllowUTF8FeatureGate could be a private variable, it is used mostly to toogle the
// value of a global variable and it's intended usage is:
/*
import "github.com/prometheus/common/model"
if AllowUTF8FeatureGate.IsEnabled() {
model.NameValidationScheme = model.UTF8Validation
}
*/
// We could do this with a init function in the translator package, but we can't guarantee that
// the featuregate.GlobalRegistry is initialized before the init function here.
//
// Components that want to respect this feature gate behavior should check if it is enabled and
// set the model.NameValidationScheme accordingly.
//
// WARNING: Since it's a global variable, if one component enables it then it works for all components
// that are using this package.
AllowUTF8FeatureGate = featuregate.GlobalRegistry().MustRegister(
"pkg.translator.prometheus.allowUTF8",
featuregate.StageAlpha,
featuregate.WithRegisterDescription("When enabled, UTF-8 characters will not be translated to underscores."),
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/35459"),
featuregate.WithRegisterFromVersion("v0.110.0"),
)
)

func init() {

}
6 changes: 3 additions & 3 deletions pkg/translator/prometheus/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.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
12 changes: 6 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.

15 changes: 4 additions & 11 deletions pkg/translator/prometheus/normalize_label.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ package prometheus // import "github.com/open-telemetry/opentelemetry-collector-
import (
"strings"
"unicode"

"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"),
)

// Normalizes the specified label to follow Prometheus label names standard
Expand All @@ -31,8 +22,10 @@ func NormalizeLabel(label string) string {
return label
}

// Replace all non-alphanumeric runes with underscores
label = strings.Map(sanitizeRune, label)
if !AllowUTF8FeatureGate.IsEnabled() {
// Replace all non-alphanumeric runes with underscores
label = strings.Map(sanitizeRune, label)
}

// If label starts with a number, prepend with "key_"
if unicode.IsDigit(rune(label[0])) {
Expand Down
83 changes: 63 additions & 20 deletions pkg/translator/prometheus/normalize_label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,68 @@ import (
"github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/testutil"
)

func TestSanitize(t *testing.T) {
func TestNormalizeLabel(t *testing.T) {
testCases := []struct {
label string
utfAllowed bool
dropSantization bool
expected string
}{
{
label: "",
expected: "",
},
{
label: "test",
expected: "test",
},
{
label: "__test",
expected: "__test",
},
{
label: "_test",
expected: "key_test",
},
{
label: "_test",
dropSantization: true,
expected: "_test",
},
{
label: "0test",
expected: "key_0test",
},
{
label: "0test",
dropSantization: true,
expected: "key_0test",
},
{
label: "test_/",
expected: "test__",
},
{
label: "test_/",
utfAllowed: true,
expected: "test_/",
},
{
label: "test.test",
expected: "test_test",
},
{
label: "test.test",
utfAllowed: true,
expected: "test.test",
},
}

defer testutil.SetFeatureGateForTest(t, dropSanitizationGate, false)()

require.Equal(t, "", NormalizeLabel(""), "")
require.Equal(t, "key_test", NormalizeLabel("_test"))
require.Equal(t, "key_0test", NormalizeLabel("0test"))
require.Equal(t, "test", NormalizeLabel("test"))
require.Equal(t, "test__", NormalizeLabel("test_/"))
require.Equal(t, "__test", NormalizeLabel("__test"))
}

func TestSanitizeDropSanitization(t *testing.T) {

defer testutil.SetFeatureGateForTest(t, dropSanitizationGate, true)()

require.Equal(t, "", NormalizeLabel(""))
require.Equal(t, "_test", NormalizeLabel("_test"))
require.Equal(t, "key_0test", NormalizeLabel("0test"))
require.Equal(t, "test", NormalizeLabel("test"))
require.Equal(t, "__test", NormalizeLabel("__test"))
for _, tc := range testCases {
t.Run(tc.label, func(t *testing.T) {
testutil.SetFeatureGateForTest(t, dropSanitizationGate, tc.dropSantization)
testutil.SetFeatureGateForTest(t, AllowUTF8FeatureGate, tc.utfAllowed)
require.Equal(t, tc.expected, NormalizeLabel(tc.label))
})
}
}
Loading

0 comments on commit e9f0df5

Please sign in to comment.