From 71d0e50b12769375e0d84bbcfdf78f9b69a02dff Mon Sep 17 00:00:00 2001 From: Ehsan Saei <71217171+esigo@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:26:14 +0100 Subject: [PATCH] [EXPORTER] Prometheus: Add unit to names, convert to word (#2213) --- .../exporters/prometheus/exporter_utils.h | 93 +++++- exporters/prometheus/src/exporter_utils.cc | 269 +++++++++++++++--- .../prometheus/test/exporter_utils_test.cc | 167 ++++++++--- 3 files changed, 454 insertions(+), 75 deletions(-) diff --git a/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter_utils.h b/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter_utils.h index 96c7ac8826..fa3eedd2c4 100644 --- a/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter_utils.h +++ b/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter_utils.h @@ -7,6 +7,7 @@ #include #include #include "opentelemetry/metrics/provider.h" +#include "opentelemetry/nostd/string_view.h" #include "opentelemetry/sdk/metrics/meter.h" #include "opentelemetry/version.h" @@ -35,7 +36,17 @@ class PrometheusExporterUtils private: /** - * Append key-value pair to prometheus labels. + * Sanitize the given metric name or label according to Prometheus rule. + * + * This function is needed because names in OpenTelemetry can contain + * alphanumeric characters, '_', '.', and '-', whereas in Prometheus the + * name should only contain alphanumeric characters and '_'. + * @param name name + */ + static std::string SanitizeNames(std::string name); + + /** + * Sanitize the given metric name or label according to Prometheus rule. * * @param name label name * @param value label value @@ -45,6 +56,79 @@ class PrometheusExporterUtils std::string value, std::vector<::prometheus::ClientMetric::Label> *labels); + static std::string MapToPrometheusName(const std::string &name, + const std::string &unit, + ::prometheus::MetricType prometheus_type); + + /** + * A utility function that returns the equivalent Prometheus name for the provided OTLP metric + * unit. + * + * @param raw_metric_unitName The raw metric unit for which Prometheus metric unit needs to be + * computed. + * @return the computed Prometheus metric unit equivalent of the OTLP metric unit + */ + static std::string GetEquivalentPrometheusUnit(const std::string &raw_metric_unitName); + + /** + * This method retrieves the expanded Prometheus unit name for known abbreviations. OTLP metrics + * use the c/s notation as specified at UCUM. The list of + * mappings is adopted from OpenTelemetry + * Collector Contrib. + * + * @param unit_abbreviation The unit that name that needs to be expanded/converted to Prometheus + * units. + * @return The expanded/converted unit name if known, otherwise returns the input unit name as-is. + */ + static std::string GetPrometheusUnit(const std::string &unit_abbreviation); + + /** + * This method retrieves the expanded Prometheus unit name to be used with "per" units for known + * units. For example: s => per second (singular) + * + * @param per_unit_abbreviation The unit abbreviation used in a 'per' unit. + * @return The expanded unit equivalent to be used in 'per' unit if the input is a known unit, + * otherwise returns the input as-is. + */ + static std::string GetPrometheusPerUnit(const std::string &per_unit_abbreviation); + + /** + * Replaces all characters that are not a letter or a digit with '_' to make the resulting string + * Prometheus compliant. This method also removes leading and trailing underscores - this is done + * to keep the resulting unit similar to what is produced from the collector's implementation. + * + * @param str The string input that needs to be made Prometheus compliant. + * @return the cleaned-up Prometheus compliant string. + */ + static std::string CleanUpString(const std::string &str); + + /** + * This method is used to convert the units expressed as a rate via '/' symbol in their name to + * their expanded text equivalent. For instance, km/h => km_per_hour. The method operates on the + * input by splitting it in 2 parts - before and after '/' symbol and will attempt to expand any + * known unit abbreviation in both parts. Unknown abbreviations & unsupported characters will + * remain unchanged in the final output of this function. + * + * @param rate_expressed_unit The rate unit input that needs to be converted to its text + * equivalent. + * @return The text equivalent of unit expressed as rate. If the input does not contain '/', the + * function returns it as-is. + */ + static std::string ConvertRateExpressedToPrometheusUnit(const std::string &rate_expressed_unit); + + /** + * This method drops all characters enclosed within '{}' (including the curly braces) by replacing + * them with an empty string. Note that this method will not produce the intended effect if there + * are nested curly braces within the outer enclosure of '{}'. + * + *

For instance, {packet{s}s} => s}. + * + * @param unit The input unit from which text within curly braces needs to be removed. + * @return The resulting unit after removing the text within '{}'. + */ + static std::string RemoveUnitPortionInBraces(const std::string &unit); + static opentelemetry::sdk::metrics::AggregationType getAggregationType( const opentelemetry::sdk::metrics::PointType &point_type); @@ -58,6 +142,7 @@ class PrometheusExporterUtils * Add a target_info metric to collect resource attributes */ static void SetTarget(const sdk::metrics::ResourceMetrics &data, + std::chrono::nanoseconds time, const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope, std::vector<::prometheus::MetricFamily> *output); @@ -70,6 +155,7 @@ class PrometheusExporterUtils const opentelemetry::sdk::metrics::PointAttributes &labels, const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope, ::prometheus::MetricType type, + std::chrono::nanoseconds time, ::prometheus::MetricFamily *metric_family, const opentelemetry::sdk::resource::Resource *resource); @@ -83,6 +169,7 @@ class PrometheusExporterUtils const std::vector &counts, const opentelemetry::sdk::metrics::PointAttributes &labels, const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope, + std::chrono::nanoseconds time, ::prometheus::MetricFamily *metric_family, const opentelemetry::sdk::resource::Resource *resource); @@ -92,6 +179,7 @@ class PrometheusExporterUtils static void SetMetricBasic( ::prometheus::ClientMetric &metric, const opentelemetry::sdk::metrics::PointAttributes &labels, + std::chrono::nanoseconds time, const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope, const opentelemetry::sdk::resource::Resource *resource); @@ -122,6 +210,9 @@ class PrometheusExporterUtils const std::vector &boundaries, const std::vector &counts, ::prometheus::ClientMetric *metric); + + // For testing + friend class SanitizeNameTester; }; } // namespace metrics } // namespace exporter diff --git a/exporters/prometheus/src/exporter_utils.cc b/exporters/prometheus/src/exporter_utils.cc index 42fafc6336..4b1ee054c4 100644 --- a/exporters/prometheus/src/exporter_utils.cc +++ b/exporters/prometheus/src/exporter_utils.cc @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include #include @@ -93,22 +94,6 @@ std::string SanitizeLabel(std::string label_key) }); } -/** - * Sanitize the given metric name according to Prometheus rule. - * Prometheus metric names are required to match the following regex: - * [a-zA-Z_:]([a-zA-Z0-9_:])* - * and multiple consecutive _ characters must be collapsed to a single _. - */ -std::string SanitizeName(std::string name) -{ - return Sanitize(name, [](int i, char c) { - return (c >= 'a' && c <= 'z') || // - (c >= 'A' && c <= 'Z') || // - c == '_' || // - c == ':' || // - (c >= '0' && c <= '9' && i > 0); - }); -} } // namespace /** @@ -132,11 +117,16 @@ std::vector PrometheusExporterUtils::TranslateT std::vector output; output.reserve(reserve_size); - + if (data.scope_metric_data_.empty()) + { + return output; + } // Append target_info as the first metric if (populate_target_info && !data.scope_metric_data_.empty()) { - SetTarget(data, (*data.scope_metric_data_.begin()).scope_, &output); + SetTarget(data, + data.scope_metric_data_.begin()->metric_data_.begin()->end_ts.time_since_epoch(), + (*data.scope_metric_data_.begin()).scope_, &output); } for (const auto &instrumentation_info : data.scope_metric_data_) @@ -145,21 +135,22 @@ std::vector PrometheusExporterUtils::TranslateT { auto origin_name = metric_data.instrument_descriptor.name_; auto unit = metric_data.instrument_descriptor.unit_; - auto sanitized = SanitizeName(origin_name); prometheus_client::MetricFamily metric_family; - metric_family.name = sanitized + "_" + unit; metric_family.help = metric_data.instrument_descriptor.description_; + auto time = metric_data.end_ts.time_since_epoch(); + auto front = metric_data.point_data_attr_.front(); + auto kind = getAggregationType(front.point_data); + bool is_monotonic = true; + if (kind == sdk::metrics::AggregationType::kSum) + { + is_monotonic = nostd::get(front.point_data).is_monotonic_; + } + const prometheus_client::MetricType type = TranslateType(kind, is_monotonic); + metric_family.name = MapToPrometheusName(metric_data.instrument_descriptor.name_, + metric_data.instrument_descriptor.unit_, type); + metric_family.type = type; for (const auto &point_data_attr : metric_data.point_data_attr_) { - auto kind = getAggregationType(point_data_attr.point_data); - bool is_monotonic = true; - if (kind == sdk::metrics::AggregationType::kSum) - { - is_monotonic = - nostd::get(point_data_attr.point_data).is_monotonic_; - } - const prometheus_client::MetricType type = TranslateType(kind, is_monotonic); - metric_family.type = type; if (type == prometheus_client::MetricType::Histogram) // Histogram { auto histogram_point_data = @@ -176,7 +167,7 @@ std::vector PrometheusExporterUtils::TranslateT sum = static_cast(nostd::get(histogram_point_data.sum_)); } SetData(std::vector{sum, (double)histogram_point_data.count_}, boundaries, counts, - point_data_attr.attributes, instrumentation_info.scope_, &metric_family, + point_data_attr.attributes, instrumentation_info.scope_, time, &metric_family, data.resource_); } else if (type == prometheus_client::MetricType::Gauge) @@ -187,7 +178,7 @@ std::vector PrometheusExporterUtils::TranslateT auto last_value_point_data = nostd::get(point_data_attr.point_data); std::vector values{last_value_point_data.value_}; - SetData(values, point_data_attr.attributes, instrumentation_info.scope_, type, + SetData(values, point_data_attr.attributes, instrumentation_info.scope_, type, time, &metric_family, data.resource_); } else if (nostd::holds_alternative(point_data_attr.point_data)) @@ -195,7 +186,7 @@ std::vector PrometheusExporterUtils::TranslateT auto sum_point_data = nostd::get(point_data_attr.point_data); std::vector values{sum_point_data.value_}; - SetData(values, point_data_attr.attributes, instrumentation_info.scope_, type, + SetData(values, point_data_attr.attributes, instrumentation_info.scope_, type, time, &metric_family, data.resource_); } else @@ -212,7 +203,7 @@ std::vector PrometheusExporterUtils::TranslateT auto sum_point_data = nostd::get(point_data_attr.point_data); std::vector values{sum_point_data.value_}; - SetData(values, point_data_attr.attributes, instrumentation_info.scope_, type, + SetData(values, point_data_attr.attributes, instrumentation_info.scope_, type, time, &metric_family, data.resource_); } else @@ -240,6 +231,205 @@ void PrometheusExporterUtils::AddPrometheusLabel( labels->emplace_back(std::move(prometheus_label)); } +/** + * Sanitize the given metric name or label according to Prometheus rule. + * + * This function is needed because names in OpenTelemetry can contain + * alphanumeric characters, '_', '.', and '-', whereas in Prometheus the + * name should only contain alphanumeric characters and '_'. + */ +std::string PrometheusExporterUtils::SanitizeNames(std::string name) +{ + constexpr const auto replacement = '_'; + constexpr const auto replacement_dup = '='; + + auto valid = [](int i, char c) { + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == ':' || + (c >= '0' && c <= '9' && i > 0)) + { + return true; + } + return false; + }; + + bool has_dup = false; + for (int i = 0; i < (int)name.size(); ++i) + { + if (valid(i, name[i])) + { + continue; + } + if (i > 0 && (name[i - 1] == replacement || name[i - 1] == replacement_dup)) + { + has_dup = true; + name[i] = replacement_dup; + } + else + { + name[i] = replacement; + } + } + if (has_dup) + { + auto end = std::remove(name.begin(), name.end(), replacement_dup); + return std::string{name.begin(), end}; + } + return name; +} + +std::regex INVALID_CHARACTERS_PATTERN("[^a-zA-Z0-9]"); +std::regex CHARACTERS_BETWEEN_BRACES_PATTERN("\\{(.*?)\\}"); +std::regex SANITIZE_LEADING_UNDERSCORES("^_+"); +std::regex SANITIZE_TRAILING_UNDERSCORES("_+$"); +std::regex SANITIZE_CONSECUTIVE_UNDERSCORES("[_]{2,}"); + +std::string PrometheusExporterUtils::GetEquivalentPrometheusUnit( + const std::string &raw_metric_unit_name) +{ + if (raw_metric_unit_name.empty()) + { + return raw_metric_unit_name; + } + + std::string converted_metric_unit_name = RemoveUnitPortionInBraces(raw_metric_unit_name); + converted_metric_unit_name = ConvertRateExpressedToPrometheusUnit(converted_metric_unit_name); + + return CleanUpString(GetPrometheusUnit(converted_metric_unit_name)); +} + +std::string PrometheusExporterUtils::GetPrometheusUnit(const std::string &unit_abbreviation) +{ + static std::unordered_map units{// Time + {"d", "days"}, + {"h", "hours"}, + {"min", "minutes"}, + {"s", "seconds"}, + {"ms", "milliseconds"}, + {"us", "microseconds"}, + {"ns", "nanoseconds"}, + // Bytes + {"By", "bytes"}, + {"KiBy", "kibibytes"}, + {"MiBy", "mebibytes"}, + {"GiBy", "gibibytes"}, + {"TiBy", "tibibytes"}, + {"KBy", "kilobytes"}, + {"MBy", "megabytes"}, + {"GBy", "gigabytes"}, + {"TBy", "terabytes"}, + {"By", "bytes"}, + {"KBy", "kilobytes"}, + {"MBy", "megabytes"}, + {"GBy", "gigabytes"}, + {"TBy", "terabytes"}, + // SI + {"m", "meters"}, + {"V", "volts"}, + {"A", "amperes"}, + {"J", "joules"}, + {"W", "watts"}, + {"g", "grams"}, + // Misc + {"Cel", "celsius"}, + {"Hz", "hertz"}, + {"1", ""}, + {"%", "percent"}}; + auto res_it = units.find(unit_abbreviation); + if (res_it == units.end()) + { + return unit_abbreviation; + } + return res_it->second; +} + +std::string PrometheusExporterUtils::GetPrometheusPerUnit(const std::string &per_unit_abbreviation) +{ + static std::unordered_map per_units{ + {"s", "second"}, {"m", "minute"}, {"h", "hour"}, {"d", "day"}, + {"w", "week"}, {"mo", "month"}, {"y", "year"}}; + auto res_it = per_units.find(per_unit_abbreviation); + if (res_it == per_units.end()) + { + return per_unit_abbreviation; + } + return res_it->second; +} + +std::string PrometheusExporterUtils::RemoveUnitPortionInBraces(const std::string &unit) +{ + return std::regex_replace(unit, CHARACTERS_BETWEEN_BRACES_PATTERN, ""); +} + +std::string PrometheusExporterUtils::ConvertRateExpressedToPrometheusUnit( + const std::string &rate_expressed_unit) +{ + size_t pos = rate_expressed_unit.find("/"); + if (pos == std::string::npos) + { + return rate_expressed_unit; + } + + std::vector rate_entities; + rate_entities.push_back(rate_expressed_unit.substr(0, pos)); + rate_entities.push_back(rate_expressed_unit.substr(pos + 1)); + + if (rate_entities[1].empty()) + { + return rate_expressed_unit; + } + + std::string prometheus_unit = GetPrometheusUnit(rate_entities[0]); + std::string prometheus_per_unit = GetPrometheusPerUnit(rate_entities[1]); + + return prometheus_unit + "_per_" + prometheus_per_unit; +} + +std::string PrometheusExporterUtils::CleanUpString(const std::string &str) +{ + std::string cleaned_string = std::regex_replace(str, INVALID_CHARACTERS_PATTERN, "_"); + cleaned_string = std::regex_replace(cleaned_string, SANITIZE_CONSECUTIVE_UNDERSCORES, "_"); + cleaned_string = std::regex_replace(cleaned_string, SANITIZE_TRAILING_UNDERSCORES, ""); + cleaned_string = std::regex_replace(cleaned_string, SANITIZE_LEADING_UNDERSCORES, ""); + + return cleaned_string; +} + +std::string PrometheusExporterUtils::MapToPrometheusName( + const std::string &name, + const std::string &unit, + prometheus_client::MetricType prometheus_type) +{ + auto sanitized_name = SanitizeNames(name); + std::string prometheus_equivalent_unit = GetEquivalentPrometheusUnit(unit); + + // Append prometheus unit if not null or empty. + if (!prometheus_equivalent_unit.empty() && + sanitized_name.find(prometheus_equivalent_unit) == std::string::npos) + { + sanitized_name += "_" + prometheus_equivalent_unit; + } + + // Special case - counter + if (prometheus_type == prometheus_client::MetricType::Counter) + { + auto t_pos = sanitized_name.rfind("_total"); + bool ends_with_total = t_pos == sanitized_name.size() - 6; + if (!ends_with_total) + { + sanitized_name += "_total"; + } + } + + // Special case - gauge + if (unit == "1" && prometheus_type == prometheus_client::MetricType::Gauge && + sanitized_name.find("ratio") == std::string::npos) + { + sanitized_name += "_ratio"; + } + + return CleanUpString(SanitizeNames(sanitized_name)); +} + metric_sdk::AggregationType PrometheusExporterUtils::getAggregationType( const metric_sdk::PointType &point_type) { @@ -295,6 +485,7 @@ prometheus_client::MetricType PrometheusExporterUtils::TranslateType( void PrometheusExporterUtils::SetTarget( const sdk::metrics::ResourceMetrics &data, + std::chrono::nanoseconds time, const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope, std::vector<::prometheus::MetricFamily> *output) { @@ -313,11 +504,11 @@ void PrometheusExporterUtils::SetTarget( metric.info.value = 1.0; metric_sdk::PointAttributes empty_attributes; - SetMetricBasic(metric, empty_attributes, scope, data.resource_); + SetMetricBasic(metric, empty_attributes, time, scope, data.resource_); for (auto &label : data.resource_->GetAttributes()) { - AddPrometheusLabel(SanitizeName(label.first), AttributeValueToString(label.second), + AddPrometheusLabel(SanitizeNames(label.first), AttributeValueToString(label.second), &metric.label); } @@ -334,12 +525,13 @@ void PrometheusExporterUtils::SetData( const metric_sdk::PointAttributes &labels, const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope, prometheus_client::MetricType type, + std::chrono::nanoseconds time, prometheus_client::MetricFamily *metric_family, const opentelemetry::sdk::resource::Resource *resource) { metric_family->metric.emplace_back(); prometheus_client::ClientMetric &metric = metric_family->metric.back(); - SetMetricBasic(metric, labels, scope, resource); + SetMetricBasic(metric, labels, time, scope, resource); SetValue(values, type, &metric); } @@ -354,12 +546,13 @@ void PrometheusExporterUtils::SetData( const std::vector &counts, const metric_sdk::PointAttributes &labels, const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope, + std::chrono::nanoseconds time, prometheus_client::MetricFamily *metric_family, const opentelemetry::sdk::resource::Resource *resource) { metric_family->metric.emplace_back(); prometheus_client::ClientMetric &metric = metric_family->metric.back(); - SetMetricBasic(metric, labels, scope, resource); + SetMetricBasic(metric, labels, time, scope, resource); SetValue(values, boundaries, counts, &metric); } @@ -369,9 +562,11 @@ void PrometheusExporterUtils::SetData( void PrometheusExporterUtils::SetMetricBasic( prometheus_client::ClientMetric &metric, const metric_sdk::PointAttributes &labels, + std::chrono::nanoseconds time, const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope, const opentelemetry::sdk::resource::Resource *resource) { + metric.timestamp_ms = time.count() / 1000000; if (labels.empty() && nullptr == resource) { return; diff --git a/exporters/prometheus/test/exporter_utils_test.cc b/exporters/prometheus/test/exporter_utils_test.cc index 8ff324d7ac..b876e7369c 100644 --- a/exporters/prometheus/test/exporter_utils_test.cc +++ b/exporters/prometheus/test/exporter_utils_test.cc @@ -15,17 +15,62 @@ namespace prometheus_client = ::prometheus; OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace metrics +{ +class SanitizeNameTester +{ +public: + static std::string sanitize(std::string name) + { + return PrometheusExporterUtils::SanitizeNames(name); + } + static std::string getPrometheusUnit(const std::string &unit_abbreviation) + { + return PrometheusExporterUtils::GetPrometheusUnit(unit_abbreviation); + } + static std::string getPrometheusPerUnit(const std::string &per_unit_abbreviation) + { + return PrometheusExporterUtils::GetPrometheusPerUnit(per_unit_abbreviation); + } + static std::string removeUnitPortionInBraces(const std::string &unit) + { + return PrometheusExporterUtils::RemoveUnitPortionInBraces(unit); + } + static std::string convertRateExpressedToPrometheusUnit(const std::string &rate_expressed_unit) + { + return PrometheusExporterUtils::ConvertRateExpressedToPrometheusUnit(rate_expressed_unit); + } + static std::string cleanUpString(const std::string &str) + { + return PrometheusExporterUtils::CleanUpString(str); + } + static std::string getEquivalentPrometheusUnit(const std::string &raw_metric_unit_name) + { + return PrometheusExporterUtils::GetEquivalentPrometheusUnit(raw_metric_unit_name); + } + static std::string mapToPrometheusName(const std::string &name, + const std::string &unit, + prometheus_client::MetricType prometheus_type) + { + return PrometheusExporterUtils::MapToPrometheusName(name, unit, prometheus_type); + } +}; +} // namespace metrics +} // namespace exporter + template void assert_basic(prometheus_client::MetricFamily &metric, - const std::string &sanitized_name, + const std::string &expected_name, const std::string &description, prometheus_client::MetricType type, size_t label_num, std::vector vals) { - ASSERT_EQ(metric.name, sanitized_name + "_unit"); // name sanitized - ASSERT_EQ(metric.help, description); // description not changed - ASSERT_EQ(metric.type, type); // type translated + ASSERT_EQ(metric.name, expected_name); // name sanitized + ASSERT_EQ(metric.help, description); // description not changed + ASSERT_EQ(metric.type, type); // type translated // Prometheus metric data points should not have explicit timestamps for (const prometheus::ClientMetric &cm : metric.metric) @@ -109,12 +154,10 @@ TEST(PrometheusExporterUtils, TranslateToPrometheusIntegerCounter) auto translated = PrometheusExporterUtils::TranslateToPrometheus(metrics_data); ASSERT_EQ(translated.size(), 2); - auto metric1 = translated[1]; std::vector vals = {10}; - assert_basic(metric1, "library_name", "description", prometheus_client::MetricType::Counter, 3, - vals); - + assert_basic(metric1, "library_name_unit_total", "description", + prometheus_client::MetricType::Counter, 3, vals); int checked_label_num = 0; for (auto &label : translated[0].metric[0].label) { @@ -157,7 +200,7 @@ TEST(PrometheusExporterUtils, TranslateToPrometheusIntegerLastValue) auto metric1 = translated[1]; std::vector vals = {10}; - assert_basic(metric1, "library_name", "description", prometheus_client::MetricType::Gauge, 3, + assert_basic(metric1, "library_name_unit", "description", prometheus_client::MetricType::Gauge, 3, vals); int checked_label_num = 0; @@ -196,8 +239,8 @@ TEST(PrometheusExporterUtils, TranslateToPrometheusHistogramNormal) auto metric = translated[1]; std::vector vals = {3, 900.5, 4}; - assert_basic(metric, "library_name", "description", prometheus_client::MetricType::Histogram, 3, - vals); + assert_basic(metric, "library_name_unit", "description", prometheus_client::MetricType::Histogram, + 3, vals); assert_histogram(metric, std::list{10.1, 20.2, 30.2}, {200, 300, 400, 500}); int checked_label_num = 0; @@ -230,27 +273,12 @@ class SanitizeTest : public ::testing::Test InstrumentationScope::Create("library_name", "1.2.0"); protected: - void CheckSanitizeName(const std::string &original, const std::string &sanitized) - { - metric_sdk::InstrumentDescriptor instrument_descriptor{ - original, "description", "unit", metric_sdk::InstrumentType::kCounter, - metric_sdk::InstrumentValueType::kDouble}; - std::vector result = PrometheusExporterUtils::TranslateToPrometheus( - {&resource_, - std::vector{ - {instrumentation_scope_.get(), - std::vector{ - {{instrument_descriptor, {}, {}, {}, {{{}, {}}}}}}}}}, - false); - EXPECT_EQ(result.begin()->name, sanitized + "_unit"); - } - void CheckSanitizeLabel(const std::string &original, const std::string &sanitized) { metric_sdk::InstrumentDescriptor instrument_descriptor{ "name", "description", "unit", metric_sdk::InstrumentType::kCounter, metric_sdk::InstrumentValueType::kDouble}; - std::vector result = PrometheusExporterUtils::TranslateToPrometheus( + auto result = PrometheusExporterUtils::TranslateToPrometheus( {&resource_, std::vector{ {instrumentation_scope_.get(), @@ -261,16 +289,6 @@ class SanitizeTest : public ::testing::Test } }; -TEST_F(SanitizeTest, Name) -{ - CheckSanitizeName("name", "name"); - CheckSanitizeName("name?", "name_"); - CheckSanitizeName("name???", "name_"); - CheckSanitizeName("name?__", "name_"); - CheckSanitizeName("name?__name", "name_name"); - CheckSanitizeName("name?__name:", "name_name:"); -} - TEST_F(SanitizeTest, Label) { CheckSanitizeLabel("name", "name"); @@ -281,6 +299,8 @@ TEST_F(SanitizeTest, Label) CheckSanitizeLabel("name?__name:", "name_name_"); } +TEST_F(SanitizeTest, Name) {} + class AttributeCollisionTest : public ::testing::Test { Resource resource_ = Resource::Create(ResourceAttributes{}); @@ -325,6 +345,79 @@ TEST_F(AttributeCollisionTest, SeparatesDistinctKeys) {"otel_scope_version", "1.2.0"}}); } +TEST(PrometheusExporterUtils, PrometheusUnit) +{ + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("d"), "days"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("h"), "hours"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("min"), "minutes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("s"), "seconds"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("ms"), "milliseconds"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("us"), "microseconds"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("ns"), "nanoseconds"); + + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("By"), "bytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("KiBy"), "kibibytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("MiBy"), "mebibytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("GiBy"), "gibibytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("TiBy"), "tibibytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("KBy"), "kilobytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("MBy"), "megabytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("GBy"), "gigabytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("TBy"), "terabytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("By"), "bytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("KBy"), "kilobytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("MBy"), "megabytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("GBy"), "gigabytes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("TBy"), "terabytes"); + + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("m"), "meters"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("V"), "volts"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("A"), "amperes"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("J"), "joules"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("W"), "watts"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("g"), "grams"); + + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("Cel"), "celsius"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("Hz"), "hertz"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("1"), ""); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusUnit("%"), "percent"); +} + +TEST(PrometheusExporterUtils, PrometheusPerUnit) +{ + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusPerUnit("s"), "second"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusPerUnit("m"), "minute"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusPerUnit("h"), "hour"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusPerUnit("d"), "day"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusPerUnit("w"), "week"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusPerUnit("mo"), "month"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::getPrometheusPerUnit("y"), "year"); +} + +TEST(PrometheusExporterUtils, RemoveUnitPortionInBraces) +{ + ASSERT_EQ(exporter::metrics::SanitizeNameTester::removeUnitPortionInBraces("{unit}"), ""); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::removeUnitPortionInBraces("unit{unit}"), "unit"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::removeUnitPortionInBraces("unit_{unit}"), + "unit_"); +} + +TEST(PrometheusExporterUtils, ConvertRateExpressedToPrometheusUnit) +{ + ASSERT_EQ(exporter::metrics::SanitizeNameTester::convertRateExpressedToPrometheusUnit("unit/d"), + "unit_per_day"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::convertRateExpressedToPrometheusUnit("unit/s"), + "unit_per_second"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::convertRateExpressedToPrometheusUnit("unit/"), + "unit/"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::convertRateExpressedToPrometheusUnit("unit"), + "unit"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::convertRateExpressedToPrometheusUnit("unit/m"), + "unit_per_minute"); + ASSERT_EQ(exporter::metrics::SanitizeNameTester::convertRateExpressedToPrometheusUnit("/m"), + "_per_minute"); +} + TEST_F(AttributeCollisionTest, JoinsCollidingKeys) { CheckTranslation({{"foo.a", "value1"}, {"foo_a", "value2"}}, {{"foo_a", "value1;value2"},