LCOV - code coverage report
Current view: top level - source/server/admin - prometheus_stats.cc (source / functions) Hit Total Coverage
Test: coverage.dat Lines: 0 161 0.0 %
Date: 2024-01-05 06:35:25 Functions: 0 21 0.0 %

          Line data    Source code
       1             : #include "source/server/admin/prometheus_stats.h"
       2             : 
       3             : #include "source/common/common/empty_string.h"
       4             : #include "source/common/common/macros.h"
       5             : #include "source/common/common/regex.h"
       6             : #include "source/common/stats/histogram_impl.h"
       7             : #include "source/common/upstream/host_utility.h"
       8             : 
       9             : #include "absl/strings/str_cat.h"
      10             : #include "absl/strings/str_replace.h"
      11             : 
      12             : namespace Envoy {
      13             : namespace Server {
      14             : 
      15             : namespace {
      16             : 
      17           0 : const Regex::CompiledGoogleReMatcher& promRegex() {
      18           0 :   CONSTRUCT_ON_FIRST_USE(Regex::CompiledGoogleReMatcher, "[^a-zA-Z0-9_]", false);
      19           0 : }
      20             : 
      21             : /**
      22             :  * Take a string and sanitize it according to Prometheus conventions.
      23             :  */
      24           0 : std::string sanitizeName(const absl::string_view name) {
      25             :   // The name must match the regex [a-zA-Z_][a-zA-Z0-9_]* as required by
      26             :   // prometheus. Refer to https://prometheus.io/docs/concepts/data_model/.
      27             :   // The initial [a-zA-Z_] constraint is always satisfied by the namespace prefix.
      28           0 :   return promRegex().replaceAll(name, "_");
      29           0 : }
      30             : 
      31             : /**
      32             :  * Take tag values and sanitize it for text serialization, according to
      33             :  * Prometheus conventions.
      34             :  */
      35           0 : std::string sanitizeValue(const absl::string_view value) {
      36             :   // Removes problematic characters from Prometheus tag values to prevent
      37             :   // text serialization issues. This matches the prometheus text formatting code:
      38             :   // https://github.com/prometheus/common/blob/88f1636b699ae4fb949d292ffb904c205bf542c9/expfmt/text_create.go#L419-L420.
      39             :   // The goal is to replace '\' with "\\", newline with "\n", and '"' with "\"".
      40           0 :   return absl::StrReplaceAll(value, {
      41           0 :                                         {R"(\)", R"(\\)"},
      42           0 :                                         {"\n", R"(\n)"},
      43           0 :                                         {R"(")", R"(\")"},
      44           0 :                                     });
      45           0 : }
      46             : 
      47             : /*
      48             :  * Comparator for Stats::Metric that does not require a string representation
      49             :  * to make the comparison, for memory efficiency.
      50             :  */
      51             : struct MetricLessThan {
      52           0 :   bool operator()(const Stats::Metric* a, const Stats::Metric* b) const {
      53           0 :     ASSERT(&a->constSymbolTable() == &b->constSymbolTable());
      54           0 :     return a->constSymbolTable().lessThan(a->statName(), b->statName());
      55           0 :   }
      56             : };
      57             : 
      58             : struct PrimitiveMetricSnapshotLessThan {
      59             :   bool operator()(const Stats::PrimitiveMetricMetadata* a,
      60           0 :                   const Stats::PrimitiveMetricMetadata* b) {
      61           0 :     return a->name() < b->name();
      62           0 :   }
      63             : };
      64             : 
      65             : std::string generateNumericOutput(uint64_t value, const Stats::TagVector& tags,
      66           0 :                                   const std::string& prefixed_tag_extracted_name) {
      67           0 :   const std::string formatted_tags = PrometheusStatsFormatter::formattedTags(tags);
      68           0 :   return fmt::format("{0}{{{1}}} {2}\n", prefixed_tag_extracted_name, formatted_tags, value);
      69           0 : }
      70             : 
      71             : /*
      72             :  * Return the prometheus output for a numeric Stat (Counter or Gauge).
      73             :  */
      74             : template <class StatType>
      75             : std::string generateStatNumericOutput(const StatType& metric,
      76           0 :                                       const std::string& prefixed_tag_extracted_name) {
      77           0 :   return generateNumericOutput(metric.value(), metric.tags(), prefixed_tag_extracted_name);
      78           0 : }
      79             : 
      80             : /*
      81             :  * Returns the prometheus output for a TextReadout in gauge format.
      82             :  * It is a workaround of a limitation of prometheus which stores only numeric metrics.
      83             :  * The output is a gauge named the same as a given text-readout. The value of returned gauge is
      84             :  * always equal to 0. Returned gauge contains all tags of a given text-readout and one additional
      85             :  * tag {"text_value":"textReadout.value"}.
      86             :  */
      87             : std::string generateTextReadoutOutput(const Stats::TextReadout& text_readout,
      88           0 :                                       const std::string& prefixed_tag_extracted_name) {
      89           0 :   auto tags = text_readout.tags();
      90           0 :   tags.push_back(Stats::Tag{"text_value", text_readout.value()});
      91           0 :   const std::string formattedTags = PrometheusStatsFormatter::formattedTags(tags);
      92           0 :   return fmt::format("{0}{{{1}}} 0\n", prefixed_tag_extracted_name, formattedTags);
      93           0 : }
      94             : 
      95             : /*
      96             :  * Returns the prometheus output for a histogram. The output is a multi-line string (with embedded
      97             :  * newlines) that contains all the individual bucket counts and sum/count for a single histogram
      98             :  * (metric_name plus all tags).
      99             :  */
     100             : std::string generateHistogramOutput(const Stats::ParentHistogram& histogram,
     101           0 :                                     const std::string& prefixed_tag_extracted_name) {
     102           0 :   const std::string tags = PrometheusStatsFormatter::formattedTags(histogram.tags());
     103           0 :   const std::string hist_tags = histogram.tags().empty() ? EMPTY_STRING : (tags + ",");
     104             : 
     105           0 :   const Stats::HistogramStatistics& stats = histogram.cumulativeStatistics();
     106           0 :   Stats::ConstSupportedBuckets& supported_buckets = stats.supportedBuckets();
     107           0 :   const std::vector<uint64_t>& computed_buckets = stats.computedBuckets();
     108           0 :   std::string output;
     109           0 :   for (size_t i = 0; i < supported_buckets.size(); ++i) {
     110           0 :     double bucket = supported_buckets[i];
     111           0 :     uint64_t value = computed_buckets[i];
     112             :     // We want to print the bucket in a fixed point (non-scientific) format. The fmt library
     113             :     // doesn't have a specific modifier to format as a fixed-point value only so we use the
     114             :     // 'g' operator which prints the number in general fixed point format or scientific format
     115             :     // with precision 50 to round the number up to 32 significant digits in fixed point format
     116             :     // which should cover pretty much all cases
     117           0 :     output.append(fmt::format("{0}_bucket{{{1}le=\"{2:.32g}\"}} {3}\n", prefixed_tag_extracted_name,
     118           0 :                               hist_tags, bucket, value));
     119           0 :   }
     120             : 
     121           0 :   output.append(fmt::format("{0}_bucket{{{1}le=\"+Inf\"}} {2}\n", prefixed_tag_extracted_name,
     122           0 :                             hist_tags, stats.sampleCount()));
     123           0 :   output.append(fmt::format("{0}_sum{{{1}}} {2:.32g}\n", prefixed_tag_extracted_name, tags,
     124           0 :                             stats.sampleSum()));
     125           0 :   output.append(fmt::format("{0}_count{{{1}}} {2}\n", prefixed_tag_extracted_name, tags,
     126           0 :                             stats.sampleCount()));
     127             : 
     128           0 :   return output;
     129           0 : };
     130             : 
     131             : /**
     132             :  * Processes a stat type (counter, gauge, histogram) by generating all output lines, sorting
     133             :  * them by tag-extracted metric name, and then outputting them in the correct sorted order into
     134             :  * response.
     135             :  *
     136             :  * @param response The buffer to put the output into.
     137             :  * @param used_only Whether to only output stats that are used.
     138             :  * @param regex A filter on which stats to output.
     139             :  * @param metrics The metrics to output stats for. This must contain all stats of the given type
     140             :  *        to be included in the same output.
     141             :  * @param generate_output A function which returns the output text for this metric.
     142             :  * @param type The name of the prometheus metric type for used in TYPE annotations.
     143             :  */
     144             : template <class StatType>
     145             : uint64_t outputStatType(
     146             :     Buffer::Instance& response, const StatsParams& params,
     147             :     const std::vector<Stats::RefcountPtr<StatType>>& metrics,
     148             :     const std::function<std::string(
     149             :         const StatType& metric, const std::string& prefixed_tag_extracted_name)>& generate_output,
     150           0 :     absl::string_view type, const Stats::CustomStatNamespaces& custom_namespaces) {
     151             : 
     152             :   /*
     153             :    * From
     154             :    * https:*github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#grouping-and-sorting:
     155             :    *
     156             :    * All lines for a given metric must be provided as one single group, with the optional HELP and
     157             :    * TYPE lines first (in no particular order). Beyond that, reproducible sorting in repeated
     158             :    * expositions is preferred but not required, i.e. do not sort if the computational cost is
     159             :    * prohibitive.
     160             :    */
     161             : 
     162             :   // This is an unsorted collection of dumb-pointers (no need to increment then decrement every
     163             :   // refcount; ownership is held throughout by `metrics`). It is unsorted for efficiency, but will
     164             :   // be sorted before producing the final output to satisfy the "preferred" ordering from the
     165             :   // prometheus spec: metrics will be sorted by their tags' textual representation, which will be
     166             :   // consistent across calls.
     167           0 :   using StatTypeUnsortedCollection = std::vector<const StatType*>;
     168             : 
     169             :   // Return early to avoid crashing when getting the symbol table from the first metric.
     170           0 :   if (metrics.empty()) {
     171           0 :     return 0;
     172           0 :   }
     173             : 
     174             :   // There should only be one symbol table for all of the stats in the admin
     175             :   // interface. If this assumption changes, the name comparisons in this function
     176             :   // will have to change to compare to convert all StatNames to strings before
     177             :   // comparison.
     178           0 :   const Stats::SymbolTable& global_symbol_table = metrics.front()->constSymbolTable();
     179             : 
     180             :   // Sorted collection of metrics sorted by their tagExtractedName, to satisfy the requirements
     181             :   // of the exposition format.
     182           0 :   std::map<Stats::StatName, StatTypeUnsortedCollection, Stats::StatNameLessThan> groups(
     183           0 :       global_symbol_table);
     184             : 
     185           0 :   for (const auto& metric : metrics) {
     186           0 :     ASSERT(&global_symbol_table == &metric->constSymbolTable());
     187           0 :     if (!params.shouldShowMetric(*metric)) {
     188           0 :       continue;
     189           0 :     }
     190           0 :     groups[metric->tagExtractedStatName()].push_back(metric.get());
     191           0 :   }
     192             : 
     193           0 :   auto result = groups.size();
     194           0 :   for (auto& group : groups) {
     195           0 :     const absl::optional<std::string> prefixed_tag_extracted_name =
     196           0 :         PrometheusStatsFormatter::metricName(global_symbol_table.toString(group.first),
     197           0 :                                              custom_namespaces);
     198           0 :     if (!prefixed_tag_extracted_name.has_value()) {
     199           0 :       --result;
     200           0 :       continue;
     201           0 :     }
     202           0 :     response.add(fmt::format("# TYPE {0} {1}\n", prefixed_tag_extracted_name.value(), type));
     203             : 
     204             :     // Sort before producing the final output to satisfy the "preferred" ordering from the
     205             :     // prometheus spec: metrics will be sorted by their tags' textual representation, which will
     206             :     // be consistent across calls.
     207           0 :     std::sort(group.second.begin(), group.second.end(), MetricLessThan());
     208             : 
     209           0 :     for (const auto& metric : group.second) {
     210           0 :       response.add(generate_output(*metric, prefixed_tag_extracted_name.value()));
     211           0 :     }
     212           0 :   }
     213           0 :   return result;
     214           0 : }
     215             : 
     216             : template <class StatType>
     217             : uint64_t outputPrimitiveStatType(Buffer::Instance& response, const StatsParams& params,
     218             :                                  const std::vector<StatType>& metrics, absl::string_view type,
     219           0 :                                  const Stats::CustomStatNamespaces& custom_namespaces) {
     220             : 
     221             :   /*
     222             :    * From
     223             :    * https:*github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#grouping-and-sorting:
     224             :    *
     225             :    * All lines for a given metric must be provided as one single group, with the optional HELP and
     226             :    * TYPE lines first (in no particular order). Beyond that, reproducible sorting in repeated
     227             :    * expositions is preferred but not required, i.e. do not sort if the computational cost is
     228             :    * prohibitive.
     229             :    */
     230             : 
     231             :   // This is an unsorted collection of dumb-pointers (no need to increment then decrement every
     232             :   // refcount; ownership is held throughout by `metrics`). It is unsorted for efficiency, but will
     233             :   // be sorted before producing the final output to satisfy the "preferred" ordering from the
     234             :   // prometheus spec: metrics will be sorted by their tags' textual representation, which will be
     235             :   // consistent across calls.
     236           0 :   using StatTypeUnsortedCollection = std::vector<const StatType*>;
     237             : 
     238             :   // Return early to avoid crashing when getting the symbol table from the first metric.
     239           0 :   if (metrics.empty()) {
     240           0 :     return 0;
     241           0 :   }
     242             : 
     243             :   // Sorted collection of metrics sorted by their tagExtractedName, to satisfy the requirements
     244             :   // of the exposition format.
     245           0 :   std::map<std::string, StatTypeUnsortedCollection> groups;
     246             : 
     247           0 :   for (const auto& metric : metrics) {
     248           0 :     if (!params.shouldShowMetric(metric)) {
     249           0 :       continue;
     250           0 :     }
     251           0 :     groups[metric.tagExtractedName()].push_back(&metric);
     252           0 :   }
     253             : 
     254           0 :   auto result = groups.size();
     255           0 :   for (auto& group : groups) {
     256           0 :     const absl::optional<std::string> prefixed_tag_extracted_name =
     257           0 :         PrometheusStatsFormatter::metricName(group.first, custom_namespaces);
     258           0 :     if (!prefixed_tag_extracted_name.has_value()) {
     259           0 :       --result;
     260           0 :       continue;
     261           0 :     }
     262           0 :     response.add(fmt::format("# TYPE {0} {1}\n", prefixed_tag_extracted_name.value(), type));
     263             : 
     264             :     // Sort before producing the final output to satisfy the "preferred" ordering from the
     265             :     // prometheus spec: metrics will be sorted by their tags' textual representation, which will
     266             :     // be consistent across calls.
     267           0 :     std::sort(group.second.begin(), group.second.end(), PrimitiveMetricSnapshotLessThan());
     268             : 
     269           0 :     for (const auto& metric : group.second) {
     270           0 :       response.add(generateNumericOutput(metric->value(), metric->tags(),
     271           0 :                                          prefixed_tag_extracted_name.value()));
     272           0 :     }
     273           0 :   }
     274           0 :   return result;
     275           0 : }
     276             : 
     277             : } // namespace
     278             : 
     279           0 : std::string PrometheusStatsFormatter::formattedTags(const std::vector<Stats::Tag>& tags) {
     280           0 :   std::vector<std::string> buf;
     281           0 :   buf.reserve(tags.size());
     282           0 :   for (const Stats::Tag& tag : tags) {
     283           0 :     buf.push_back(fmt::format("{}=\"{}\"", sanitizeName(tag.name_), sanitizeValue(tag.value_)));
     284           0 :   }
     285           0 :   return absl::StrJoin(buf, ",");
     286           0 : }
     287             : 
     288             : absl::optional<std::string>
     289             : PrometheusStatsFormatter::metricName(const std::string& extracted_name,
     290           0 :                                      const Stats::CustomStatNamespaces& custom_namespaces) {
     291           0 :   const absl::optional<absl::string_view> custom_namespace_stripped =
     292           0 :       custom_namespaces.stripRegisteredPrefix(extracted_name);
     293           0 :   if (custom_namespace_stripped.has_value()) {
     294             :     // This case the name has a custom namespace, and it is a custom metric.
     295           0 :     const std::string sanitized_name = sanitizeName(custom_namespace_stripped.value());
     296             :     // We expose these metrics without modifying (e.g. without "envoy_"),
     297             :     // so we have to check the "user-defined" stat name complies with the Prometheus naming
     298             :     // convention. Specifically the name must start with the "[a-zA-Z_]" pattern.
     299             :     // All the characters in sanitized_name are already in "[a-zA-Z0-9_]" pattern
     300             :     // thanks to sanitizeName above, so the only thing we have to do is check
     301             :     // if it does not start with digits.
     302           0 :     if (sanitized_name.empty() || absl::ascii_isdigit(sanitized_name.front())) {
     303           0 :       return absl::nullopt;
     304           0 :     }
     305           0 :     return sanitized_name;
     306           0 :   }
     307             : 
     308             :   // If it does not have a custom namespace, add namespacing prefix to avoid conflicts, as per best
     309             :   // practice: https://prometheus.io/docs/practices/naming/#metric-names Also, naming conventions on
     310             :   // https://prometheus.io/docs/concepts/data_model/
     311           0 :   return absl::StrCat("envoy_", sanitizeName(extracted_name));
     312           0 : }
     313             : 
     314             : uint64_t PrometheusStatsFormatter::statsAsPrometheus(
     315             :     const std::vector<Stats::CounterSharedPtr>& counters,
     316             :     const std::vector<Stats::GaugeSharedPtr>& gauges,
     317             :     const std::vector<Stats::ParentHistogramSharedPtr>& histograms,
     318             :     const std::vector<Stats::TextReadoutSharedPtr>& text_readouts,
     319             :     const Upstream::ClusterManager& cluster_manager, Buffer::Instance& response,
     320           0 :     const StatsParams& params, const Stats::CustomStatNamespaces& custom_namespaces) {
     321             : 
     322           0 :   uint64_t metric_name_count = 0;
     323           0 :   metric_name_count += outputStatType<Stats::Counter>(response, params, counters,
     324           0 :                                                       generateStatNumericOutput<Stats::Counter>,
     325           0 :                                                       "counter", custom_namespaces);
     326             : 
     327           0 :   metric_name_count += outputStatType<Stats::Gauge>(response, params, gauges,
     328           0 :                                                     generateStatNumericOutput<Stats::Gauge>,
     329           0 :                                                     "gauge", custom_namespaces);
     330             : 
     331             :   // TextReadout stats are returned in gauge format, so "gauge" type is set intentionally.
     332           0 :   metric_name_count += outputStatType<Stats::TextReadout>(
     333           0 :       response, params, text_readouts, generateTextReadoutOutput, "gauge", custom_namespaces);
     334             : 
     335           0 :   metric_name_count += outputStatType<Stats::ParentHistogram>(
     336           0 :       response, params, histograms, generateHistogramOutput, "histogram", custom_namespaces);
     337             : 
     338             :   // Note: This assumes that there is no overlap in stat name between per-endpoint stats and all
     339             :   // other stats. If this is not true, then the counters/gauges for per-endpoint need to be combined
     340             :   // with the above counter/gauge calls so that stats can be properly grouped.
     341           0 :   std::vector<Stats::PrimitiveCounterSnapshot> host_counters;
     342           0 :   std::vector<Stats::PrimitiveGaugeSnapshot> host_gauges;
     343           0 :   Upstream::HostUtility::forEachHostMetric(
     344           0 :       cluster_manager,
     345           0 :       [&](Stats::PrimitiveCounterSnapshot&& metric) {
     346           0 :         host_counters.emplace_back(std::move(metric));
     347           0 :       },
     348           0 :       [&](Stats::PrimitiveGaugeSnapshot&& metric) { host_gauges.emplace_back(std::move(metric)); });
     349             : 
     350           0 :   metric_name_count +=
     351           0 :       outputPrimitiveStatType(response, params, host_counters, "counter", custom_namespaces);
     352             : 
     353           0 :   metric_name_count +=
     354           0 :       outputPrimitiveStatType(response, params, host_gauges, "gauge", custom_namespaces);
     355             : 
     356           0 :   return metric_name_count;
     357           0 : }
     358             : 
     359             : } // namespace Server
     360             : } // namespace Envoy

Generated by: LCOV version 1.15