Skip to content

Commit

Permalink
prometheus: allow stats to be opt out from automatic namespacing (env…
Browse files Browse the repository at this point in the history
…oyproxy#11808)

Filters can register prefixes that will not have the normal `envoy_` namespace.

Signed-off-by: Lizan Zhou <[email protected]>
  • Loading branch information
lizan authored Jul 10, 2020
1 parent 46ef88f commit e712d9d
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 7 deletions.
45 changes: 38 additions & 7 deletions source/server/admin/prometheus_stats.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "server/admin/prometheus_stats.h"

#include "common/common/empty_string.h"
#include "common/common/macros.h"
#include "common/stats/histogram_impl.h"

#include "absl/strings/str_cat.h"
Expand All @@ -11,19 +12,18 @@ namespace Server {
namespace {

const std::regex& promRegex() { CONSTRUCT_ON_FIRST_USE(std::regex, "[^a-zA-Z0-9_]"); }
const std::regex& namespaceRegex() {
CONSTRUCT_ON_FIRST_USE(std::regex, "^[a-zA-Z_][a-zA-Z0-9]*$");
}

/**
* Take a string and sanitize it according to Prometheus conventions.
*/
std::string sanitizeName(const std::string& name) {
// The name must match the regex [a-zA-Z_][a-zA-Z0-9_]* as required by
// prometheus. Refer to https://prometheus.io/docs/concepts/data_model/.
std::string stats_name = std::regex_replace(name, promRegex(), "_");
if (stats_name[0] >= '0' && stats_name[0] <= '9') {
return absl::StrCat("_", stats_name);
} else {
return stats_name;
}
// The initial [a-zA-Z_] constraint is always satisfied by the namespace prefix.
return std::regex_replace(name, promRegex(), "_");
}

/*
Expand Down Expand Up @@ -176,6 +176,10 @@ std::string generateHistogramOutput(const Stats::ParentHistogram& histogram,
return output;
};

absl::flat_hash_set<std::string>& prometheusNamespaces() {
MUTABLE_CONSTRUCT_ON_FIRST_USE(absl::flat_hash_set<std::string>);
}

} // namespace

std::string PrometheusStatsFormatter::formattedTags(const std::vector<Stats::Tag>& tags) {
Expand All @@ -188,10 +192,19 @@ std::string PrometheusStatsFormatter::formattedTags(const std::vector<Stats::Tag
}

std::string PrometheusStatsFormatter::metricName(const std::string& extracted_name) {
std::string sanitized_name = sanitizeName(extracted_name);

absl::string_view prom_namespace{sanitized_name};
prom_namespace = prom_namespace.substr(0, prom_namespace.find_first_of('_'));

if (prometheusNamespaces().contains(prom_namespace)) {
return sanitized_name;
}

// Add namespacing prefix to avoid conflicts, as per best practice:
// https://prometheus.io/docs/practices/naming/#metric-names
// Also, naming conventions on https://prometheus.io/docs/concepts/data_model/
return sanitizeName(fmt::format("envoy_{0}", extracted_name));
return absl::StrCat("envoy_", sanitized_name);
}

// TODO(efimki): Add support of text readouts stats.
Expand All @@ -214,5 +227,23 @@ uint64_t PrometheusStatsFormatter::statsAsPrometheus(
return metric_name_count;
}

bool PrometheusStatsFormatter::registerPrometheusNamespace(absl::string_view prometheus_namespace) {
if (std::regex_match(prometheus_namespace.begin(), prometheus_namespace.end(),
namespaceRegex())) {
return prometheusNamespaces().insert(std::string(prometheus_namespace)).second;
}
return false;
}

bool PrometheusStatsFormatter::unregisterPrometheusNamespace(
absl::string_view prometheus_namespace) {
auto it = prometheusNamespaces().find(prometheus_namespace);
if (it == prometheusNamespaces().end()) {
return false;
}
prometheusNamespaces().erase(it);
return true;
}

} // namespace Server
} // namespace Envoy
18 changes: 18 additions & 0 deletions source/server/admin/prometheus_stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,28 @@ class PrometheusStatsFormatter {
* of <tag_name>="<tag_value>" pairs.
*/
static std::string formattedTags(const std::vector<Stats::Tag>& tags);

/**
* Format the given metric name, prefixed with "envoy_".
*/
static std::string metricName(const std::string& extracted_name);

/**
* Register a prometheus namespace, stats starting with the namespace will not be
* automatically prefixed with envoy namespace.
* This method must be called from the main thread.
* @returns bool if a new namespace is registered, false if the namespace is already
* registered or the namespace is invalid.
*/
static bool registerPrometheusNamespace(absl::string_view prometheus_namespace);

/**
* Unregister a prometheus namespace registered by `registerPrometheusNamespace`
* This method must be called from the main thread.
* @returns bool if the Prometheus namespace is unregistered. false if the namespace
* wasn't registered.
*/
static bool unregisterPrometheusNamespace(absl::string_view prometheus_namespace);
};

} // namespace Server
Expand Down
16 changes: 16 additions & 0 deletions test/server/admin/prometheus_stats_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ TEST_F(PrometheusStatsFormatterTest, SanitizeMetricNameDigitFirst) {
EXPECT_EQ(expected, actual);
}

TEST_F(PrometheusStatsFormatterTest, NamespaceRegistry) {
std::string raw = "vulture.eats-liver";
std::string expected = "vulture_eats_liver";

EXPECT_FALSE(PrometheusStatsFormatter::registerPrometheusNamespace("3vulture"));
EXPECT_FALSE(PrometheusStatsFormatter::registerPrometheusNamespace(".vulture"));

EXPECT_FALSE(PrometheusStatsFormatter::unregisterPrometheusNamespace("vulture"));
EXPECT_TRUE(PrometheusStatsFormatter::registerPrometheusNamespace("vulture"));
EXPECT_FALSE(PrometheusStatsFormatter::registerPrometheusNamespace("vulture"));
EXPECT_EQ(expected, PrometheusStatsFormatter::metricName(raw));
EXPECT_TRUE(PrometheusStatsFormatter::unregisterPrometheusNamespace("vulture"));

EXPECT_EQ("envoy_" + expected, PrometheusStatsFormatter::metricName(raw));
}

TEST_F(PrometheusStatsFormatterTest, FormattedTags) {
std::vector<Stats::Tag> tags;
Stats::Tag tag1 = {"a.tag-name", "a.tag-value"};
Expand Down

0 comments on commit e712d9d

Please sign in to comment.