[chore] bump dependencies (#4406)

- codeberg.org/gruf/go-ffmpreg: v0.6.9 -> v0.6.10
- github.com/ncruces/go-sqlite3: v0.27.1 -> v0.28.0
- github.com/stretchr/testify: v1.10.0 -> v1.11.1
- github.com/tdewolff/minify/v2 v2.23.11 -> v2.24.2
- go.opentelemetry.io/otel{,/*}: v1.37.0 -> v1.38.0
- go.opentelemetry.io/contrib/*: v0.62.0 -> v0.63.0

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4406
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-09-04 15:29:27 +02:00 committed by kim
commit 78defcd916
274 changed files with 9213 additions and 2368 deletions

View file

@ -453,7 +453,7 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
}
group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
}
if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') {
if len(group) > 0 && (len(group) != 1 || group[0].Tag != 'e') {
groups = append(groups, group)
}
return groups
@ -568,7 +568,7 @@ func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error {
buf := bufio.NewWriter(writer)
defer buf.Flush()
wf := func(format string, args ...interface{}) error {
_, err := buf.WriteString(fmt.Sprintf(format, args...))
_, err := fmt.Fprintf(buf, format, args...)
return err
}
ws := func(s string) error {

View file

@ -186,21 +186,31 @@ func (m *withExemplarsMetric) Write(pb *dto.Metric) error {
case pb.Counter != nil:
pb.Counter.Exemplar = m.exemplars[len(m.exemplars)-1]
case pb.Histogram != nil:
h := pb.Histogram
for _, e := range m.exemplars {
// pb.Histogram.Bucket are sorted by UpperBound.
i := sort.Search(len(pb.Histogram.Bucket), func(i int) bool {
return pb.Histogram.Bucket[i].GetUpperBound() >= e.GetValue()
if (h.GetZeroThreshold() != 0 || h.GetZeroCount() != 0 ||
len(h.PositiveSpan) != 0 || len(h.NegativeSpan) != 0) &&
e.GetTimestamp() != nil {
h.Exemplars = append(h.Exemplars, e)
if len(h.Bucket) == 0 {
// Don't proceed to classic buckets if there are none.
continue
}
}
// h.Bucket are sorted by UpperBound.
i := sort.Search(len(h.Bucket), func(i int) bool {
return h.Bucket[i].GetUpperBound() >= e.GetValue()
})
if i < len(pb.Histogram.Bucket) {
pb.Histogram.Bucket[i].Exemplar = e
if i < len(h.Bucket) {
h.Bucket[i].Exemplar = e
} else {
// The +Inf bucket should be explicitly added if there is an exemplar for it, similar to non-const histogram logic in https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L357-L365.
b := &dto.Bucket{
CumulativeCount: proto.Uint64(pb.Histogram.GetSampleCount()),
CumulativeCount: proto.Uint64(h.GetSampleCount()),
UpperBound: proto.Float64(math.Inf(1)),
Exemplar: e,
}
pb.Histogram.Bucket = append(pb.Histogram.Bucket, b)
h.Bucket = append(h.Bucket, b)
}
}
default:
@ -227,6 +237,7 @@ type Exemplar struct {
// Only last applicable exemplar is injected from the list.
// For example for Counter it means last exemplar is injected.
// For Histogram, it means last applicable exemplar for each bucket is injected.
// For a Native Histogram, all valid exemplars are injected.
//
// NewMetricWithExemplars works best with MustNewConstMetric and
// MustNewConstHistogram, see example.

View file

@ -25,9 +25,9 @@ import (
"golang.org/x/sys/unix"
)
// notImplementedErr is returned by stub functions that replace cgo functions, when cgo
// errNotImplemented is returned by stub functions that replace cgo functions, when cgo
// isn't available.
var notImplementedErr = errors.New("not implemented")
var errNotImplemented = errors.New("not implemented")
type memoryInfo struct {
vsize uint64 // Virtual memory size in bytes
@ -101,7 +101,7 @@ func (c *processCollector) processCollect(ch chan<- Metric) {
if memInfo, err := getMemory(); err == nil {
ch <- MustNewConstMetric(c.rss, GaugeValue, float64(memInfo.rss))
ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(memInfo.vsize))
} else if !errors.Is(err, notImplementedErr) {
} else if !errors.Is(err, errNotImplemented) {
// Don't report an error when support is not compiled in.
c.reportError(ch, c.rss, err)
c.reportError(ch, c.vsize, err)

View file

@ -16,7 +16,7 @@
package prometheus
func getMemory() (*memoryInfo, error) {
return nil, notImplementedErr
return nil, errNotImplemented
}
// describe returns all descriptions of the collector for Darwin.

View file

@ -66,11 +66,11 @@ func (c *processCollector) processCollect(ch chan<- Metric) {
if netstat, err := p.Netstat(); err == nil {
var inOctets, outOctets float64
if netstat.IpExt.InOctets != nil {
inOctets = *netstat.IpExt.InOctets
if netstat.InOctets != nil {
inOctets = *netstat.InOctets
}
if netstat.IpExt.OutOctets != nil {
outOctets = *netstat.IpExt.OutOctets
if netstat.OutOctets != nil {
outOctets = *netstat.OutOctets
}
ch <- MustNewConstMetric(c.inBytes, CounterValue, inOctets)
ch <- MustNewConstMetric(c.outBytes, CounterValue, outOctets)

View file

@ -392,7 +392,7 @@ func isLabelCurried(c prometheus.Collector, label string) bool {
func labels(code, method bool, reqMethod string, status int, extraMethods ...string) prometheus.Labels {
labels := prometheus.Labels{}
if !(code || method) {
if !code && !method {
return labels
}

View file

@ -79,7 +79,7 @@ func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
return false
}
return m.metricMap.deleteByHashWithLabelValues(h, lvs, m.curry)
return m.deleteByHashWithLabelValues(h, lvs, m.curry)
}
// Delete deletes the metric where the variable labels are the same as those
@ -101,7 +101,7 @@ func (m *MetricVec) Delete(labels Labels) bool {
return false
}
return m.metricMap.deleteByHashWithLabels(h, labels, m.curry)
return m.deleteByHashWithLabels(h, labels, m.curry)
}
// DeletePartialMatch deletes all metrics where the variable labels contain all of those
@ -114,7 +114,7 @@ func (m *MetricVec) DeletePartialMatch(labels Labels) int {
labels, closer := constrainLabels(m.desc, labels)
defer closer()
return m.metricMap.deleteByLabels(labels, m.curry)
return m.deleteByLabels(labels, m.curry)
}
// Without explicit forwarding of Describe, Collect, Reset, those methods won't
@ -216,7 +216,7 @@ func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
return nil, err
}
return m.metricMap.getOrCreateMetricWithLabelValues(h, lvs, m.curry), nil
return m.getOrCreateMetricWithLabelValues(h, lvs, m.curry), nil
}
// GetMetricWith returns the Metric for the given Labels map (the label names
@ -244,7 +244,7 @@ func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) {
return nil, err
}
return m.metricMap.getOrCreateMetricWithLabels(h, labels, m.curry), nil
return m.getOrCreateMetricWithLabels(h, labels, m.curry), nil
}
func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) {

View file

@ -63,7 +63,7 @@ func WrapRegistererWith(labels Labels, reg Registerer) Registerer {
// metric names that are standardized across applications, as that would break
// horizontal monitoring, for example the metrics provided by the Go collector
// (see NewGoCollector) and the process collector (see NewProcessCollector). (In
// fact, those metrics are already prefixed with “go_” or “process_”,
// fact, those metrics are already prefixed with "go_" or "process_",
// respectively.)
//
// Conflicts between Collectors registered through the original Registerer with
@ -78,6 +78,40 @@ func WrapRegistererWithPrefix(prefix string, reg Registerer) Registerer {
}
}
// WrapCollectorWith returns a Collector wrapping the provided Collector. The
// wrapped Collector will add the provided Labels to all Metrics it collects (as
// ConstLabels). The Metrics collected by the unmodified Collector must not
// duplicate any of those labels.
//
// WrapCollectorWith can be useful to work with multiple instances of a third
// party library that does not expose enough flexibility on the lifecycle of its
// registered metrics.
// For example, let's say you have a foo.New(reg Registerer) constructor that
// registers metrics but never unregisters them, and you want to create multiple
// instances of foo.Foo with different labels.
// The way to achieve that, is to create a new Registry, pass it to foo.New,
// then use WrapCollectorWith to wrap that Registry with the desired labels and
// register that as a collector in your main Registry.
// Then you can un-register the wrapped collector effectively un-registering the
// metrics registered by foo.New.
func WrapCollectorWith(labels Labels, c Collector) Collector {
return &wrappingCollector{
wrappedCollector: c,
labels: labels,
}
}
// WrapCollectorWithPrefix returns a Collector wrapping the provided Collector. The
// wrapped Collector will add the provided prefix to the name of all Metrics it collects.
//
// See the documentation of WrapCollectorWith for more details on the use case.
func WrapCollectorWithPrefix(prefix string, c Collector) Collector {
return &wrappingCollector{
wrappedCollector: c,
prefix: prefix,
}
}
type wrappingRegisterer struct {
wrappedRegisterer Registerer
prefix string

View file

@ -1,2 +1,120 @@
# otlp-prometheus-translator
Library providing API to convert OTLP metric and attribute names to respectively Prometheus metric and label names.
# OTLP Prometheus Translator
A Go library for converting [OpenTelemetry Protocol (OTLP)](https://opentelemetry.io/docs/specs/otlp/) metric and attribute names to [Prometheus](https://prometheus.io/)-compliant formats.
Part of the [Prometheus](https://prometheus.io/) ecosystem, following the [OpenTelemetry to Prometheus compatibility specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md).
## Features
- **Metric Name and Label Translation**: Convert OTLP metric names and attributes to Prometheus-compliant format
- **Unit Handling**: Translate OTLP units to Prometheus unit conventions
- **Type-Aware Suffixes**: Optionally append `_total`, `_ratio` based on metric type
- **Namespace Support**: Add configurable namespace prefixes
- **UTF-8 Support**: Choose between Prometheus legacy scheme compliant metric/label names (`[a-zA-Z0-9:_]`) or untranslated metric/label names
- **Translation Strategy Configuration**: Select a translation strategy with a standard set of strings.
## Installation
```bash
go get github.com/prometheus/otlptranslator
```
## Quick Start
```go
package main
import (
"fmt"
"github.com/prometheus/otlptranslator"
)
func main() {
// Create a metric namer using traditional Prometheus name translation, with suffixes added and UTF-8 disallowed.
strategy := otlptranslator.UnderscoreEscapingWithSuffixes
namer := otlptranslator.NewMetricNamer("myapp", strategy)
// Translate OTLP metric to Prometheus format
metric := otlptranslator.Metric{
Name: "http.server.request.duration",
Unit: "s",
Type: otlptranslator.MetricTypeHistogram,
}
fmt.Println(namer.Build(metric)) // Output: myapp_http_server_request_duration_seconds
// Translate label names
labelNamer := otlptranslator.LabelNamer{UTF8Allowed: false}
fmt.Println(labelNamer.Build("http.method")) // Output: http_method
}
```
## Usage Examples
### Metric Name Translation
```go
namer := otlptranslator.MetricNamer{WithMetricSuffixes: true, UTF8Allowed: false}
// Counter gets _total suffix
counter := otlptranslator.Metric{
Name: "requests.count", Unit: "1", Type: otlptranslator.MetricTypeMonotonicCounter,
}
fmt.Println(namer.Build(counter)) // requests_count_total
// Gauge with unit conversion
gauge := otlptranslator.Metric{
Name: "memory.usage", Unit: "By", Type: otlptranslator.MetricTypeGauge,
}
fmt.Println(namer.Build(gauge)) // memory_usage_bytes
// Dimensionless gauge gets _ratio suffix
ratio := otlptranslator.Metric{
Name: "cpu.utilization", Unit: "1", Type: otlptranslator.MetricTypeGauge,
}
fmt.Println(namer.Build(ratio)) // cpu_utilization_ratio
```
### Label Translation
```go
labelNamer := otlptranslator.LabelNamer{UTF8Allowed: false}
labelNamer.Build("http.method") // http_method
labelNamer.Build("123invalid") // key_123invalid
labelNamer.Build("_private") // key_private
labelNamer.Build("__reserved__") // __reserved__ (preserved)
labelNamer.Build("label@with$symbols") // label_with_symbols
```
### Unit Translation
```go
unitNamer := otlptranslator.UnitNamer{UTF8Allowed: false}
unitNamer.Build("s") // seconds
unitNamer.Build("By") // bytes
unitNamer.Build("requests/s") // requests_per_second
unitNamer.Build("1") // "" (dimensionless)
```
### Configuration Options
```go
// Prometheus-compliant mode - supports [a-zA-Z0-9:_]
compliantNamer := otlptranslator.MetricNamer{UTF8Allowed: false, WithMetricSuffixes: true}
// Transparent pass-through mode, aka "NoTranslation"
utf8Namer := otlptranslator.MetricNamer{UTF8Allowed: true, WithMetricSuffixes: false}
utf8Namer = otlptranslator.NewMetricNamer("", otlpTranslator.NoTranslation)
// With namespace and suffixes
productionNamer := otlptranslator.MetricNamer{
Namespace: "myservice",
WithMetricSuffixes: true,
UTF8Allowed: false,
}
```
## License
Licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

24
vendor/github.com/prometheus/otlptranslator/doc.go generated vendored Normal file
View file

@ -0,0 +1,24 @@
// Copyright 2025 The Prometheus 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 otlptranslator provides utilities for converting OpenTelemetry Protocol (OTLP)
// metric and attribute names to Prometheus-compliant formats.
//
// This package is designed to help users translate OpenTelemetry metrics to Prometheus
// metrics while following the official OpenTelemetry to Prometheus compatibility specification.
//
// Main components:
// - MetricNamer: Translates OTLP metric names to Prometheus metric names
// - LabelNamer: Translates OTLP attribute names to Prometheus label names
// - UnitNamer: Translates OTLP units to Prometheus unit conventions
package otlptranslator

View file

@ -0,0 +1,90 @@
// Copyright 2025 The Prometheus 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.
// Provenance-includes-location: https://github.com/prometheus/prometheus/blob/93e991ef7ed19cc997a9360c8016cac3767b8057/storage/remote/otlptranslator/prometheus/normalize_label.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: Copyright The Prometheus Authors
// Provenance-includes-location: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/95e8f8fdc2a9dc87230406c9a3cf02be4fd68bea/pkg/translator/prometheus/normalize_label.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: Copyright The OpenTelemetry Authors.
package otlptranslator
import (
"fmt"
"strings"
"unicode"
)
// LabelNamer is a helper struct to build label names.
// It translates OpenTelemetry Protocol (OTLP) attribute names to Prometheus-compliant label names.
//
// Example usage:
//
// namer := LabelNamer{UTF8Allowed: false}
// result := namer.Build("http.method") // "http_method"
type LabelNamer struct {
UTF8Allowed bool
}
// Build normalizes the specified label to follow Prometheus label names standard.
//
// Translation rules:
// - Replaces invalid characters with underscores
// - Prefixes labels with invalid start characters (numbers or `_`) with "key"
// - Preserves double underscore labels (reserved names)
// - If UTF8Allowed is true, returns label as-is
//
// Examples:
//
// namer := LabelNamer{UTF8Allowed: false}
// namer.Build("http.method") // "http_method"
// namer.Build("123invalid") // "key_123invalid"
// namer.Build("__reserved__") // "__reserved__" (preserved)
func (ln *LabelNamer) Build(label string) (normalizedName string, err error) {
defer func() {
if len(normalizedName) == 0 {
err = fmt.Errorf("normalization for label name %q resulted in empty name", label)
return
}
if ln.UTF8Allowed || normalizedName == label {
return
}
// Check that the resulting normalized name contains at least one non-underscore character
for _, c := range normalizedName {
if c != '_' {
return
}
}
err = fmt.Errorf("normalization for label name %q resulted in invalid name %q", label, normalizedName)
normalizedName = ""
}()
// Trivial case.
if len(label) == 0 || ln.UTF8Allowed {
normalizedName = label
return
}
normalizedName = sanitizeLabelName(label)
// If label starts with a number, prepend with "key_".
if unicode.IsDigit(rune(normalizedName[0])) {
normalizedName = "key_" + normalizedName
} else if strings.HasPrefix(normalizedName, "_") && !strings.HasPrefix(normalizedName, "__") {
normalizedName = "key" + normalizedName
}
return
}

View file

@ -20,6 +20,7 @@
package otlptranslator
import (
"fmt"
"slices"
"strings"
"unicode"
@ -81,13 +82,48 @@ var perUnitMap = map[string]string{
}
// MetricNamer is a helper struct to build metric names.
// It converts OpenTelemetry Protocol (OTLP) metric names to Prometheus-compliant metric names.
//
// Example usage:
//
// namer := MetricNamer{
// WithMetricSuffixes: true,
// UTF8Allowed: false,
// }
//
// metric := Metric{
// Name: "http.server.duration",
// Unit: "s",
// Type: MetricTypeHistogram,
// }
//
// result := namer.Build(metric) // "http_server_duration_seconds"
type MetricNamer struct {
Namespace string
WithMetricSuffixes bool
UTF8Allowed bool
}
// NewMetricNamer creates a MetricNamer with the specified namespace (can be
// blank) and the requested Translation Strategy.
func NewMetricNamer(namespace string, strategy TranslationStrategyOption) MetricNamer {
return MetricNamer{
Namespace: namespace,
WithMetricSuffixes: strategy.ShouldAddSuffixes(),
UTF8Allowed: !strategy.ShouldEscape(),
}
}
// Metric is a helper struct that holds information about a metric.
// It represents an OpenTelemetry metric with its name, unit, and type.
//
// Example:
//
// metric := Metric{
// Name: "http.server.request.duration",
// Unit: "s",
// Type: MetricTypeHistogram,
// }
type Metric struct {
Name string
Unit string
@ -96,34 +132,70 @@ type Metric struct {
// Build builds a metric name for the specified metric.
//
// If UTF8Allowed is true, the metric name is returned as is, only with the addition of type/unit suffixes and namespace preffix if required.
// Otherwise the metric name is normalized to be Prometheus-compliant.
// See rules at https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels,
// https://prometheus.io/docs/practices/naming/#metric-and-label-naming
func (mn *MetricNamer) Build(metric Metric) string {
// The method applies different transformations based on the MetricNamer configuration:
// - If UTF8Allowed is true, doesn't translate names - all characters must be valid UTF-8, however.
// - If UTF8Allowed is false, translates metric names to comply with legacy Prometheus name scheme by escaping invalid characters to `_`.
// - If WithMetricSuffixes is true, adds appropriate suffixes based on type and unit.
//
// See rules at https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
//
// Examples:
//
// namer := MetricNamer{WithMetricSuffixes: true, UTF8Allowed: false}
//
// // Counter gets _total suffix
// counter := Metric{Name: "requests.count", Unit: "1", Type: MetricTypeMonotonicCounter}
// result := namer.Build(counter) // "requests_count_total"
//
// // Gauge with unit suffix
// gauge := Metric{Name: "memory.usage", Unit: "By", Type: MetricTypeGauge}
// result = namer.Build(gauge) // "memory_usage_bytes"
func (mn *MetricNamer) Build(metric Metric) (string, error) {
if mn.UTF8Allowed {
return mn.buildMetricName(metric.Name, metric.Unit, metric.Type)
}
return mn.buildCompliantMetricName(metric.Name, metric.Unit, metric.Type)
}
func (mn *MetricNamer) buildCompliantMetricName(name, unit string, metricType MetricType) string {
func (mn *MetricNamer) buildCompliantMetricName(name, unit string, metricType MetricType) (normalizedName string, err error) {
defer func() {
if len(normalizedName) == 0 {
err = fmt.Errorf("normalization for metric %q resulted in empty name", name)
return
}
if normalizedName == name {
return
}
// Check that the resulting normalized name contains at least one non-underscore character
for _, c := range normalizedName {
if c != '_' {
return
}
}
err = fmt.Errorf("normalization for metric %q resulted in invalid name %q", name, normalizedName)
normalizedName = ""
}()
// Full normalization following standard Prometheus naming conventions
if mn.WithMetricSuffixes {
return normalizeName(name, unit, metricType, mn.Namespace)
normalizedName = normalizeName(name, unit, metricType, mn.Namespace)
return
}
// Simple case (no full normalization, no units, etc.).
metricName := strings.Join(strings.FieldsFunc(name, func(r rune) bool {
return invalidMetricCharRE.MatchString(string(r))
return !isValidCompliantMetricChar(r) && r != '_'
}), "_")
// Namespace?
if mn.Namespace != "" {
namespace := strings.Join(strings.FieldsFunc(mn.Namespace, func(r rune) bool {
return invalidMetricCharRE.MatchString(string(r))
return !isValidCompliantMetricChar(r) && r != '_'
}), "_")
return namespace + "_" + metricName
normalizedName = namespace + "_" + metricName
return
}
// Metric name starts with a digit? Prefix it with an underscore.
@ -131,14 +203,11 @@ func (mn *MetricNamer) buildCompliantMetricName(name, unit string, metricType Me
metricName = "_" + metricName
}
return metricName
normalizedName = metricName
return
}
var (
// Regexp for metric name characters that should be replaced with _.
invalidMetricCharRE = regexp.MustCompile(`[^a-zA-Z0-9:_]`)
multipleUnderscoresRE = regexp.MustCompile(`__+`)
)
var multipleUnderscoresRE = regexp.MustCompile(`__+`)
// isValidCompliantMetricChar checks if a rune is a valid metric name character (a-z, A-Z, 0-9, :).
func isValidCompliantMetricChar(r rune) bool {
@ -243,33 +312,54 @@ func removeItem(slice []string, value string) []string {
return newSlice
}
func (mn *MetricNamer) buildMetricName(name, unit string, metricType MetricType) string {
func (mn *MetricNamer) buildMetricName(inputName, unit string, metricType MetricType) (name string, err error) {
name = inputName
if mn.Namespace != "" {
name = mn.Namespace + "_" + name
}
if mn.WithMetricSuffixes {
mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit)
if mainUnitSuffix != "" {
name = name + "_" + mainUnitSuffix
}
if perUnitSuffix != "" {
name = name + "_" + perUnitSuffix
}
// Append _total for Counters
if metricType == MetricTypeMonotonicCounter {
name += "_total"
}
// Append _ratio for metrics with unit "1"
// Some OTel receivers improperly use unit "1" for counters of objects
// See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aissue+some+metric+units+don%27t+follow+otel+semantic+conventions
// Until these issues have been fixed, we're appending `_ratio` for gauges ONLY
// Theoretically, counters could be ratios as well, but it's absurd (for mathematical reasons)
if unit == "1" && metricType == MetricTypeGauge {
name += "_ratio"
name = trimSuffixAndDelimiter(name, "ratio")
defer func() {
name += "_ratio"
}()
}
// Append _total for Counters.
if metricType == MetricTypeMonotonicCounter {
name = trimSuffixAndDelimiter(name, "total")
defer func() {
name += "_total"
}()
}
mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit)
if perUnitSuffix != "" {
name = trimSuffixAndDelimiter(name, perUnitSuffix)
defer func() {
name = name + "_" + perUnitSuffix
}()
}
// We don't need to trim and re-append the suffix here because this is
// the inner-most suffix.
if mainUnitSuffix != "" && !strings.HasSuffix(name, mainUnitSuffix) {
name = name + "_" + mainUnitSuffix
}
}
return
}
// trimSuffixAndDelimiter trims a suffix, plus one extra character which is
// assumed to be a delimiter.
func trimSuffixAndDelimiter(name, suffix string) string {
if strings.HasSuffix(name, suffix) && len(name) > len(suffix)+1 {
return name[:len(name)-(len(suffix)+1)]
}
return name
}

View file

@ -1,57 +0,0 @@
// Copyright 2025 The Prometheus 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.
// Provenance-includes-location: https://github.com/prometheus/prometheus/blob/93e991ef7ed19cc997a9360c8016cac3767b8057/storage/remote/otlptranslator/prometheus/normalize_label.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: Copyright The Prometheus Authors
// Provenance-includes-location: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/95e8f8fdc2a9dc87230406c9a3cf02be4fd68bea/pkg/translator/prometheus/normalize_label.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: Copyright The OpenTelemetry Authors.
package otlptranslator
import (
"strings"
"unicode"
)
// LabelNamer is a helper struct to build label names.
type LabelNamer struct {
UTF8Allowed bool
}
// Build normalizes the specified label to follow Prometheus label names standard.
//
// See rules at https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels.
//
// Labels that start with non-letter rune will be prefixed with "key_".
// An exception is made for double-underscores which are allowed.
//
// If UTF8Allowed is true, the label is returned as is. This option is provided just to
// keep a consistent interface with the MetricNamer.
func (ln *LabelNamer) Build(label string) string {
// Trivial case.
if len(label) == 0 || ln.UTF8Allowed {
return label
}
label = sanitizeLabelName(label)
// If label starts with a number, prepend with "key_".
if unicode.IsDigit(rune(label[0])) {
label = "key_" + label
} else if strings.HasPrefix(label, "_") && !strings.HasPrefix(label, "__") {
label = "key" + label
}
return label
}

View file

@ -0,0 +1,86 @@
// Copyright 2025 The Prometheus 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.
// Provenance-includes-location: https://github.com/prometheus/prometheus/blob/3602785a89162ccc99a940fb9d862219a2d02241/config/config.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: Copyright The Prometheus Authors
package otlptranslator
// TranslationStrategyOption is a constant that defines how metric and label
// names should be handled during translation. The recommended approach is to
// use either UnderscoreEscapingWithSuffixes for full Prometheus-style
// compatibility, or NoTranslation for Otel-style names.
type TranslationStrategyOption string
var (
// NoUTF8EscapingWithSuffixes will accept metric/label names as they are. Unit
// and type suffixes may be added to metric names, according to certain rules.
NoUTF8EscapingWithSuffixes TranslationStrategyOption = "NoUTF8EscapingWithSuffixes"
// UnderscoreEscapingWithSuffixes is the default option for translating OTLP
// to Prometheus. This option will translate metric name characters that are
// not alphanumerics/underscores/colons to underscores, and label name
// characters that are not alphanumerics/underscores to underscores. Unit and
// type suffixes may be appended to metric names, according to certain rules.
UnderscoreEscapingWithSuffixes TranslationStrategyOption = "UnderscoreEscapingWithSuffixes"
// UnderscoreEscapingWithoutSuffixes translates metric name characters that
// are not alphanumerics/underscores/colons to underscores, and label name
// characters that are not alphanumerics/underscores to underscores, but
// unlike UnderscoreEscapingWithSuffixes it does not append any suffixes to
// the names.
UnderscoreEscapingWithoutSuffixes TranslationStrategyOption = "UnderscoreEscapingWithoutSuffixes"
// NoTranslation (EXPERIMENTAL): disables all translation of incoming metric
// and label names. This offers a way for the OTLP users to use native metric
// names, reducing confusion.
//
// WARNING: This setting has significant known risks and limitations (see
// https://prometheus.io/docs/practices/naming/ for details): * Impaired UX
// when using PromQL in plain YAML (e.g. alerts, rules, dashboard, autoscaling
// configuration). * Series collisions which in the best case may result in
// OOO errors, in the worst case a silently malformed time series. For
// instance, you may end up in situation of ingesting `foo.bar` series with
// unit `seconds` and a separate series `foo.bar` with unit `milliseconds`.
//
// As a result, this setting is experimental and currently, should not be used
// in production systems.
//
// TODO(ArthurSens): Mention `type-and-unit-labels` feature
// (https://github.com/prometheus/proposals/pull/39) once released, as
// potential mitigation of the above risks.
NoTranslation TranslationStrategyOption = "NoTranslation"
)
// ShouldEscape returns true if the translation strategy requires that metric
// names be escaped.
func (o TranslationStrategyOption) ShouldEscape() bool {
switch o {
case UnderscoreEscapingWithSuffixes, UnderscoreEscapingWithoutSuffixes:
return true
case NoTranslation, NoUTF8EscapingWithSuffixes:
return false
default:
return false
}
}
// ShouldAddSuffixes returns a bool deciding whether the given translation
// strategy should have suffixes added.
func (o TranslationStrategyOption) ShouldAddSuffixes() bool {
switch o {
case UnderscoreEscapingWithSuffixes, NoUTF8EscapingWithSuffixes:
return true
case UnderscoreEscapingWithoutSuffixes, NoTranslation:
return false
default:
return false
}
}

View file

@ -15,14 +15,34 @@ package otlptranslator
import "strings"
// UnitNamer is a helper for building compliant unit names.
// It processes OpenTelemetry Protocol (OTLP) unit strings and converts them
// to Prometheus-compliant unit names.
//
// Example usage:
//
// namer := UnitNamer{UTF8Allowed: false}
// result := namer.Build("s") // "seconds"
// result = namer.Build("By/s") // "bytes_per_second"
type UnitNamer struct {
UTF8Allowed bool
}
// Build builds a unit name for the specified unit string.
// It processes the unit by splitting it into main and per components,
// applying appropriate unit mappings, and cleaning up invalid characters
// when the whole UTF-8 character set is not allowed.
// applying unit mappings, and cleaning up invalid characters when UTF8Allowed is false.
//
// Unit mappings include:
// - Time: s→seconds, ms→milliseconds, h→hours
// - Bytes: By→bytes, KBy→kilobytes, MBy→megabytes
// - SI: m→meters, V→volts, W→watts
// - Special: 1→"" (empty), %→percent
//
// Examples:
//
// namer := UnitNamer{UTF8Allowed: false}
// namer.Build("s") // "seconds"
// namer.Build("requests/s") // "requests_per_second"
// namer.Build("1") // "" (dimensionless)
func (un *UnitNamer) Build(unit string) string {
mainUnit, perUnit := buildUnitSuffixes(unit)
if !un.UTF8Allowed {