2 * This file is open source software, licensed to you under the terms
3 * of the Apache License, Version 2.0 (the "License"). See the NOTICE file
4 * distributed with this work for additional information regarding copyright
5 * ownership. You may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing,
12 * software distributed under the License is distributed on an
13 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 * KIND, either express or implied. See the License for the
15 * specific language governing permissions and limitations
19 * Copyright (C) 2016 ScyllaDB
22 #include <seastar/core/prometheus.hh>
25 #include <seastar/core/scollectd_api.hh>
26 #include "core/scollectd-impl.hh"
27 #include <seastar/core/metrics_api.hh>
28 #include <seastar/http/function_handlers.hh>
29 #include <boost/algorithm/string/replace.hpp>
30 #include <boost/range/algorithm_ext/erase.hpp>
31 #include <boost/algorithm/string.hpp>
32 #include <boost/range/algorithm.hpp>
33 #include <boost/range/combine.hpp>
34 #include <seastar/core/thread.hh>
35 #include <seastar/core/loop.hh>
40 extern seastar::logger seastar_logger
;
42 namespace prometheus
{
44 namespace mi
= metrics::impl
;
46 static std::string
to_str(seastar::metrics::impl::data_type dt
) {
48 case seastar::metrics::impl::data_type::GAUGE
:
50 case seastar::metrics::impl::data_type::COUNTER
:
51 case seastar::metrics::impl::data_type::REAL_COUNTER
:
53 case seastar::metrics::impl::data_type::HISTOGRAM
:
55 case seastar::metrics::impl::data_type::SUMMARY
:
61 static std::string
to_str(const seastar::metrics::impl::metric_value
& v
) {
63 case seastar::metrics::impl::data_type::GAUGE
:
64 case seastar::metrics::impl::data_type::REAL_COUNTER
:
65 return std::to_string(v
.d());
66 case seastar::metrics::impl::data_type::COUNTER
:
67 return std::to_string(v
.i());
68 case seastar::metrics::impl::data_type::HISTOGRAM
:
69 case seastar::metrics::impl::data_type::SUMMARY
:
72 return ""; // we should never get here but it makes the compiler happy
75 static void add_name(std::ostream
& s
, const sstring
& name
, const std::map
<sstring
, sstring
>& labels
, const config
& ctx
) {
77 const char* delimiter
= "";
79 s
<< ctx
.label
->key() << "=\"" << ctx
.label
->value() << '"';
83 if (!labels
.empty()) {
84 for (auto l
: labels
) {
86 s
<< l
.first
<< "=\"" << l
.second
<< '"';
95 * \brief iterator for metric family
97 * In prometheus, a single shard collecct all the data from the other
98 * shards and report it.
100 * Each shard returns a value_copy struct that has a vector of vector values (a vector per metric family)
101 * and a vector of metadata (and insdie it a vector of metric metadata)
103 * The metrics are sorted by the metric family name.
105 * In prometheus, all the metrics that belongs to the same metric family are reported together.
107 * For efficiency the results from the metrics layer are kept in a vector.
109 * So we have a vector of shards of a vector of metric families of a vector of values.
111 * To produce the result, we use the metric_family_iterator that is created by metric_family_range.
113 * When iterating over the metrics we use two helper structure.
115 * 1. A map between metric family name and the total number of values (combine on all shards) and
116 * pointer to the metric family metadata.
117 * 2. A vector of positions to the current metric family for each shard.
119 * The metric_family_range returns a metric_family_iterator that goes over all the families.
121 * The iterator returns a metric_family object, that can report the metric_family name, the size (how many
122 * metrics in total belongs to the metric family) and a a foreach_metric method.
124 * The foreach_metric method can be used to perform an action on each of the metric that belongs to
127 * Iterating over the metrics is done:
128 * - go over each of the shard and each of the entry in the position vector:
129 * - if the current family (the metric family that we get from the shard and position) has the current name:
130 * - iterate over each of the metrics belong to that metric family:
132 * for example, if m is a metric_family_range
134 * for (auto&& i : m) {
135 * std::cout << i.name() << std::endl;
136 * i.foreach_metric([](const mi::metric_value& value, const mi::metric_info& value_info) {
137 * std::cout << value_info.id.labels().size() <<std::cout;
141 * Will print all the metric family names followed by the number of labels each metric has.
143 class metric_family_iterator
;
145 class metric_family_range
;
147 class metrics_families_per_shard
{
148 using metrics_family_per_shard_data_container
= std::vector
<foreign_ptr
<mi::values_reference
>>;
149 metrics_family_per_shard_data_container _data
;
150 using comp_function
= std::function
<bool(const sstring
&, const mi::metric_family_metadata
&)>;
152 * \brief find the last item in a range of metric family based on a comparator function
155 metric_family_iterator
find_bound(const sstring
& family_name
, comp_function comp
) const;
159 using const_iterator
= metrics_family_per_shard_data_container::const_iterator
;
160 using iterator
= metrics_family_per_shard_data_container::iterator
;
161 using reference
= metrics_family_per_shard_data_container::reference
;
162 using const_reference
= metrics_family_per_shard_data_container::const_reference
;
165 * \brief find the first item following a metric family range.
166 * metric family are sorted, this will return the first item that is outside
169 metric_family_iterator
upper_bound(const sstring
& family_name
) const;
172 * \brief find the first item in a range of metric family.
173 * metric family are sorted, the first item, is the first to match the
176 metric_family_iterator
lower_bound(const sstring
& family_name
) const;
179 * \defgroup Variables Global variables
183 * @defgroup Vector properties
184 * The following methods making metrics_families_per_shard act as
185 * a vector of foreign_ptr<mi::values_reference>
191 return _data
.begin();
198 const_iterator
begin() const {
199 return _data
.begin();
202 const_iterator
end() const {
206 void resize(size_t new_size
) {
207 _data
.resize(new_size
);
210 reference
& operator[](size_t n
) {
214 const_reference
& operator[](size_t n
) const {
220 static future
<> get_map_value(metrics_families_per_shard
& vec
) {
221 vec
.resize(smp::count
);
222 return parallel_for_each(boost::irange(0u, smp::count
), [&vec
] (auto cpu
) {
223 return smp::submit_to(cpu
, [] {
224 return mi::get_values();
225 }).then([&vec
, cpu
] (auto res
) {
226 vec
[cpu
] = std::move(res
);
233 * \brief a facade class for metric family
235 class metric_family
{
236 const sstring
* _name
= nullptr;
238 const mi::metric_family_info
* _family_info
= nullptr;
239 metric_family_iterator
& _iterator_state
;
240 metric_family(metric_family_iterator
& state
) : _iterator_state(state
) {
242 metric_family(const sstring
* name
, uint32_t size
, const mi::metric_family_info
* family_info
, metric_family_iterator
& state
) :
243 _name(name
), _size(size
), _family_info(family_info
), _iterator_state(state
) {
245 metric_family(const metric_family
& info
, metric_family_iterator
& state
) :
246 metric_family(info
._name
, info
._size
, info
._family_info
, state
) {
249 metric_family(const metric_family
&) = delete;
250 metric_family(metric_family
&&) = delete;
252 const sstring
& name() const {
256 const uint32_t size() const {
260 const mi::metric_family_info
& metadata() const {
261 return *_family_info
;
264 void foreach_metric(std::function
<void(const mi::metric_value
&, const mi::metric_info
&)>&& f
);
267 return !_name
|| !_family_info
;
269 friend class metric_family_iterator
;
272 class metric_family_iterator
{
273 const metrics_families_per_shard
& _families
;
274 std::vector
<size_t> _positions
;
278 if (_positions
.empty()) {
281 const sstring
*new_name
= nullptr;
282 const mi::metric_family_info
* new_family_info
= nullptr;
284 for (auto&& i
: boost::combine(_positions
, _families
)) {
285 auto& pos_in_metric_per_shard
= boost::get
<0>(i
);
286 auto& metric_family
= boost::get
<1>(i
);
287 if (_info
._name
&& pos_in_metric_per_shard
< metric_family
->metadata
->size() &&
288 metric_family
->metadata
->at(pos_in_metric_per_shard
).mf
.name
.compare(*_info
._name
) <= 0) {
289 pos_in_metric_per_shard
++;
291 if (pos_in_metric_per_shard
>= metric_family
->metadata
->size()) {
292 // no more metric family in this shard
295 auto& metadata
= metric_family
->metadata
->at(pos_in_metric_per_shard
);
296 int cmp
= (!new_name
) ? -1 : metadata
.mf
.name
.compare(*new_name
);
298 new_name
= &metadata
.mf
.name
;
299 new_family_info
= &metadata
.mf
;
303 _info
._size
+= metadata
.metrics
.size();
306 _info
._name
= new_name
;
307 _info
._family_info
= new_family_info
;
311 metric_family_iterator() = delete;
312 metric_family_iterator(const metric_family_iterator
& o
) : _families(o
._families
), _positions(o
._positions
), _info(*this) {
316 metric_family_iterator(metric_family_iterator
&& o
) : _families(o
._families
), _positions(std::move(o
._positions
)),
321 metric_family_iterator(const metrics_families_per_shard
& families
,
323 : _families(families
), _positions(shards
, 0), _info(*this) {
327 metric_family_iterator(const metrics_families_per_shard
& families
,
328 std::vector
<size_t>&& positions
)
329 : _families(families
), _positions(std::move(positions
)), _info(*this) {
333 metric_family_iterator
& operator++() {
338 metric_family_iterator
operator++(int) {
339 metric_family_iterator
previous(*this);
344 bool operator!=(const metric_family_iterator
& o
) const {
345 return !(*this == o
);
348 bool operator==(const metric_family_iterator
& o
) const {
355 return name() == o
.name();
358 metric_family
& operator*() {
362 metric_family
* operator->() {
365 const sstring
& name() const {
369 const uint32_t size() const {
373 const mi::metric_family_info
& metadata() const {
374 return *_info
._family_info
;
378 return _positions
.empty() || _info
.end();
381 void foreach_metric(std::function
<void(const mi::metric_value
&, const mi::metric_info
&)>&& f
) {
382 // iterating over the shard vector and the position vector
383 for (auto&& i
: boost::combine(_positions
, _families
)) {
384 auto& pos_in_metric_per_shard
= boost::get
<0>(i
);
385 auto& metric_family
= boost::get
<1>(i
);
386 if (pos_in_metric_per_shard
>= metric_family
->metadata
->size()) {
387 // no more metric family in this shard
390 auto& metadata
= metric_family
->metadata
->at(pos_in_metric_per_shard
);
391 // the the name is different, that means that on this shard, the metric family
392 // does not exist, because everything is sorted by metric family name, this is fine.
393 if (metadata
.mf
.name
== name()) {
394 const mi::value_vector
& values
= metric_family
->values
[pos_in_metric_per_shard
];
395 const mi::metric_metadata_vector
& metrics_metadata
= metadata
.metrics
;
396 for (auto&& vm
: boost::combine(values
, metrics_metadata
)) {
397 auto& value
= boost::get
<0>(vm
);
398 auto& metric_metadata
= boost::get
<1>(vm
);
399 f(value
, metric_metadata
);
407 void metric_family::foreach_metric(std::function
<void(const mi::metric_value
&, const mi::metric_info
&)>&& f
) {
408 _iterator_state
.foreach_metric(std::move(f
));
411 class metric_family_range
{
412 metric_family_iterator _begin
;
413 metric_family_iterator _end
;
415 metric_family_range(const metrics_families_per_shard
& families
) : _begin(families
, smp::count
),
416 _end(metric_family_iterator(families
, 0))
420 metric_family_range(const metric_family_iterator
& b
, const metric_family_iterator
& e
) : _begin(b
), _end(e
)
424 metric_family_iterator
begin() const {
428 metric_family_iterator
end() const {
433 metric_family_iterator
metrics_families_per_shard::find_bound(const sstring
& family_name
, comp_function comp
) const {
434 std::vector
<size_t> positions
;
435 positions
.reserve(smp::count
);
437 for (auto& shard_info
: _data
) {
438 std::vector
<mi::metric_family_metadata
>& metadata
= *(shard_info
->metadata
);
439 std::vector
<mi::metric_family_metadata
>::iterator it_b
= boost::range::upper_bound(metadata
, family_name
, comp
);
440 positions
.emplace_back(it_b
- metadata
.begin());
442 return metric_family_iterator(*this, std::move(positions
));
446 metric_family_iterator
metrics_families_per_shard::lower_bound(const sstring
& family_name
) const {
447 return find_bound(family_name
, [](const sstring
& a
, const mi::metric_family_metadata
& b
) {
448 //sstring doesn't have a <= operator
449 return a
< b
.mf
.name
|| a
== b
.mf
.name
;
453 metric_family_iterator
metrics_families_per_shard::upper_bound(const sstring
& family_name
) const {
454 return find_bound(family_name
, [](const sstring
& a
, const mi::metric_family_metadata
& b
) {
455 return a
< b
.mf
.name
;
460 * \brief a helper function to get metric family range
461 * if metric_family_name is empty will return everything, if not, it will return
462 * the range of metric family that match the metric_family_name.
464 * if prefix is true the match will be based on prefix
466 metric_family_range
get_range(const metrics_families_per_shard
& mf
, const sstring
& metric_family_name
, bool prefix
) {
467 if (metric_family_name
== "") {
468 return metric_family_range(mf
);
470 auto upper_bount_prefix
= metric_family_name
;
471 ++upper_bount_prefix
.back();
473 return metric_family_range(mf
.lower_bound(metric_family_name
), mf
.lower_bound(upper_bount_prefix
));
475 auto lb
= mf
.lower_bound(metric_family_name
);
476 if (lb
.end() || lb
->name() != metric_family_name
) {
477 return metric_family_range(lb
, lb
); // just return an empty range
481 return metric_family_range(lb
, up
);
485 void write_histogram(std::stringstream
& s
, const config
& ctx
, const sstring
& name
, const seastar::metrics::histogram
& h
, std::map
<sstring
, sstring
> labels
) noexcept
{
486 add_name(s
, name
+ "_sum", labels
, ctx
);
487 s
<< h
.sample_sum
<< '\n';
489 add_name(s
, name
+ "_count", labels
, ctx
);
490 s
<< h
.sample_count
<< '\n';
492 auto& le
= labels
["le"];
493 auto bucket
= name
+ "_bucket";
494 for (auto i
: h
.buckets
) {
495 le
= std::to_string(i
.upper_bound
);
496 add_name(s
, bucket
, labels
, ctx
);
497 s
<< i
.count
<< '\n';
499 labels
["le"] = "+Inf";
500 add_name(s
, bucket
, labels
, ctx
);
501 s
<< h
.sample_count
<< '\n';
504 void write_summary(std::stringstream
& s
, const config
& ctx
, const sstring
& name
, const seastar::metrics::histogram
& h
, std::map
<sstring
, sstring
> labels
) noexcept
{
506 add_name(s
, name
+ "_sum", labels
, ctx
);
507 s
<< h
.sample_sum
<< '\n';
509 if (h
.sample_count
) {
510 add_name(s
, name
+ "_count", labels
, ctx
);
511 s
<< h
.sample_count
<< '\n';
513 auto& le
= labels
["quantile"];
514 for (auto i
: h
.buckets
) {
515 le
= std::to_string(i
.upper_bound
);
516 add_name(s
, name
, labels
, ctx
);
517 s
<< i
.count
<< '\n';
521 * \brief a helper class to aggregate metrics over labels
523 * This class sum multiple metrics based on a list of labels.
524 * It returns one or more metrics each aggregated by the aggregate_by labels.
526 * To use it, you define what labels it should aggregate by and then pass to
527 * it metrics with their labels.
528 * For example if a metrics has a 'shard' and 'name' labels and you aggregate by 'shard'
529 * it would return a map of metrics each with only the 'name' label
532 class metric_aggregate_by_labels
{
533 std::vector
<std::string
> _labels_to_aggregate_by
;
534 std::unordered_map
<std::map
<sstring
, sstring
>, seastar::metrics::impl::metric_value
> _values
;
536 metric_aggregate_by_labels(std::vector
<std::string
> labels
) : _labels_to_aggregate_by(std::move(labels
)) {
539 * \brief add a metric
541 * This method gets a metric and its labels and adds it to the aggregated metric.
542 * For example, if a metric has the labels {'shard':'0', 'name':'myhist'} and we are aggregating
544 * The metric would be added to the aggregated metric with labels {'name':'myhist'}.
547 void add(const seastar::metrics::impl::metric_value
& m
, std::map
<sstring
, sstring
> labels
) noexcept
{
548 for (auto&& l
: _labels_to_aggregate_by
) {
551 std::unordered_map
<std::map
<sstring
, sstring
>, seastar::metrics::impl::metric_value
>::iterator i
= _values
.find(labels
);
552 if ( i
== _values
.end()) {
553 _values
.emplace(std::move(labels
), m
);
558 const std::unordered_map
<std::map
<sstring
, sstring
>, seastar::metrics::impl::metric_value
>& get_values() const noexcept
{
561 bool empty() const noexcept
{
562 return _values
.empty();
566 std::string
get_value_as_string(std::stringstream
& s
, const mi::metric_value
& value
) noexcept
{
567 std::string value_str
;
569 value_str
= to_str(value
);
570 } catch (const std::range_error
& e
) {
571 seastar_logger
.debug("prometheus: get_value_as_string: {}: {}", s
.str(), e
.what());
574 auto ex
= std::current_exception();
575 // print this error as it's ignored later on by `connection::start_response`
576 seastar_logger
.error("prometheus: get_value_as_string: {}: {}", s
.str(), ex
);
577 std::rethrow_exception(std::move(ex
));
582 future
<> write_text_representation(output_stream
<char>& out
, const config
& ctx
, const metric_family_range
& m
, bool show_help
, std::function
<bool(const mi::labels_type
&)> filter
) {
583 return seastar::async([&ctx
, &out
, &m
, show_help
, filter
] () mutable {
586 for (metric_family
& metric_family
: m
) {
587 auto name
= ctx
.prefix
+ "_" + metric_family
.name();
589 metric_aggregate_by_labels
aggregated_values(metric_family
.metadata().aggregate_labels
);
590 bool should_aggregate
= !metric_family
.metadata().aggregate_labels
.empty();
591 metric_family
.foreach_metric([&s
, &out
, &ctx
, &found
, &name
, &metric_family
, &aggregated_values
, should_aggregate
, show_help
, &filter
](auto value
, auto value_info
) mutable {
594 if ((value_info
.should_skip_when_empty
&& value
.is_empty()) || !filter(value_info
.id
.labels())) {
598 if (show_help
&& metric_family
.metadata().d
.str() != "") {
599 s
<< "# HELP " << name
<< " " << metric_family
.metadata().d
.str() << '\n';
601 s
<< "# TYPE " << name
<< " " << to_str(metric_family
.metadata().type
) << '\n';
604 if (should_aggregate
) {
605 aggregated_values
.add(value
, value_info
.id
.labels());
606 } else if (value
.type() == mi::data_type::SUMMARY
) {
607 write_summary(s
, ctx
, name
, value
.get_histogram(), value_info
.id
.labels());
608 } else if (value
.type() == mi::data_type::HISTOGRAM
) {
609 write_histogram(s
, ctx
, name
, value
.get_histogram(), value_info
.id
.labels());
611 add_name(s
, name
, value_info
.id
.labels(), ctx
);
612 s
<< get_value_as_string(s
, value
) << '\n';
614 out
.write(s
.str()).get();
615 thread::maybe_yield();
617 if (!aggregated_values
.empty()) {
618 for (auto&& h
: aggregated_values
.get_values()) {
621 if (h
.second
.type() == mi::data_type::HISTOGRAM
) {
622 write_histogram(s
, ctx
, name
, h
.second
.get_histogram(), h
.first
);
624 add_name(s
, name
, h
.first
, ctx
);
625 s
<< get_value_as_string(s
, h
.second
) << '\n';
627 out
.write(s
.str()).get();
628 thread::maybe_yield();
635 class metrics_handler
: public handler_base
{
638 static std::function
<bool(const mi::labels_type
&)> _true_function
;
641 * \brief tries to trim an asterisk from the end of the string
642 * return true if an asterisk exists.
644 bool trim_asterisk(sstring
& name
) {
645 if (name
.size() && name
.back() == '*') {
646 name
.resize(name
.length() - 1);
649 // Prometheus uses url encoding for the path so '*' is encoded as '%2A'
650 if (boost::algorithm::ends_with(name
, "%2A")) {
651 // This assert is obviously true. It is in here just to
652 // silence a bogus gcc warning:
653 // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=89337
654 assert(name
.length() >= 3);
655 name
.resize(name
.length() - 3);
661 * \brief Return a filter function, based on the request
663 * A filter function filter what metrics should be included.
664 * It returns true if a metric should be included, or false otherwise.
665 * The filters are created from the request query parameters.
667 std::function
<bool(const mi::labels_type
&)> make_filter(const http::request
& req
) {
668 std::unordered_map
<sstring
, std::regex
> matcher
;
669 auto labels
= mi::get_local_impl()->get_labels();
670 for (auto&& qp
: req
.query_parameters
) {
671 if (labels
.find(qp
.first
) != labels
.end()) {
672 matcher
.emplace(qp
.first
, std::regex(qp
.second
.c_str()));
675 return (matcher
.empty()) ? _true_function
: [matcher
](const mi::labels_type
& labels
) {
676 for (auto&& m
: matcher
) {
677 auto l
= labels
.find(m
.first
);
678 if (!std::regex_match((l
== labels
.end())? "" : l
->second
.c_str(), m
.second
)) {
687 metrics_handler(config ctx
) : _ctx(ctx
) {}
689 future
<std::unique_ptr
<http::reply
>> handle(const sstring
& path
,
690 std::unique_ptr
<http::request
> req
, std::unique_ptr
<http::reply
> rep
) override
{
691 sstring metric_family_name
= req
->get_query_param("__name__");
692 bool prefix
= trim_asterisk(metric_family_name
);
693 bool show_help
= req
->get_query_param("__help__") != "false";
694 std::function
<bool(const mi::labels_type
&)> filter
= make_filter(*req
);
696 rep
->write_body("txt", [this, metric_family_name
, prefix
, show_help
, filter
] (output_stream
<char>&& s
) {
697 return do_with(metrics_families_per_shard(), output_stream
<char>(std::move(s
)),
698 [this, prefix
, &metric_family_name
, show_help
, filter
] (metrics_families_per_shard
& families
, output_stream
<char>& s
) mutable {
699 return get_map_value(families
).then([&s
, &families
, this, prefix
, &metric_family_name
, show_help
, filter
]() mutable {
700 return do_with(get_range(families
, metric_family_name
, prefix
),
701 [&s
, this, show_help
, filter
](metric_family_range
& m
) {
702 return write_text_representation(s
, _ctx
, m
, show_help
, filter
);
704 }).finally([&s
] () mutable {
709 return make_ready_future
<std::unique_ptr
<http::reply
>>(std::move(rep
));
713 std::function
<bool(const mi::labels_type
&)> metrics_handler::_true_function
= [](const mi::labels_type
&) {
717 future
<> add_prometheus_routes(http_server
& server
, config ctx
) {
718 server
._routes
.put(GET
, "/metrics", new metrics_handler(ctx
));
719 return make_ready_future
<>();
722 future
<> add_prometheus_routes(distributed
<http_server
>& server
, config ctx
) {
723 return server
.invoke_on_all([ctx
](http_server
& s
) {
724 return add_prometheus_routes(s
, ctx
);
728 future
<> start(httpd::http_server_control
& http_server
, config ctx
) {
729 return add_prometheus_routes(http_server
.server(), ctx
);