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
|