You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kudu.apache.org by al...@apache.org on 2026/09/24 01:17:43 UTC
(kudu) branch master updated: [metrics] add --metrics_prometheus_default_metrics for a server-side allowlist
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kudu.git
The following commit(s) were added to refs/heads/master by this push:
new ee47a9ca1 [metrics] add --metrics_prometheus_default_metrics for a server-side allowlist
ee47a9ca1 is described below
commit ee47a9ca1939200e227f9bf266c97a7c0eed3ccc
Author: Yan-Daojiang <ya...@163.com>
AuthorDate: Thu Sep 10 18:09:18 2026 +0800
[metrics] add --metrics_prometheus_default_metrics for a server-side allowlist
The /metrics_prometheus endpoint already honors a per-request 'metrics'
allowlist (a case-insensitive substring match), but there is no server-side
default: only severity has one, via --metrics_default_level. As a result, any
scrape that omits the 'metrics' parameter -- an ad-hoc curl, a one-off script,
or a misconfigured collector -- receives the full metric set, and the allowlist
must be carried in every scrape URL. That URL channel is bounded by the ~16KB
request buffer and is prone to silent degradation: if the value collapses to no
usable token, filtering is disabled and the full output is returned.
Add --metrics_prometheus_default_metrics, mirroring the existing
--metrics_prometheus_default_merge_rules / --metrics_prometheus_default_quantiles
family: a request's own 'metrics' parameter takes precedence over the flag
default, which is applied only when the request carries none; an empty value
(the default) preserves the current behavior. This turns the allowlist from a
per-request convention into a server-side policy that covers parameter-less
scrapes as well.
Also simplify the 'export_all' check in Histogram::WriteAsPrometheus() per
a review suggestion from Change-Id: I01b48aa.
Change-Id: I70ef54eef456108f8503a4edfdfb68fab3173353
Reviewed-on: http://gerrit.cloudera.org:8080/24836
Tested-by: Alexey Serbin <al...@apache.org>
Reviewed-by: Alexey Serbin <al...@apache.org>
---
src/kudu/server/default_path_handlers.cc | 6 +++
src/kudu/tserver/tablet_server-test.cc | 31 +++++++++++++++
src/kudu/util/metrics-test.cc | 68 ++++++++++++++++++++++++++++++++
src/kudu/util/metrics.cc | 65 ++++++++++++++++++++++++++++--
src/kudu/util/metrics.h | 7 ++++
5 files changed, 173 insertions(+), 4 deletions(-)
diff --git a/src/kudu/server/default_path_handlers.cc b/src/kudu/server/default_path_handlers.cc
index 86bb8ad04..4e09c9c45 100644
--- a/src/kudu/server/default_path_handlers.cc
+++ b/src/kudu/server/default_path_handlers.cc
@@ -552,6 +552,12 @@ static void WriteMetricsAsPrometheus(const MetricRegistry* const metrics,
// default configured via --metrics_prometheus_default_quantiles.
GetPrometheusQuantiles(ParseArray(req.parsed_args, kQuantilesParam), &opts.quantiles);
+ // A 'metrics' query parameter (already parsed into 'entity_metrics' above)
+ // takes precedence over the server-side default configured via
+ // --metrics_prometheus_default_metrics, which restricts even parameter-less
+ // scrapes to the configured allowlist.
+ GetPrometheusMetricsFilter(&opts.filters.entity_metrics);
+
// The hostname label is emitted either in the label-based non-merged format,
// or whenever merging is active (merged output is always label-based).
if (FLAGS_metrics_prometheus_use_entity_labels || !opts.merge_rules.empty()) {
diff --git a/src/kudu/tserver/tablet_server-test.cc b/src/kudu/tserver/tablet_server-test.cc
index 6a3b087c6..c1dc57523 100644
--- a/src/kudu/tserver/tablet_server-test.cc
+++ b/src/kudu/tserver/tablet_server-test.cc
@@ -219,6 +219,7 @@ DECLARE_string(block_manager);
DECLARE_string(env_inject_eio_globs);
DECLARE_string(env_inject_full_globs);
DECLARE_string(metrics_default_level);
+DECLARE_string(metrics_prometheus_default_metrics);
DECLARE_string(webserver_doc_root);
DECLARE_uint32(tablet_apply_pool_overload_threshold_ms);
@@ -4623,6 +4624,36 @@ TEST_F(TabletServerTest, PrometheusMetricsNameFiltering) {
}
}
+// Verify that --metrics_prometheus_default_metrics applies the allowlist to a
+// parameter-less scrape, and that a ?metrics= query parameter overrides it.
+TEST_F(TabletServerTest, PrometheusDefaultMetricsFlag) {
+ google::FlagSaver saver;
+ FLAGS_metrics_prometheus_default_metrics = "raft_term";
+
+ const string base_url = Substitute("http://$0/metrics_prometheus",
+ mini_server_->bound_http_addr().ToString());
+ {
+ // No query parameter: the flag default restricts the output to raft_term.
+ EasyCurl c;
+ faststring buf;
+ ASSERT_OK(c.FetchURL(base_url, &buf));
+ const auto& str = buf.ToString();
+ NO_FATALS(CheckPrometheusOutput(str));
+ ASSERT_STR_MATCHES(str, "raft_term ");
+ ASSERT_STR_NOT_MATCHES(str, "threads_running ");
+ }
+ {
+ // ?metrics=threads_running overrides the flag default for this request.
+ EasyCurl c;
+ faststring buf;
+ ASSERT_OK(c.FetchURL(base_url + "?metrics=threads_running", &buf));
+ const auto& str = buf.ToString();
+ NO_FATALS(CheckPrometheusOutput(str));
+ ASSERT_STR_MATCHES(str, "threads_running ");
+ ASSERT_STR_NOT_MATCHES(str, "raft_term ");
+ }
+}
+
// Verify that ?ids= filters the output to only entities with the given ID.
TEST_F(TabletServerTest, PrometheusMetricsIdFiltering) {
const string base_url = Substitute("http://$0/metrics_prometheus",
diff --git a/src/kudu/util/metrics-test.cc b/src/kudu/util/metrics-test.cc
index 7a762c33c..54a918cda 100644
--- a/src/kudu/util/metrics-test.cc
+++ b/src/kudu/util/metrics-test.cc
@@ -62,6 +62,7 @@ DECLARE_bool(metrics_prometheus_use_entity_labels);
DECLARE_bool(metrics_prometheus_export_hostname);
DECLARE_string(metrics_prometheus_default_merge_rules);
DECLARE_string(metrics_prometheus_default_quantiles);
+DECLARE_string(metrics_prometheus_default_metrics);
namespace kudu {
@@ -550,6 +551,73 @@ TEST_F(MetricsTest, PrometheusMalformedDefaultMergeRulesFlagTest) {
ASSERT_TRUE(ContainsKey(rules, "tablet"));
}
+// The server-wide --metrics_prometheus_default_metrics allowlist is applied
+// when a request carries no 'metrics' filter of its own.
+TEST_F(MetricsTest, PrometheusDefaultMetricsFromFlagTest) {
+ google::FlagSaver saver;
+ FLAGS_metrics_prometheus_default_metrics = "test_counter";
+
+ // No per-request allowlist: the flag default seeds the filter.
+ vector<string> entity_metrics;
+ GetPrometheusMetricsFilter(&entity_metrics);
+ ASSERT_EQ(vector<string>{"test_counter"}, entity_metrics);
+}
+
+// A request's own 'metrics' allowlist takes precedence over the flag default.
+TEST_F(MetricsTest, PrometheusRequestMetricsOverrideFlagTest) {
+ google::FlagSaver saver;
+ FLAGS_metrics_prometheus_default_metrics = "test_counter";
+
+ // The request-supplied allowlist is left untouched by the flag default.
+ vector<string> entity_metrics = {"raft_term"};
+ GetPrometheusMetricsFilter(&entity_metrics);
+ ASSERT_EQ(vector<string>{"raft_term"}, entity_metrics);
+}
+
+// A --metrics_prometheus_default_metrics value with no usable token (e.g. only
+// commas) yields an empty allowlist, so no metric name filtering is performed.
+TEST_F(MetricsTest, PrometheusNoUsableDefaultMetricsFlagTest) {
+ google::FlagSaver saver;
+
+ // Empty flag and empty request: nothing to filter by.
+ FLAGS_metrics_prometheus_default_metrics = "";
+ vector<string> entity_metrics;
+ GetPrometheusMetricsFilter(&entity_metrics);
+ ASSERT_TRUE(entity_metrics.empty());
+
+ // Only commas: SplitStringUsing() skips the empty tokens, so the allowlist
+ // stays empty rather than degrading to an all-matching empty substring.
+ FLAGS_metrics_prometheus_default_metrics = ",,";
+ entity_metrics.clear();
+ GetPrometheusMetricsFilter(&entity_metrics);
+ ASSERT_TRUE(entity_metrics.empty());
+}
+
+// The server-wide --metrics_prometheus_default_metrics allowlist restricts the
+// exported metrics end-to-end when a request carries no 'metrics' filter.
+TEST_F(MetricsTest, PrometheusDefaultMetricsFilterAppliedTest) {
+ google::FlagSaver saver;
+ FLAGS_metrics_prometheus_default_metrics = "tablet_test_counter";
+
+ MetricRegistry registry;
+ auto tablet = METRIC_ENTITY_tablet.Instantiate(®istry, "tablet-1");
+ METRIC_tablet_test_counter.Instantiate(tablet)->IncrementBy(7);
+ auto table = METRIC_ENTITY_table.Instantiate(®istry, "table-1");
+ METRIC_table_test_counter.Instantiate(table)->IncrementBy(9);
+
+ // No per-request allowlist: the flag default restricts the output.
+ MetricPrometheusOptions opts;
+ GetPrometheusMetricsFilter(&opts.filters.entity_metrics);
+
+ ostringstream output;
+ PrometheusWriter writer(&output);
+ ASSERT_OK(registry.WriteAsPrometheus(&writer, opts));
+
+ const auto& out = output.str();
+ ASSERT_STR_CONTAINS(out, "tablet_test_counter");
+ ASSERT_STR_NOT_CONTAINS(out, "table_test_counter");
+}
+
TEST_F(MetricsTest, CounterPrometheusTest) {
scoped_refptr<Counter> requests(new Counter(&METRIC_test_counter));
diff --git a/src/kudu/util/metrics.cc b/src/kudu/util/metrics.cc
index 4d45cc709..d8cacc223 100644
--- a/src/kudu/util/metrics.cc
+++ b/src/kudu/util/metrics.cc
@@ -182,6 +182,46 @@ DEFINE_validator(metrics_prometheus_default_quantiles,
return true;
});
+DEFINE_string(metrics_prometheus_default_metrics, "",
+ "The default metric name allowlist applied by the "
+ "'/metrics_prometheus' endpoint when the request does not carry a "
+ "'metrics' query parameter. The value is a comma-separated list of "
+ "case-insensitive substrings; a metric is exported when its name "
+ "contains at least one of them, matching the semantics of the "
+ "'metrics' query parameter. For example, "
+ "'connections_accepted,raft_term' restricts the output to metrics "
+ "whose names contain either substring. This applies the allowlist "
+ "to every scrape, including parameter-less ones, rather than "
+ "relying on each consumer to pass 'metrics'. Empty by default, "
+ "i.e. no metric name filtering is performed unless a request asks "
+ "for it.");
+TAG_FLAG(metrics_prometheus_default_metrics, advanced);
+TAG_FLAG(metrics_prometheus_default_metrics, runtime);
+TAG_FLAG(metrics_prometheus_default_metrics, evolving);
+DEFINE_validator(metrics_prometheus_default_metrics,
+ [](const char* flag_name, const string& value) {
+ // An empty value disables default filtering and is always valid. A metric
+ // name substring has no internal structure to validate, but warn about (but
+ // tolerate) a non-empty value that yields no usable token (e.g. only commas),
+ // which would silently export every metric instead of restricting the
+ // output; see GetPrometheusMetricsFilter(). Only this zero-token case is
+ // guarded: a value that yields a whitespace-only token (e.g. " " or "a, ")
+ // survives SplitStringUsing(), matches no metric name, and produces the
+ // opposite silent failure -- an empty scrape -- which is left unguarded here
+ // to match the 'metrics' query parameter and the sibling default flags.
+ if (value.empty()) {
+ return true;
+ }
+ vector<string> tokens;
+ SplitStringUsing(value, ",", &tokens);
+ if (tokens.empty()) {
+ LOG(WARNING) << Substitute(
+ "--$0 is set to '$1' but contains no usable metric name; no metric "
+ "name filtering will be applied", flag_name, value);
+ }
+ return true;
+});
+
// Process/server-wide metrics should go into the 'server' entity.
// More complex applications will define other entities.
METRIC_DEFINE_entity(server);
@@ -342,6 +382,18 @@ void GetPrometheusQuantiles(const vector<string>& request_quantiles,
}
}
+void GetPrometheusMetricsFilter(vector<string>* entity_metrics) {
+ // A request-supplied 'metrics' allowlist (already parsed into
+ // 'entity_metrics') takes precedence over the server-side default. The value
+ // is used verbatim by MatchNameInList(), so no further parsing is needed.
+ if (!entity_metrics->empty()) {
+ return;
+ }
+ if (!FLAGS_metrics_prometheus_default_metrics.empty()) {
+ SplitStringUsing(FLAGS_metrics_prometheus_default_metrics, ",", entity_metrics);
+ }
+}
+
//
// MetricUnit
//
@@ -1476,11 +1528,16 @@ Status Histogram::WriteAsPrometheus(PrometheusWriter* writer,
const char* const unit = MetricUnit::Name(prototype_->unit());
DCHECK(unit);
- // An all-empty selection exports every quantile (the default). This is
- // loop-invariant, so evaluate it once here rather than per quantile line.
+ // An all-empty selection exports every quantile (the default). The selection
+ // is filled contiguously from the first slot in canonical order (see
+ // ParseQuantiles()), so an empty first slot means nothing was selected. This
+ // is loop-invariant, so evaluate it once here rather than per quantile line.
const auto& quantiles = opts.quantiles;
- const bool export_all = std::all_of(quantiles.begin(), quantiles.end(),
- [](const char* q) { return q == nullptr; });
+ const bool export_all = (quantiles[0] == nullptr);
+ // Guard the contiguous-fill invariant the shortcut above relies on: an empty
+ // first slot must imply an all-empty selection.
+ DCHECK(!export_all || std::all_of(quantiles.begin(), quantiles.end(),
+ [](const char* q) { return q == nullptr; }));
// A snapshot is taken to have more consistent statistics while generating
// the output.
diff --git a/src/kudu/util/metrics.h b/src/kudu/util/metrics.h
index 57e223ac2..99dc7219e 100644
--- a/src/kudu/util/metrics.h
+++ b/src/kudu/util/metrics.h
@@ -505,6 +505,13 @@ typedef std::array<const char*, kNumHistogramQuantiles> HistogramQuantiles;
void GetPrometheusQuantiles(const std::vector<std::string>& request_quantiles,
HistogramQuantiles* quantiles);
+// Apply the server-side default metric name allowlist for a /metrics_prometheus
+// request to 'entity_metrics'. A request-supplied 'metrics' allowlist (already
+// parsed into 'entity_metrics') takes precedence; the server-wide default
+// configured via --metrics_prometheus_default_metrics is applied only when
+// 'entity_metrics' is empty. An empty result means "export every metric".
+void GetPrometheusMetricsFilter(std::vector<std::string>* entity_metrics);
+
struct MetricJsonOptions {
MetricJsonOptions() :
include_raw_histograms(false),