Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EXPORTER] Prometheus: Sanitize labels according to spec #2330

Merged
merged 1 commit into from
Sep 27, 2023
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Increment the:
[#2324](https://github.com/open-telemetry/opentelemetry-cpp/pull/2324)
* [EXPORTER] Handle attribute key collisions caused by sanitation
[#2324](https://github.com/open-telemetry/opentelemetry-cpp/pull/2326)
* [EXPORTER] Replace colons with underscores when converting to Prometheus label
[#2324](https://github.com/open-telemetry/opentelemetry-cpp/pull/2330)

## [1.11.0] 2023-08-21

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,6 @@ class PrometheusExporterUtils
const sdk::metrics::ResourceMetrics &data);

private:
/**
* 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 '_'.
*/
static std::string SanitizeNames(std::string name);

static opentelemetry::sdk::metrics::AggregationType getAggregationType(
const opentelemetry::sdk::metrics::PointType &point_type);

Expand Down
129 changes: 81 additions & 48 deletions exporters/prometheus/src/exporter_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,85 @@ namespace exporter
{
namespace metrics
{
namespace
{
/**
* Sanitize the given metric name by replacing invalid characters with _,
* ensuring that multiple consecutive _ characters are collapsed to a single _.
*
* @param valid a callable with the signature `(int pos, char ch) -> bool` that
* returns whether `ch` is valid at position `pos` in the string
* @param name the string to sanitize
*/
template <typename T>
inline std::string Sanitize(std::string name, const T &valid)
{
static_assert(std::is_convertible<T, std::function<bool(int, char)>>::value,
"valid should be a callable with the signature "
"(int, char) -> bool");

constexpr const auto replacement = '_';
constexpr const auto replacement_dup = '=';

bool has_dup = false;
for (int i = 0; i < (int)name.size(); ++i)
{
if (valid(i, name[i]) && name[i] != replacement)
{
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;
}

/**
* Sanitize the given metric label key according to Prometheus rule.
* Prometheus metric label keys 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 SanitizeLabel(std::string label_key)
{
return Sanitize(label_key, [](int i, char c) {
return (c >= 'a' && c <= 'z') || //
(c >= 'A' && c <= 'Z') || //
c == '_' || //
(c >= '0' && c <= '9' && i > 0);
lalitb marked this conversation as resolved.
Show resolved Hide resolved
});
}

/**
* 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

/**
* Helper function to convert OpenTelemetry metrics data collection
* to Prometheus metrics data collection
Expand All @@ -40,7 +119,7 @@ std::vector<prometheus_client::MetricFamily> PrometheusExporterUtils::TranslateT
{
auto origin_name = metric_data.instrument_descriptor.name_;
auto unit = metric_data.instrument_descriptor.unit_;
auto sanitized = SanitizeNames(origin_name);
auto sanitized = SanitizeName(origin_name);
prometheus_client::MetricFamily metric_family;
metric_family.name = sanitized + "_" + unit;
metric_family.help = metric_data.instrument_descriptor.description_;
Expand Down Expand Up @@ -120,52 +199,6 @@ std::vector<prometheus_client::MetricFamily> PrometheusExporterUtils::TranslateT
return output;
}

/**
* 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;
}

metric_sdk::AggregationType PrometheusExporterUtils::getAggregationType(
const metric_sdk::PointType &point_type)
{
Expand Down Expand Up @@ -271,7 +304,7 @@ void PrometheusExporterUtils::SetMetricBasic(prometheus_client::ClientMetric &me
std::string previous_key;
for (auto const &label : labels)
{
auto sanitized = SanitizeNames(label.first);
auto sanitized = SanitizeLabel(label.first);
int comparison = previous_key.compare(sanitized);
if (metric.label.empty() || comparison < 0) // new key
{
Expand Down
44 changes: 33 additions & 11 deletions exporters/prometheus/test/exporter_utils_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -138,33 +138,55 @@ TEST(PrometheusExporterUtils, TranslateToPrometheusHistogramNormal)
assert_histogram(metric, std::list<double>{10.1, 20.2, 30.2}, {200, 300, 400, 500});
}

class SanitizeNameTest : public ::testing::Test
class SanitizeTest : public ::testing::Test
{
Resource resource_ = Resource::Create({});
nostd::unique_ptr<InstrumentationScope> instrumentation_scope_ =
InstrumentationScope::Create("library_name", "1.2.0");

protected:
void CheckSanitation(const std::string &original, const std::string &sanitized)
void CheckSanitizeName(const std::string &original, const std::string &sanitized)
{
metric_sdk::InstrumentDescriptor instrument_descriptor_{
metric_sdk::InstrumentDescriptor instrument_descriptor{
original, "description", "unit", metric_sdk::InstrumentType::kCounter,
metric_sdk::InstrumentValueType::kDouble};
std::vector<prometheus::MetricFamily> result = PrometheusExporterUtils::TranslateToPrometheus(
{&resource_,
{{instrumentation_scope_.get(), {{instrument_descriptor_, {}, {}, {}, {{{}, {}}}}}}}});
{{instrumentation_scope_.get(), {{instrument_descriptor, {}, {}, {}, {{{}, {}}}}}}}});
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<prometheus::MetricFamily> result = PrometheusExporterUtils::TranslateToPrometheus(
{&resource_,
{{instrumentation_scope_.get(),
{{instrument_descriptor, {}, {}, {}, {{{{original, "value"}}, {}}}}}}}});
EXPECT_EQ(result.begin()->metric.begin()->label.begin()->name, sanitized);
}
};

TEST_F(SanitizeNameTest, All)
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)
{
CheckSanitation("name", "name");
CheckSanitation("name?", "name_");
CheckSanitation("name???", "name_");
CheckSanitation("name?__", "name_");
CheckSanitation("name?__name", "name_name");
CheckSanitation("name?__name:", "name_name:");
CheckSanitizeLabel("name", "name");
CheckSanitizeLabel("name?", "name_");
CheckSanitizeLabel("name???", "name_");
CheckSanitizeLabel("name?__", "name_");
CheckSanitizeLabel("name?__name", "name_name");
CheckSanitizeLabel("name?__name:", "name_name_");
}

class AttributeCollisionTest : public ::testing::Test
Expand Down