-
Notifications
You must be signed in to change notification settings - Fork 5.5k
prometheus stats: Correctly group lines of the same metric name. #10833
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
Changes from 8 commits
d790da2
48b0f06
10e06ac
6778f66
e4d0374
a6ba179
1e4d334
b7f3fac
c2eb4fb
8aee0a6
836783a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -190,52 +190,110 @@ uint64_t PrometheusStatsFormatter::statsAsPrometheus( | |
| const std::vector<Stats::GaugeSharedPtr>& gauges, | ||
| const std::vector<Stats::ParentHistogramSharedPtr>& histograms, Buffer::Instance& response, | ||
| const bool used_only, const absl::optional<std::regex>& regex) { | ||
| std::unordered_set<std::string> metric_type_tracker; | ||
| for (const auto& counter : counters) { | ||
| if (!shouldShowMetric(*counter, used_only, regex)) { | ||
| continue; | ||
| } | ||
|
|
||
| const std::string tags = formattedTags(counter->tags()); | ||
| const std::string metric_name = metricName(counter->tagExtractedName()); | ||
| if (metric_type_tracker.find(metric_name) == metric_type_tracker.end()) { | ||
| metric_type_tracker.insert(metric_name); | ||
| response.add(fmt::format("# TYPE {0} counter\n", metric_name)); | ||
| /* | ||
| * From | ||
| * https:*github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#grouping-and-sorting: | ||
| * | ||
| * All lines for a given metric must be provided as one single group, with the optional HELP and | ||
| * TYPE lines first (in no particular order). Beyond that, reproducible sorting in repeated | ||
| * expositions is preferred but not required, i.e. do not sort if the computational cost is | ||
| * prohibitive. | ||
| */ | ||
|
|
||
| /** | ||
| * Processes a metric type (counter, gauge, histogram) by generating all output lines, sorting | ||
| * them by tag-extracted metric name, and then outputting them in the correct sorted order into | ||
| * response. | ||
| * | ||
| * @param metrics A vector of Stats::RefcountPtr to a metric type. | ||
| * @param generate_output A std::function<std::string(const MetricType& metric, const std::string& | ||
| * prefixedTagExtractedName)> which returns the output text for this metric. | ||
| */ | ||
| auto process_type = [&response, ®ex, used_only](const auto& metrics, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was wondering whether you could use templates in some way to reduce the use of See https://google.github.io/styleguide/cppguide.html#Type_deduction for discussion. I think my concrete suggestion is to break up most of this code into a private helper method I think that might be clearer than using the lambda with the auto...WDYT? That would also remove the need for the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both I can make the helpers into template methods instead of a lambda, and I agree that may make this a little easier to follow. Some of the types will become more concrete.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think once you make the helper into an explicit template method you'll be able to drop the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it came out better than I expected, although I had to explicitly specify template parameters in some cases that were unexpected to me. But I think it is more readable. |
||
| const auto& generate_output, | ||
| absl::string_view type) -> uint64_t { | ||
| // Get the inner element type (MetricType) from a `const | ||
| // std::vector<Stats::MetricTypeSharedPtr>&` | ||
| using MetricType = | ||
| typename std::remove_reference<decltype(metrics)>::type::value_type::element_type; | ||
|
|
||
| struct MetricLessThan { | ||
| bool operator()(const MetricType* a, const MetricType* b) const { | ||
| ASSERT(&a->constSymbolTable() == &b->constSymbolTable()); | ||
| return a->constSymbolTable().lessThan(a->statName(), b->statName()); | ||
| } | ||
| }; | ||
|
|
||
| // This is an unsorted collection of dumb-pointers (no need to increment then decrement every | ||
| // refcount; ownership is held throughout by `metrics`). It is unsorted for efficiency, but will | ||
| // be sorted before producing the final output to satisfy the "preferred" ordering from the | ||
| // prometheus spec: metrics will be sorted by their tags' textual representation, which will be | ||
| // consistent across calls. | ||
| using MetricTypeUnsortedCollection = std::vector<const MetricType*>; | ||
|
|
||
| // Return early to avoid crashing when getting the symbol table from the first metric. | ||
| if (metrics.empty()) { | ||
| return 0; | ||
| } | ||
| response.add(fmt::format("{0}{{{1}}} {2}\n", metric_name, tags, counter->value())); | ||
| } | ||
|
|
||
| for (const auto& gauge : gauges) { | ||
| if (!shouldShowMetric(*gauge, used_only, regex)) { | ||
| continue; | ||
| } | ||
| // There should only be one symbol table for all of the stats in the admin | ||
| // interface. If this assumption changes, the name comparisons in this function | ||
| // will have to change to compare to convert all StatNames to strings before | ||
| // comparison. | ||
| const Stats::SymbolTable& global_symbol_table = metrics.front()->constSymbolTable(); | ||
|
|
||
| const std::string tags = formattedTags(gauge->tags()); | ||
| const std::string metric_name = metricName(gauge->tagExtractedName()); | ||
| if (metric_type_tracker.find(metric_name) == metric_type_tracker.end()) { | ||
| metric_type_tracker.insert(metric_name); | ||
| response.add(fmt::format("# TYPE {0} gauge\n", metric_name)); | ||
| } | ||
| response.add(fmt::format("{0}{{{1}}} {2}\n", metric_name, tags, gauge->value())); | ||
| } | ||
| // Sorted collection of metrics sorted by their tagExtractedName, to satisfy the requirements | ||
| // of the exposition format. | ||
| std::map<Stats::StatName, MetricTypeUnsortedCollection, Stats::StatNameLessThan> groups( | ||
| global_symbol_table); | ||
|
|
||
| for (const auto& histogram : histograms) { | ||
| if (!shouldShowMetric(*histogram, used_only, regex)) { | ||
| continue; | ||
| } | ||
| for (const auto& metric : metrics) { | ||
| ASSERT(&global_symbol_table == &metric->constSymbolTable()); | ||
|
|
||
| const std::string tags = formattedTags(histogram->tags()); | ||
| const std::string hist_tags = histogram->tags().empty() ? EMPTY_STRING : (tags + ","); | ||
| if (!shouldShowMetric(*metric, used_only, regex)) { | ||
| continue; | ||
| } | ||
|
|
||
| const std::string metric_name = metricName(histogram->tagExtractedName()); | ||
| if (metric_type_tracker.find(metric_name) == metric_type_tracker.end()) { | ||
| metric_type_tracker.insert(metric_name); | ||
| response.add(fmt::format("# TYPE {0} histogram\n", metric_name)); | ||
| groups[metric->tagExtractedStatName()].push_back(metric.get()); | ||
| } | ||
|
|
||
| const Stats::HistogramStatistics& stats = histogram->cumulativeStatistics(); | ||
| for (auto& group : groups) { | ||
| const std::string metric_name = metricName(global_symbol_table.toString(group.first)); | ||
| response.add(fmt::format("# TYPE {0} {1}\n", metric_name, type)); | ||
|
|
||
| // Sort before producing the final output to satisfy the "preferred" ordering from the | ||
| // prometheus spec: metrics will be sorted by their tags' textual representation, which will | ||
| // be consistent across calls. | ||
| std::sort(group.second.begin(), group.second.end(), MetricLessThan()); | ||
|
|
||
| for (const auto& metric : group.second) { | ||
| response.add(generate_output(*metric, metric_name)); | ||
| } | ||
| response.add("\n"); | ||
| } | ||
| return groups.size(); | ||
| }; | ||
|
|
||
| // Returns the prometheus output line for a counter or a gauge. | ||
| auto generate_counter_and_gauge_output = [](const auto& metric, | ||
| const std::string& metric_name) -> std::string { | ||
| const std::string tags = formattedTags(metric.tags()); | ||
| return fmt::format("{0}{{{1}}} {2}\n", metric_name, tags, metric.value()); | ||
| }; | ||
|
|
||
| // Returns the prometheus output for a histogram. The output is a multi-line string (with embedded | ||
| // newlines) that contains all the individual bucket counts and sum/count for a single histogram | ||
| // (metric_name plus all tags). | ||
| auto generate_histogram_output = [](const Stats::ParentHistogram& histogram, | ||
| const std::string& metric_name) -> std::string { | ||
| const std::string tags = formattedTags(histogram.tags()); | ||
| const std::string hist_tags = histogram.tags().empty() ? EMPTY_STRING : (tags + ","); | ||
|
|
||
| const Stats::HistogramStatistics& stats = histogram.cumulativeStatistics(); | ||
| const std::vector<double>& supported_buckets = stats.supportedBuckets(); | ||
| const std::vector<uint64_t>& computed_buckets = stats.computedBuckets(); | ||
| std::string output; | ||
| for (size_t i = 0; i < supported_buckets.size(); ++i) { | ||
| double bucket = supported_buckets[i]; | ||
| uint64_t value = computed_buckets[i]; | ||
|
|
@@ -244,18 +302,25 @@ uint64_t PrometheusStatsFormatter::statsAsPrometheus( | |
| // 'g' operator which prints the number in general fixed point format or scientific format | ||
| // with precision 50 to round the number up to 32 significant digits in fixed point format | ||
| // which should cover pretty much all cases | ||
| response.add(fmt::format("{0}_bucket{{{1}le=\"{2:.32g}\"}} {3}\n", metric_name, hist_tags, | ||
| bucket, value)); | ||
| output.append(fmt::format("{0}_bucket{{{1}le=\"{2:.32g}\"}} {3}\n", metric_name, hist_tags, | ||
| bucket, value)); | ||
| } | ||
|
|
||
| response.add(fmt::format("{0}_bucket{{{1}le=\"+Inf\"}} {2}\n", metric_name, hist_tags, | ||
| stats.sampleCount())); | ||
| response.add(fmt::format("{0}_sum{{{1}}} {2:.32g}\n", metric_name, tags, stats.sampleSum())); | ||
| response.add(fmt::format("{0}_count{{{1}}} {2}\n", metric_name, tags, stats.sampleCount())); | ||
| } | ||
| output.append(fmt::format("{0}_bucket{{{1}le=\"+Inf\"}} {2}\n", metric_name, hist_tags, | ||
| stats.sampleCount())); | ||
| output.append(fmt::format("{0}_sum{{{1}}} {2:.32g}\n", metric_name, tags, stats.sampleSum())); | ||
| output.append(fmt::format("{0}_count{{{1}}} {2}\n", metric_name, tags, stats.sampleCount())); | ||
|
|
||
| return metric_type_tracker.size(); | ||
| } | ||
| return output; | ||
| }; | ||
|
|
||
| uint64_t metric_name_count = 0; | ||
| metric_name_count += process_type(counters, generate_counter_and_gauge_output, "counter"); | ||
| metric_name_count += process_type(gauges, generate_counter_and_gauge_output, "gauge"); | ||
| metric_name_count += process_type(histograms, generate_histogram_output, "histogram"); | ||
|
ggreenway marked this conversation as resolved.
Outdated
|
||
|
|
||
| return metric_name_count; | ||
| } // namespace Server | ||
|
|
||
| std::string | ||
| StatsHandler::statsAsJson(const std::map<std::string, uint64_t>& all_stats, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doc this? And also consider spelling it ElementType per Envoy style?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definition is taken from std::shared_ptr. I'll doc it.