Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Part of the [Prometheus](https://prometheus.io/) ecosystem, following the [OpenT
- **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.
- **Updated UCUM Unit Mappings**: Opt in to corrected UCUM unit suffixes (`TiBy`: `tebibytes`, `kBy`: `kilobytes`) by selecting the `UnderscoreEscapingWithUpdatedSuffixes` or `NoUTF8EscapingWithUpdatedSuffixes` translation strategy.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently reads like a changelog item. Please see my suggestion for line 10 instead.

Suggested change
- **Updated UCUM Unit Mappings**: Opt in to corrected UCUM unit suffixes (`TiBy`: `tebibytes`, `kBy`: `kilobytes`) by selecting the `UnderscoreEscapingWithUpdatedSuffixes` or `NoUTF8EscapingWithUpdatedSuffixes` translation strategy.


## Installation

Expand All @@ -31,7 +32,9 @@ import (

func main() {
// Create a metric namer using traditional Prometheus name translation, with suffixes added and UTF-8 disallowed.
strategy := otlptranslator.UnderscoreEscapingWithSuffixes
// Use UnderscoreEscapingWithUpdatedSuffixes to opt into corrected UCUM unit mappings:
// TiBy -> "tebibytes" instead of "tibibytes" and kBy -> "kilobytes".
strategy := otlptranslator.UnderscoreEscapingWithUpdatedSuffixes
Comment on lines +35 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo this change.

namer := otlptranslator.NewMetricNamer("myapp", strategy)

// Translate OTLP metric to Prometheus format
Expand Down
30 changes: 19 additions & 11 deletions metric_namer.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ var unitMap = map[string]string{
"%": "percent",
}

var updatedUnitMap = map[string]string{
"TiBy": "tebibytes",
"kBy": "kilobytes",
}
Comment on lines +70 to +73
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var updatedUnitMap = map[string]string{
"TiBy": "tebibytes",
"kBy": "kilobytes",
}
// legacyUnitMap holds the unit mappings this library shipped before the
// TiBy/kBy corrections. Selected when MetricNamer.LegacyUnitMapping or
// UnitNamer.LegacyUnitMapping is true.
var legacyUnitMap = map[string]string{
"TiBy": "tibibytes",
}
// legacyUnitExclusions are units that map in unitMap but were not present in
// the historical map; in legacy mode they fall through unchanged.
var legacyUnitExclusions = map[string]struct{}{
"kBy": {},
}


// The map that translates the "per" unit.
// Example: s => per second (singular).
var perUnitMap = map[string]string{
Expand Down Expand Up @@ -97,19 +102,22 @@ var perUnitMap = map[string]string{
//
// result := namer.Build(metric) // "http_server_duration_seconds"
type MetricNamer struct {
Namespace string
WithMetricSuffixes bool
UTF8Allowed bool
Namespace string
WithMetricSuffixes bool
UTF8Allowed bool
UpdatedMetricMapping bool // to fix the UCUM metrics suffix tebibyte and kBy
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
UpdatedMetricMapping bool // to fix the UCUM metrics suffix tebibyte and kBy
// LegacyUnitMapping selects the pre-correction UCUM unit mappings (e.g.
// "TiBy" -> "tibibytes" instead of the spec-correct "tebibytes"). The
// default value (false) uses the spec-correct mappings. Set to true to
// preserve metric names produced by older versions of this library.
LegacyUnitMapping 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(),
mn := MetricNamer{
Namespace: namespace,
WithMetricSuffixes: strategy.ShouldAddSuffixes(),
UTF8Allowed: !strategy.ShouldEscape(),
UpdatedMetricMapping: strategy.ShouldUseUpdatedSuffixes(),
}
return mn
Comment on lines -108 to +120
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo this stylistic change:

Suggested change
return MetricNamer{
Namespace: namespace,
WithMetricSuffixes: strategy.ShouldAddSuffixes(),
UTF8Allowed: !strategy.ShouldEscape(),
mn := MetricNamer{
Namespace: namespace,
WithMetricSuffixes: strategy.ShouldAddSuffixes(),
UTF8Allowed: !strategy.ShouldEscape(),
UpdatedMetricMapping: strategy.ShouldUseUpdatedSuffixes(),
}
return mn
return MetricNamer{
Namespace: namespace,
WithMetricSuffixes: strategy.ShouldAddSuffixes(),
UTF8Allowed: !strategy.ShouldEscape(),
}

}

// Metric is a helper struct that holds information about a metric.
Expand Down Expand Up @@ -178,7 +186,7 @@ func (mn *MetricNamer) buildCompliantMetricName(name, unit string, metricType Me

// Full normalization following standard Prometheus naming conventions
if mn.WithMetricSuffixes {
normalizedName = normalizeName(name, unit, metricType, mn.Namespace)
normalizedName = normalizeName(name, unit, metricType, mn.Namespace, mn.UpdatedMetricMapping)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
normalizedName = normalizeName(name, unit, metricType, mn.Namespace, mn.UpdatedMetricMapping)
normalizedName = normalizeName(name, unit, metricType, mn.Namespace, mn.LegacyUnitMapping)

return
}

Expand Down Expand Up @@ -222,7 +230,7 @@ func replaceInvalidMetricChar(r rune) rune {
}

// Build a normalized name for the specified metric.
func normalizeName(name, unit string, metricType MetricType, namespace string) string {
func normalizeName(name, unit string, metricType MetricType, namespace string, updatedMetricsMapping bool) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func normalizeName(name, unit string, metricType MetricType, namespace string, updatedMetricsMapping bool) string {
func normalizeName(name, unit string, metricType MetricType, namespace string, legacyUnitMapping bool) string {

// Split metric name into "tokens" (of supported metric name runes).
// Note that this has the side effect of replacing multiple consecutive underscores with a single underscore.
// This is part of the OTel to Prometheus specification: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.38.0/specification/compatibility/prometheus_and_openmetrics.md#otlp-metric-points-to-prometheus.
Expand All @@ -231,7 +239,7 @@ func normalizeName(name, unit string, metricType MetricType, namespace string) s
func(r rune) bool { return !isValidCompliantMetricChar(r) },
)

mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit)
mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit, updatedMetricsMapping)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit, updatedMetricsMapping)
mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit, legacyUnitMapping)

nameTokens = addUnitTokens(nameTokens, cleanUpUnit(mainUnitSuffix), cleanUpUnit(perUnitSuffix))

// Append _total for Counters
Expand Down Expand Up @@ -335,7 +343,7 @@ func (mn *MetricNamer) buildMetricName(inputName, unit string, metricType Metric
}()
}

mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit)
mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit, mn.UpdatedMetricMapping)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit, mn.UpdatedMetricMapping)
mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit, mn.LegacyUnitMapping)

if perUnitSuffix != "" {
name = trimSuffixAndDelimiter(name, perUnitSuffix)
defer func() {
Expand Down
56 changes: 55 additions & 1 deletion metric_namer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,43 @@ func TestMetricNamer_Build(t *testing.T) {
wantMetricName: "capacity_tibibytes",
wantUnitName: "tibibytes",
},
{
name: "metric with tebibytes unit with flag",
namer: MetricNamer{
UTF8Allowed: false,
WithMetricSuffixes: true,
UpdatedMetricMapping: true,
},
metric: Metric{
Name: "capacity",
Unit: "TiBy",
Type: MetricTypeGauge,
},
wantMetricName: "capacity_tebibytes",
wantUnitName: "tebibytes",
},
{
name: "metrics with updated mapping using NoUTF8EscapingWithUpdatedSuffixes flag",
namer: NewMetricNamer("mock", NoUTF8EscapingWithUpdatedSuffixes),
metric: Metric{
Name: "capacity",
Unit: "TiBy",
Type: MetricTypeGauge,
},
wantMetricName: "mock_capacity_tebibytes",
wantUnitName: "tebibytes",
},
{
name: "metrics with updated mapping using UnderscoreEscapingWithUpdatedSuffixes flag",
namer: NewMetricNamer("mock", UnderscoreEscapingWithUpdatedSuffixes),
metric: Metric{
Name: "capacity",
Unit: "TiBy",
Type: MetricTypeGauge,
},
wantMetricName: "mock_capacity_tebibytes",
wantUnitName: "tebibytes",
},
{
name: "metric with kilobytes unit",
namer: MetricNamer{
Expand All @@ -818,6 +855,22 @@ func TestMetricNamer_Build(t *testing.T) {
wantMetricName: "transfer_kilobytes",
wantUnitName: "kilobytes",
},

{
name: "metric with kilobytes unit with flag",
namer: MetricNamer{
UTF8Allowed: false,
WithMetricSuffixes: true,
UpdatedMetricMapping: true,
},
metric: Metric{
Name: "transfer",
Unit: "kBy",
Type: MetricTypeGauge,
},
wantMetricName: "transfer_kilobytes",
wantUnitName: "kilobytes",
},
{
name: "metric with megabytes unit",
namer: MetricNamer{
Expand Down Expand Up @@ -1207,7 +1260,8 @@ func TestMetricNamer_Build(t *testing.T) {
// Build unit name using UnitNamer to verify correlation when suffixes are enabled
if tt.namer.WithMetricSuffixes {
unitNamer := UnitNamer{
UTF8Allowed: tt.namer.UTF8Allowed,
UTF8Allowed: tt.namer.UTF8Allowed,
UpdatedMetricMapping: tt.namer.UpdatedMetricMapping,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
UpdatedMetricMapping: tt.namer.UpdatedMetricMapping,
LegacyUnitMapping: tt.namer.LegacyUnitMapping,

}
gotUnitName := unitNamer.Build(tt.metric.Unit)
if gotUnitName != tt.wantUnitName {
Expand Down
28 changes: 25 additions & 3 deletions strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,26 @@ var (
// (https://github.com/prometheus/proposals/pull/39) once released, as
// potential mitigation of the above risks.
NoTranslation TranslationStrategyOption = "NoTranslation"
// NoUTF8EscapingWithUpdatedSuffixes will accept metric/label names as they are. Unit
// and type suffixes may be added to metric names, according to certain rules.
// This option includes an updated mapping for the UCUM metrics suffixes tebibyte and kBy.
NoUTF8EscapingWithUpdatedSuffixes TranslationStrategyOption = "NoUTF8EscapingWithUpdatedSuffixes"
// 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.
// This option includes an updated mapping for the UCUM metrics suffixes tebibyte and kBy.
UnderscoreEscapingWithUpdatedSuffixes TranslationStrategyOption = "UnderscoreEscapingWithUpdatedSuffixes"
Comment on lines +60 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New strategies is the wrong approach:

Suggested change
// NoUTF8EscapingWithUpdatedSuffixes will accept metric/label names as they are. Unit
// and type suffixes may be added to metric names, according to certain rules.
// This option includes an updated mapping for the UCUM metrics suffixes tebibyte and kBy.
NoUTF8EscapingWithUpdatedSuffixes TranslationStrategyOption = "NoUTF8EscapingWithUpdatedSuffixes"
// 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.
// This option includes an updated mapping for the UCUM metrics suffixes tebibyte and kBy.
UnderscoreEscapingWithUpdatedSuffixes TranslationStrategyOption = "UnderscoreEscapingWithUpdatedSuffixes"

)

// ShouldEscape returns true if the translation strategy requires that metric
// names be escaped.
func (o TranslationStrategyOption) ShouldEscape() bool {
switch o {
case UnderscoreEscapingWithSuffixes, UnderscoreEscapingWithoutSuffixes:
case UnderscoreEscapingWithSuffixes, UnderscoreEscapingWithoutSuffixes, UnderscoreEscapingWithUpdatedSuffixes:
return true
case NoTranslation, NoUTF8EscapingWithSuffixes:
case NoTranslation, NoUTF8EscapingWithSuffixes, NoUTF8EscapingWithUpdatedSuffixes:
return false
default:
return false
Expand All @@ -76,11 +87,22 @@ func (o TranslationStrategyOption) ShouldEscape() bool {
// strategy should have suffixes added.
func (o TranslationStrategyOption) ShouldAddSuffixes() bool {
switch o {
case UnderscoreEscapingWithSuffixes, NoUTF8EscapingWithSuffixes:
case UnderscoreEscapingWithSuffixes, NoUTF8EscapingWithSuffixes, UnderscoreEscapingWithUpdatedSuffixes, NoUTF8EscapingWithUpdatedSuffixes:
return true
case UnderscoreEscapingWithoutSuffixes, NoTranslation:
return false
default:
return false
}
}

func (o TranslationStrategyOption) ShouldUseUpdatedSuffixes() bool {
switch o {
case NoUTF8EscapingWithUpdatedSuffixes, UnderscoreEscapingWithUpdatedSuffixes:
return true
case NoUTF8EscapingWithSuffixes, UnderscoreEscapingWithSuffixes, UnderscoreEscapingWithoutSuffixes, NoTranslation:
return false
default:
return false
}
}
21 changes: 15 additions & 6 deletions unit_namer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import "strings"
// result := namer.Build("s") // "seconds"
// result = namer.Build("By/s") // "bytes_per_second"
type UnitNamer struct {
UTF8Allowed bool
UTF8Allowed bool
UpdatedMetricMapping bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
UpdatedMetricMapping bool
// LegacyUnitMapping selects the pre-correction UCUM unit mappings (e.g.
// "TiBy" -> "tibibytes" instead of the spec-correct "tebibytes"). The
// default value (false) uses the spec-correct mappings. Set to true to
// preserve metric names produced by older versions of this library.
LegacyUnitMapping bool

}

// Build builds a unit name for the specified unit string.
Expand All @@ -33,7 +34,7 @@ type UnitNamer struct {
//
// Unit mappings include:
// - Time: s→seconds, ms→milliseconds, h→hours
// - Bytes: By→bytes, KBy→kilobytes, MBy→megabytes
// - Bytes: By→bytes, kBy→kilobytes, MBy→megabytes
// - SI: m→meters, V→volts, W→watts
// - Special: 1→"" (empty), %→percent
//
Expand All @@ -44,7 +45,7 @@ type UnitNamer struct {
// namer.Build("requests/s") // "requests_per_second"
// namer.Build("1") // "" (dimensionless)
func (un *UnitNamer) Build(unit string) string {
mainUnit, perUnit := buildUnitSuffixes(unit)
mainUnit, perUnit := buildUnitSuffixes(unit, un.UpdatedMetricMapping)
if !un.UTF8Allowed {
mainUnit, perUnit = cleanUpUnit(mainUnit), cleanUpUnit(perUnit)
}
Expand Down Expand Up @@ -72,7 +73,15 @@ func (un *UnitNamer) Build(unit string) string {

// Retrieve the Prometheus "basic" unit corresponding to the specified "basic" unit.
// Returns the specified unit if not found in unitMap.
func unitMapGetOrDefault(unit string) string {
func unitMapGetOrDefault(unit string, updatedSuffix bool) string {
if updatedSuffix {
// checks for updatd values, TiBy <-> tebibytes and kBy <-> kilobytes.
if promUnit, ok := updatedUnitMap[unit]; ok {
return promUnit
}
// doesnt return cause what if the unit is not in the updated map but is
// in the original map, we want to check that as well.
}
Comment on lines +77 to +84
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if updatedSuffix {
// checks for updatd values, TiBy <-> tebibytes and kBy <-> kilobytes.
if promUnit, ok := updatedUnitMap[unit]; ok {
return promUnit
}
// doesnt return cause what if the unit is not in the updated map but is
// in the original map, we want to check that as well.
}
if legacy {
if promUnit, ok := legacyUnitMap[unit]; ok {
return promUnit
}
if _, excluded := legacyUnitExclusions[unit]; excluded {
return unit
}
}

if promUnit, ok := unitMap[unit]; ok {
return promUnit
}
Expand All @@ -91,7 +100,7 @@ func perUnitMapGetOrDefault(perUnit string) string {
// buildUnitSuffixes builds the main and per unit suffixes for the specified unit
// but doesn't do any special character transformation to accommodate Prometheus naming conventions.
// Removing trailing underscores or appending suffixes is done in the caller.
func buildUnitSuffixes(unit string) (mainUnitSuffix, perUnitSuffix string) {
func buildUnitSuffixes(unit string, updatedMetricName bool) (mainUnitSuffix, perUnitSuffix string) {
// Split unit at the '/' if any
unitTokens := strings.SplitN(unit, "/", 2)

Expand All @@ -100,7 +109,7 @@ func buildUnitSuffixes(unit string) (mainUnitSuffix, perUnitSuffix string) {
// Update if not blank and doesn't contain '{}'
mainUnitOTel := strings.TrimSpace(unitTokens[0])
if mainUnitOTel != "" && !strings.ContainsAny(mainUnitOTel, "{}") {
mainUnitSuffix = unitMapGetOrDefault(mainUnitOTel)
mainUnitSuffix = unitMapGetOrDefault(mainUnitOTel, updatedMetricName)
}

// Per unit
Expand Down