This is an automated email from the ASF dual-hosted git repository.
gehafearless pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pegasus.git
The following commit(s) were added to refs/heads/master by this push:
new 5ea60899a feat(new_metrics): build the http query string based on
metric filters (#1870)
5ea60899a is described below
commit 5ea60899aec815ff0384d005b2c556cae72d905a
Author: Dan Wang <[email protected]>
AuthorDate: Thu Jan 25 13:56:06 2024 +0800
feat(new_metrics): build the http query string based on metric filters
(#1870)
Pegasus shell needs to fetch metrics by http client. However, building query
string is not simple. You have to write each field name yourself, and
assemble
all field names/values into a valid query string.
To simplify this operation, we could allow metric filters to support
exporting
the query string. Users could just set conditions they need to metric
filters,
then get the query string just by a function conveniently.
---
src/http/http_server.cpp | 26 ++++++++---
src/http/http_server.h | 8 +++-
src/utils/CMakeLists.txt | 5 ++-
src/utils/metrics.cpp | 36 ++++++++++++++-
src/utils/metrics.h | 11 ++++-
src/utils/strings.cpp | 11 +++++
src/utils/strings.h | 4 ++
src/utils/test/metrics_test.cpp | 97 +++++++++++++++++++++++++++++++++++++++++
src/utils/test/utils.cpp | 38 +++++++++++++++-
9 files changed, 221 insertions(+), 15 deletions(-)
diff --git a/src/http/http_server.cpp b/src/http/http_server.cpp
index aa628dac9..94de34bb7 100644
--- a/src/http/http_server.cpp
+++ b/src/http/http_server.cpp
@@ -57,6 +57,16 @@ error_s update_config(const http_request &req)
return update_flag(iter->first, iter->second);
}
+// If sub_path is 'app/duplication', the built path would be
'<root_path>/app/duplication'.
+std::string build_rel_path(const std::string &root_path, const std::string
&sub_path)
+{
+ std::string rel_path(root_path);
+ if (!rel_path.empty()) {
+ rel_path += '/';
+ }
+ return rel_path += sub_path;
+}
+
} // anonymous namespace
/*extern*/ http_call ®ister_http_call(std::string full_path)
@@ -73,18 +83,20 @@ error_s update_config(const http_request &req)
http_call_registry::instance().remove(full_path);
}
-void http_service::register_handler(std::string sub_path, http_callback cb,
std::string help)
+std::string http_service::get_rel_path(const std::string &sub_path) const
{
- CHECK(!sub_path.empty(), "");
+ return build_rel_path(path(), sub_path);
+}
+
+void http_service::register_handler(std::string sub_path, http_callback cb,
std::string help) const
+{
+ CHECK_FALSE(sub_path.empty());
if (!FLAGS_enable_http_server) {
return;
}
+
auto call = std::make_unique<http_call>();
- call->path = this->path();
- if (!call->path.empty()) {
- call->path += '/';
- }
- call->path += sub_path;
+ call->path = get_rel_path(sub_path);
call->callback = std::move(cb);
call->help = std::move(help);
http_call_registry::instance().add(std::move(call));
diff --git a/src/http/http_server.h b/src/http/http_server.h
index f6cb1628d..71fd8bde2 100644
--- a/src/http/http_server.h
+++ b/src/http/http_server.h
@@ -89,11 +89,17 @@ struct http_call
class http_service
{
public:
+ http_service() noexcept = default;
virtual ~http_service() = default;
virtual std::string path() const = 0;
- void register_handler(std::string sub_path, http_callback cb, std::string
help);
+ void register_handler(std::string sub_path, http_callback cb, std::string
help) const;
+
+private:
+ // If sub_path is 'app/duplication', the built path would be
'<root_path>/app/duplication',
+ // where path() would be called as root_path.
+ std::string get_rel_path(const std::string &sub_path) const;
};
class http_server_base : public http_service
diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt
index 1c1c7d0c4..337ac2591 100644
--- a/src/utils/CMakeLists.txt
+++ b/src/utils/CMakeLists.txt
@@ -36,14 +36,15 @@ set(MY_PROJ_LIBS
rocksdb
lz4
zstd
- snappy)
+ snappy
+ absl::strings)
# Extra files that will be installed
set(MY_BINPLACES "")
if (APPLE)
dsn_add_static_library()
- target_link_libraries(${MY_PROJ_NAME} PRIVATE dsn_http
dsn_replication_common)
+ target_link_libraries(${MY_PROJ_NAME} PRIVATE dsn_http
dsn_replication_common absl::strings)
else()
dsn_add_shared_library()
target_link_libraries(${MY_PROJ_NAME} PRIVATE dsn_replication_common)
diff --git a/src/utils/metrics.cpp b/src/utils/metrics.cpp
index 2cd8e70d0..681b6555c 100644
--- a/src/utils/metrics.cpp
+++ b/src/utils/metrics.cpp
@@ -18,6 +18,7 @@
#include "utils/metrics.h"
#include <absl/strings/string_view.h>
+#include <boost/algorithm/string/join.hpp>
#include <boost/asio/basic_deadline_timer.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/system/error_code.hpp>
@@ -230,6 +231,32 @@ void metric_filters::extract_entity_metrics(const
metric_entity::metric_map &can
}
}
+std::string metric_filters::to_query_string() const
+{
+#define COMBINE_FIELD_PAIR(name, container)
\
+ do {
\
+ if (container.empty()) {
\
+ break;
\
+ }
\
+
\
+ std::string pair(#name);
\
+ pair += '=';
\
+ pair += boost::join(container, ",");
\
+ fields.push_back(std::move(pair));
\
+ } while (0)
+
+ std::vector<std::string> fields;
+ COMBINE_FIELD_PAIR(with_metric_fields, with_metric_fields);
+ COMBINE_FIELD_PAIR(types, entity_types);
+ COMBINE_FIELD_PAIR(ids, entity_ids);
+ COMBINE_FIELD_PAIR(attributes, entity_attrs);
+ COMBINE_FIELD_PAIR(metrics, entity_metrics);
+
+#undef COMBINE_FIELD_PAIR
+
+ return boost::join(fields, "&");
+}
+
metric_entity_ptr metric_entity_prototype::instantiate(const std::string &id,
const
metric_entity::attr_map &attrs) const
{
@@ -245,14 +272,19 @@ metric_entity_prototype::metric_entity_prototype(const
char *name) : _name(name)
metric_entity_prototype::~metric_entity_prototype() {}
+const std::string metrics_http_service::kMetricsRootPath("");
+const std::string metrics_http_service::kMetricsQuerySubPath("metrics");
+const std::string
+ metrics_http_service::kMetricsQueryPath('/' +
metrics_http_service::kMetricsQuerySubPath);
+
metrics_http_service::metrics_http_service(metric_registry *registry) :
_registry(registry)
{
- register_handler("metrics",
+ register_handler(kMetricsQuerySubPath,
std::bind(&metrics_http_service::get_metrics_handler,
this,
std::placeholders::_1,
std::placeholders::_2),
- "ip:port/metrics");
+ fmt::format("ip:port{}", kMetricsQueryPath));
}
namespace {
diff --git a/src/utils/metrics.h b/src/utils/metrics.h
index abd7d5ef2..47ab3a720 100644
--- a/src/utils/metrics.h
+++ b/src/utils/metrics.h
@@ -509,6 +509,11 @@ struct metric_filters
void extract_entity_metrics(const metric_entity::metric_map &candidates,
metric_entity::metric_map &target_metrics)
const;
+ // Build the http query string based on metric filters. This is useful
when an http request
+ // is performed for metrics query: firstly, set metric filters with what
you want; then,
+ // get query string by this function conveniently and put it into the http
request.
+ std::string to_query_string() const;
+
// `with_metric_fields` includes all the metric fields that are wanted by
client. If it
// is empty, there will be no restriction: in other words, all fields
owned by the metric
// will be put in the response.
@@ -563,12 +568,16 @@ class metric_registry; // IWYU pragma: keep
class metrics_http_service : public http_server_base
{
public:
+ static const std::string kMetricsRootPath;
+ static const std::string kMetricsQuerySubPath;
+ static const std::string kMetricsQueryPath;
+
explicit metrics_http_service(metric_registry *registry);
~metrics_http_service() = default;
// There is only one API now whose URI is "/metrics", thus just make
// this URI as sub path while leaving the root path empty.
- std::string path() const override { return ""; }
+ std::string path() const override { return kMetricsRootPath; }
private:
friend void test_get_metrics_handler(const http_request &req,
http_response &resp);
diff --git a/src/utils/strings.cpp b/src/utils/strings.cpp
index 32e810ebd..c7c48a9c3 100644
--- a/src/utils/strings.cpp
+++ b/src/utils/strings.cpp
@@ -24,6 +24,7 @@
* THE SOFTWARE.
*/
+#include <absl/strings/ascii.h>
#include <openssl/md5.h>
#include <stdio.h>
#include <strings.h>
@@ -424,5 +425,15 @@ std::string find_string_prefix(const std::string &input,
char separator)
}
return input.substr(0, current);
}
+
+bool has_space(const std::string &str)
+{
+ // Use absl::ascii_isspace() instead of std::isspace(), which could not be
used as
+ // the predicate directly, since it might be implemented as a macro, and
its parameter
+ // must be declared as unsigned. Thus, to use std::isspace(), we have to
wrap it into
+ // a lambda expression.
+ return std::any_of(str.begin(), str.end(), absl::ascii_isspace);
+}
+
} // namespace utils
} // namespace dsn
diff --git a/src/utils/strings.h b/src/utils/strings.h
index 1759856f0..85d4a75a9 100644
--- a/src/utils/strings.h
+++ b/src/utils/strings.h
@@ -121,5 +121,9 @@ std::string string_md5(const char *buffer, unsigned int
length);
// splits the "input" string by the only character "separator" to get the
string prefix.
// if there is no prefix or the first character is "separator", it will return
"".
std::string find_string_prefix(const std::string &input, char separator);
+
+// Decide if there are some space characters in the given string, such as ' ',
'\r', '\n' or '\t'.
+bool has_space(const std::string &str);
+
} // namespace utils
} // namespace dsn
diff --git a/src/utils/test/metrics_test.cpp b/src/utils/test/metrics_test.cpp
index d1d33bf1c..cbacc6a7b 100644
--- a/src/utils/test/metrics_test.cpp
+++ b/src/utils/test/metrics_test.cpp
@@ -2887,6 +2887,103 @@ TEST(metrics_test, http_get_metrics)
}
}
+struct metric_filters_query_string_case
+{
+ metric_filters::metric_fields_type with_metric_fields;
+ metric_filters::entity_types_type entity_types;
+ metric_filters::entity_ids_type entity_ids;
+ metric_filters::entity_attrs_type entity_attrs;
+ metric_filters::entity_metrics_type entity_metrics;
+ size_t expected_fields;
+};
+
+class MetricFiltersQueryStringTest : public
testing::TestWithParam<metric_filters_query_string_case>
+{
+};
+
+const std::vector<metric_filters_query_string_case>
metric_filters_query_string_tests = {
+ // Empty query string.
+ {{}, {}, {}, {}, {}, 0},
+ // Some fields were missing in the query string.
+ {{"name", "value"}, {"replica"}, {}, {}, {"rdb_total_sst_files",
"rdb_total_sst_size_mb"}, 3},
+ // All fields were present.
+ {{"name", "value"},
+ {"replica"},
+ {"replica5.2"},
+ {"table_id", "partition_id"},
+ {"rdb_total_sst_files", "rdb_total_sst_size_mb"},
+ 5},
+};
+
+TEST_P(MetricFiltersQueryStringTest, BuildQueryString)
+{
+ const auto &query_string_case = GetParam();
+
+ metric_filters filters;
+
+#define COPY_CONTAINER(field) filters.field = query_string_case.field
+
+ // Copy the data of the case to the tested metric filters.
+ COPY_CONTAINER(with_metric_fields);
+ COPY_CONTAINER(entity_types);
+ COPY_CONTAINER(entity_ids);
+ COPY_CONTAINER(entity_attrs);
+ COPY_CONTAINER(entity_metrics);
+
+#undef COPY_CONTAINER
+
+ // Build the http query string based on the tested metric filters.
+ const auto &query_string = filters.to_query_string();
+ std::cout << "query string: " << query_string << std::endl;
+
+ // There should not be any space character in the query string.
+ ASSERT_FALSE(utils::has_space(query_string));
+
+ // Extract each field name/value pair from the query string and check the
number of
+ // the fields.
+ std::vector<std::string> fields;
+ utils::split_args(query_string.c_str(), fields, '&');
+ ASSERT_EQ(query_string_case.expected_fields, fields.size());
+
+ size_t i = 0;
+
+#define CHECK_FIELD(name, type, field)
\
+ do {
\
+ if (query_string_case.field.empty()) {
\
+ break;
\
+ }
\
+
\
+ ASSERT_LT(i, query_string_case.expected_fields);
\
+
\
+ std::vector<std::string> kvs;
\
+ utils::split_args(fields[i].c_str(), kvs, '=');
\
+ ASSERT_EQ(2, kvs.size());
\
+ ASSERT_STREQ(#name, kvs[0].c_str());
\
+
\
+ type actual_field;
\
+ utils::split_args(kvs[1].c_str(), actual_field, ',');
\
+ ASSERT_EQ(query_string_case.field, actual_field);
\
+ ++i;
\
+ } while (0)
+
+ // Check each field name/value extracted from the query string is exactly
equal to the
+ // original metric filters.
+ CHECK_FIELD(with_metric_fields, metric_filters::metric_fields_type,
with_metric_fields);
+ CHECK_FIELD(types, metric_filters::entity_types_type, entity_types);
+ CHECK_FIELD(ids, metric_filters::entity_ids_type, entity_ids);
+ CHECK_FIELD(attributes, metric_filters::entity_attrs_type, entity_attrs);
+ CHECK_FIELD(metrics, metric_filters::entity_metrics_type, entity_metrics);
+
+#undef CHECK_FIELD
+
+ // All of the fields should have been checked.
+ ASSERT_EQ(query_string_case.expected_fields, i);
+}
+
+INSTANTIATE_TEST_SUITE_P(MetricsTest,
+ MetricFiltersQueryStringTest,
+ testing::ValuesIn(metric_filters_query_string_tests));
+
using surviving_metrics_case = std::tuple<std::string, bool, bool, bool, bool>;
class MetricsRetirementTest : public
testing::TestWithParam<surviving_metrics_case>
diff --git a/src/utils/test/utils.cpp b/src/utils/test/utils.cpp
index 311b04c51..cd5708750 100644
--- a/src/utils/test/utils.cpp
+++ b/src/utils/test/utils.cpp
@@ -45,8 +45,8 @@
#include "utils/strings.h"
#include "utils/utils.h"
-using namespace ::dsn;
-using namespace ::dsn::utils;
+namespace dsn {
+namespace utils {
TEST(core, get_last_component)
{
@@ -568,3 +568,37 @@ TEST(core, get_intersection)
ASSERT_EQ(intersection.size(), 1);
ASSERT_EQ(*intersection.begin(), 3);
}
+
+struct has_space_case
+{
+ std::string str;
+ bool expected_has_space;
+};
+
+class HasSpaceTest : public testing::TestWithParam<has_space_case>
+{
+};
+
+TEST_P(HasSpaceTest, HasSpace)
+{
+ const auto &space_case = GetParam();
+ EXPECT_EQ(space_case.expected_has_space, has_space(space_case.str));
+}
+
+const std::vector<has_space_case> has_space_tests = {
+ {"abc xyz", true},
+ {" abcxyz", true},
+ {"abcxyz ", true},
+ {"abc xyz", true},
+ {"abc\r\nxyz", true},
+ {"abc\r\nxyz", true},
+ {"abc\txyz", true},
+ {"abc\txyz", true},
+ {"abcxyz", false},
+ {"abc_xyz", false},
+};
+
+INSTANTIATE_TEST_SUITE_P(StringTest, HasSpaceTest,
testing::ValuesIn(has_space_tests));
+
+} // namespace utils
+} // namespace dsn
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]