Coverage Report

Created: 2023-11-12 09:30

/proc/self/cwd/source/server/admin/prometheus_stats.cc
Line
Count
Source (jump to first uncovered line)
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
}
Unexecuted instantiation: prometheus_stats.cc:std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > Envoy::Server::(anonymous namespace)::generateStatNumericOutput<Envoy::Stats::Counter>(Envoy::Stats::Counter const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)
Unexecuted instantiation: prometheus_stats.cc:std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > Envoy::Server::(anonymous namespace)::generateStatNumericOutput<Envoy::Stats::Gauge>(Envoy::Stats::Gauge const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)
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
}
Unexecuted instantiation: prometheus_stats.cc:unsigned long Envoy::Server::(anonymous namespace)::outputStatType<Envoy::Stats::Counter>(Envoy::Buffer::Instance&, Envoy::Server::StatsParams const&, std::__1::vector<Envoy::Stats::RefcountPtr<Envoy::Stats::Counter>, std::__1::allocator<Envoy::Stats::RefcountPtr<Envoy::Stats::Counter> > > const&, std::__1::function<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > (Envoy::Stats::Counter const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)> const&, std::__1::basic_string_view<char, std::__1::char_traits<char> >, Envoy::Stats::CustomStatNamespaces const&)
Unexecuted instantiation: prometheus_stats.cc:unsigned long Envoy::Server::(anonymous namespace)::outputStatType<Envoy::Stats::Gauge>(Envoy::Buffer::Instance&, Envoy::Server::StatsParams const&, std::__1::vector<Envoy::Stats::RefcountPtr<Envoy::Stats::Gauge>, std::__1::allocator<Envoy::Stats::RefcountPtr<Envoy::Stats::Gauge> > > const&, std::__1::function<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > (Envoy::Stats::Gauge const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)> const&, std::__1::basic_string_view<char, std::__1::char_traits<char> >, Envoy::Stats::CustomStatNamespaces const&)
Unexecuted instantiation: prometheus_stats.cc:unsigned long Envoy::Server::(anonymous namespace)::outputStatType<Envoy::Stats::TextReadout>(Envoy::Buffer::Instance&, Envoy::Server::StatsParams const&, std::__1::vector<Envoy::Stats::RefcountPtr<Envoy::Stats::TextReadout>, std::__1::allocator<Envoy::Stats::RefcountPtr<Envoy::Stats::TextReadout> > > const&, std::__1::function<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > (Envoy::Stats::TextReadout const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)> const&, std::__1::basic_string_view<char, std::__1::char_traits<char> >, Envoy::Stats::CustomStatNamespaces const&)
Unexecuted instantiation: prometheus_stats.cc:unsigned long Envoy::Server::(anonymous namespace)::outputStatType<Envoy::Stats::ParentHistogram>(Envoy::Buffer::Instance&, Envoy::Server::StatsParams const&, std::__1::vector<Envoy::Stats::RefcountPtr<Envoy::Stats::ParentHistogram>, std::__1::allocator<Envoy::Stats::RefcountPtr<Envoy::Stats::ParentHistogram> > > const&, std::__1::function<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > (Envoy::Stats::ParentHistogram const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)> const&, std::__1::basic_string_view<char, std::__1::char_traits<char> >, Envoy::Stats::CustomStatNamespaces const&)
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
}
Unexecuted instantiation: prometheus_stats.cc:unsigned long Envoy::Server::(anonymous namespace)::outputPrimitiveStatType<Envoy::Stats::PrimitiveCounterSnapshot>(Envoy::Buffer::Instance&, Envoy::Server::StatsParams const&, std::__1::vector<Envoy::Stats::PrimitiveCounterSnapshot, std::__1::allocator<Envoy::Stats::PrimitiveCounterSnapshot> > const&, std::__1::basic_string_view<char, std::__1::char_traits<char> >, Envoy::Stats::CustomStatNamespaces const&)
Unexecuted instantiation: prometheus_stats.cc:unsigned long Envoy::Server::(anonymous namespace)::outputPrimitiveStatType<Envoy::Stats::PrimitiveGaugeSnapshot>(Envoy::Buffer::Instance&, Envoy::Server::StatsParams const&, std::__1::vector<Envoy::Stats::PrimitiveGaugeSnapshot, std::__1::allocator<Envoy::Stats::PrimitiveGaugeSnapshot> > const&, std::__1::basic_string_view<char, std::__1::char_traits<char> >, Envoy::Stats::CustomStatNamespaces const&)
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