// 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 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 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 { mainUnit, perUnit = cleanUpUnit(mainUnit), cleanUpUnit(perUnit) } var u string switch { case mainUnit != "" && perUnit != "": u = mainUnit + "_" + perUnit case mainUnit != "": u = mainUnit default: u = perUnit } // Clean up leading and trailing underscores if len(u) > 0 && u[0:1] == "_" { u = u[1:] } if len(u) > 0 && u[len(u)-1:] == "_" { u = u[:len(u)-1] } return u } // 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 { if promUnit, ok := unitMap[unit]; ok { return promUnit } return unit } // Retrieve the Prometheus "per" unit corresponding to the specified "per" unit. // Returns the specified unit if not found in perUnitMap. func perUnitMapGetOrDefault(perUnit string) string { if promPerUnit, ok := perUnitMap[perUnit]; ok { return promPerUnit } return perUnit } // 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) { // Split unit at the '/' if any unitTokens := strings.SplitN(unit, "/", 2) if len(unitTokens) > 0 { // Main unit // Update if not blank and doesn't contain '{}' mainUnitOTel := strings.TrimSpace(unitTokens[0]) if mainUnitOTel != "" && !strings.ContainsAny(mainUnitOTel, "{}") { mainUnitSuffix = unitMapGetOrDefault(mainUnitOTel) } // Per unit // Update if not blank and doesn't contain '{}' if len(unitTokens) > 1 && unitTokens[1] != "" { perUnitOTel := strings.TrimSpace(unitTokens[1]) if perUnitOTel != "" && !strings.ContainsAny(perUnitOTel, "{}") { perUnitSuffix = perUnitMapGetOrDefault(perUnitOTel) } if perUnitSuffix != "" { perUnitSuffix = "per_" + perUnitSuffix } } } return mainUnitSuffix, perUnitSuffix } // cleanUpUnit cleans up unit so it matches model.LabelNameRE. func cleanUpUnit(unit string) string { // Multiple consecutive underscores are replaced 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. return strings.TrimPrefix(multipleUnderscoresRE.ReplaceAllString( strings.Map(replaceInvalidMetricChar, unit), "_", ), "_") }