1
#include "source/server/admin/prometheus_stats.h"
2

            
3
#include <cmath>
4
#include <map>
5
#include <set>
6

            
7
#include "source/common/common/empty_string.h"
8
#include "source/common/common/macros.h"
9
#include "source/common/common/regex.h"
10
#include "source/common/protobuf/protobuf.h"
11
#include "source/common/stats/histogram_impl.h"
12
#include "source/common/upstream/host_utility.h"
13

            
14
#include "absl/strings/str_cat.h"
15
#include "absl/strings/str_replace.h"
16
#include "io/prometheus/client/metrics.pb.h"
17

            
18
namespace Envoy {
19
namespace Server {
20

            
21
namespace {
22

            
23
5920
const Regex::CompiledGoogleReMatcher& promRegex() {
24
5920
  CONSTRUCT_ON_FIRST_USE(Regex::CompiledGoogleReMatcherNoSafetyChecks, "[^a-zA-Z0-9_]");
25
5920
}
26

            
27
/**
28
 * Take a string and sanitize it according to Prometheus conventions.
29
 */
30
5920
std::string sanitizeName(const absl::string_view name) {
31
  // The name must match the regex [a-zA-Z_][a-zA-Z0-9_]* as required by
32
  // prometheus. Refer to https://prometheus.io/docs/concepts/data_model/.
33
  // The initial [a-zA-Z_] constraint is always satisfied by the namespace prefix.
34
5920
  return promRegex().replaceAll(name, "_");
35
5920
}
36

            
37
/**
38
 * Take tag values and sanitize it for text serialization, according to
39
 * Prometheus conventions.
40
 */
41
3027
std::string sanitizeValue(const absl::string_view value) {
42
  // Removes problematic characters from Prometheus tag values to prevent
43
  // text serialization issues. This matches the prometheus text formatting code:
44
  // https://github.com/prometheus/common/blob/88f1636b699ae4fb949d292ffb904c205bf542c9/expfmt/text_create.go#L419-L420.
45
  // The goal is to replace '\' with "\\", newline with "\n", and '"' with "\"".
46
3027
  return absl::StrReplaceAll(value, {
47
3027
                                        {R"(\)", R"(\\)"},
48
3027
                                        {"\n", R"(\n)"},
49
3027
                                        {R"(")", R"(\")"},
50
3027
                                    });
51
3027
}
52

            
53
/*
54
 * Comparator for Stats::Metric that does not require a string representation
55
 * to make the comparison, for memory efficiency.
56
 */
57
struct MetricLessThan {
58
1013
  bool operator()(const Stats::Metric* a, const Stats::Metric* b) const {
59
1013
    ASSERT(&a->constSymbolTable() == &b->constSymbolTable());
60
1013
    return a->constSymbolTable().lessThan(a->statName(), b->statName());
61
1013
  }
62
};
63

            
64
struct PrimitiveMetricSnapshotLessThan {
65
  bool operator()(const Stats::PrimitiveMetricMetadata* a,
66
25
                  const Stats::PrimitiveMetricMetadata* b) {
67
25
    return a->name() < b->name();
68
25
  }
69
};
70

            
71
class TextFormat : public PrometheusStatsFormatter::OutputFormat {
72
public:
73
  void generateOutput(Buffer::Instance& output, const std::vector<const Stats::Counter*>& counters,
74
1371
                      const std::string& prefixed_tag_extracted_name) const override {
75
1371
    generateNumericOutput(output, counters, prefixed_tag_extracted_name);
76
1371
  }
77

            
78
  void generateOutput(Buffer::Instance& output,
79
                      const std::vector<const Stats::PrimitiveCounterSnapshot*>& counters,
80
3
                      const std::string& prefixed_tag_extracted_name) const override {
81
3
    generateNumericOutput(output, counters, prefixed_tag_extracted_name);
82
3
  }
83

            
84
  void generateOutput(Buffer::Instance& output, const std::vector<const Stats::Gauge*>& gauges,
85
457
                      const std::string& prefixed_tag_extracted_name) const override {
86
457
    generateNumericOutput(output, gauges, prefixed_tag_extracted_name);
87
457
  }
88

            
89
  void generateOutput(Buffer::Instance& output,
90
                      const std::vector<const Stats::PrimitiveGaugeSnapshot*>& gauges,
91
3
                      const std::string& prefixed_tag_extracted_name) const override {
92
3
    generateNumericOutput(output, gauges, prefixed_tag_extracted_name);
93
3
  }
94

            
95
  void generateOutput(Buffer::Instance& output,
96
                      const std::vector<const Stats::ParentHistogram*>& histograms,
97
85
                      const std::string& prefixed_tag_extracted_name) const override {
98
85
    switch (histogramType()) {
99
2
    case HistogramType::Summary:
100
2
      generateSummaryOutput(output, histograms, prefixed_tag_extracted_name);
101
2
      break;
102
83
    case HistogramType::ClassicHistogram:
103
83
      generateHistogramOutput(output, histograms, prefixed_tag_extracted_name);
104
83
      break;
105
    case HistogramType::NativeHistogram:
106
      IS_ENVOY_BUG("invalid type");
107
      break;
108
85
    }
109
85
  }
110

            
111
  /*
112
   * Returns the prometheus output for a group of TextReadouts in gauge format.
113
   * It is a workaround of a limitation of prometheus which stores only numeric metrics.
114
   * The output is a gauge named the same as a given text-readout. The value of returned gauge is
115
   * always equal to 0. Returned gauge contains all tags of a given text-readout and one additional
116
   * tag {"text_value":"textReadout.value"}.
117
   */
118
  void generateOutput(Buffer::Instance& output,
119
                      const std::vector<const Stats::TextReadout*>& text_readouts,
120
6
                      const std::string& prefixed_tag_extracted_name) const override {
121
    // TextReadout stats are returned in gauge format, so "gauge" type is set intentionally.
122
6
    generateTypeOutput(output, "gauge", prefixed_tag_extracted_name);
123

            
124
6
    for (const auto* text_readout : text_readouts) {
125
6
      auto tags = text_readout->tags();
126
6
      tags.push_back(Stats::Tag{"text_value", text_readout->value()});
127
6
      const std::string formattedTags = PrometheusStatsFormatter::formattedTags(tags);
128
6
      output.add(fmt::format("{0}{{{1}}} 0\n", prefixed_tag_extracted_name, formattedTags));
129
6
    }
130
6
  }
131

            
132
private:
133
  void generateTypeOutput(Buffer::Instance& output, absl::string_view type,
134
1925
                          const std::string& prefixed_tag_extracted_name) const {
135
1925
    output.add(fmt::format("# TYPE {0} {1}\n", prefixed_tag_extracted_name, type));
136
1925
  }
137

            
138
  template <class StatType>
139
  void generateNumericOutput(Buffer::Instance& output, const std::vector<const StatType*>& metrics,
140
1834
                             const std::string& prefixed_tag_extracted_name) const {
141
1834
    absl::string_view type;
142
    if constexpr (std::is_same_v<Stats::Counter, StatType> ||
143
1374
                  std::is_same_v<Stats::PrimitiveCounterSnapshot, StatType>) {
144
1374
      type = "counter";
145
    } else if constexpr (std::is_same_v<Stats::Gauge, StatType> ||
146
460
                         std::is_same_v<Stats::PrimitiveGaugeSnapshot, StatType>) {
147
460
      type = "gauge";
148
    } else {
149
      static_assert(false, "Unexpected StatsType");
150
    }
151

            
152
1834
    generateTypeOutput(output, type, prefixed_tag_extracted_name);
153
2364
    for (const auto* metric : metrics) {
154
2364
      const std::string formatted_tags = PrometheusStatsFormatter::formattedTags(metric->tags());
155
2364
      output.add(fmt::format("{0}{{{1}}} {2}\n", prefixed_tag_extracted_name, formatted_tags,
156
2364
                             metric->value()));
157
2364
    }
158
1834
  }
159

            
160
  /*
161
   * Returns the prometheus output for a histogram. The output is a multi-line string (with embedded
162
   * newlines) that contains all the individual bucket counts and sum/count for a single histogram
163
   * (metric_name plus all tags).
164
   */
165
  void generateHistogramOutput(Buffer::Instance& output,
166
                               const std::vector<const Stats::ParentHistogram*>& histograms,
167
83
                               const std::string& prefixed_tag_extracted_name) const {
168
83
    generateTypeOutput(output, "histogram", prefixed_tag_extracted_name);
169

            
170
99
    for (const auto* histogram : histograms) {
171
99
      const std::string tags = PrometheusStatsFormatter::formattedTags(histogram->tags());
172
99
      const std::string hist_tags = histogram->tags().empty() ? EMPTY_STRING : (tags + ",");
173

            
174
99
      const Stats::HistogramStatistics& stats = histogram->cumulativeStatistics();
175
99
      Stats::ConstSupportedBuckets& supported_buckets = stats.supportedBuckets();
176
99
      const std::vector<uint64_t>& computed_buckets = stats.computedBuckets();
177
1856
      for (size_t i = 0; i < supported_buckets.size(); ++i) {
178
1757
        double bucket = supported_buckets[i];
179
1757
        uint64_t value = computed_buckets[i];
180
        // We want to print the bucket in a fixed point (non-scientific) format. The fmt library
181
        // doesn't have a specific modifier to format as a fixed-point value only so we use the
182
        // 'g' operator which prints the number in general fixed point format or scientific format
183
        // with precision 50 to round the number up to 32 significant digits in fixed point format
184
        // which should cover pretty much all cases
185
1757
        output.add(fmt::format("{0}_bucket{{{1}le=\"{2:.32g}\"}} {3}\n",
186
1757
                               prefixed_tag_extracted_name, hist_tags, bucket, value));
187
1757
      }
188

            
189
99
      output.add(fmt::format("{0}_bucket{{{1}le=\"+Inf\"}} {2}\n", prefixed_tag_extracted_name,
190
99
                             hist_tags, stats.sampleCount()));
191
99
      output.add(fmt::format("{0}_sum{{{1}}} {2:.32g}\n", prefixed_tag_extracted_name, tags,
192
99
                             stats.sampleSum()));
193
99
      output.add(fmt::format("{0}_count{{{1}}} {2}\n", prefixed_tag_extracted_name, tags,
194
99
                             stats.sampleCount()));
195
99
    }
196
83
  }
197

            
198
  /*
199
   * Returns the prometheus output for a summary. The output is a multi-line string (with embedded
200
   * newlines) that contains all the individual quantile values and sum/count for a single histogram
201
   * (metric_name plus all tags).
202
   */
203
  void generateSummaryOutput(Buffer::Instance& output,
204
                             const std::vector<const Stats::ParentHistogram*>& histograms,
205
2
                             const std::string& prefixed_tag_extracted_name) const {
206
2
    generateTypeOutput(output, "summary", prefixed_tag_extracted_name);
207

            
208
2
    for (const auto* histogram : histograms) {
209
2
      const std::string tags = PrometheusStatsFormatter::formattedTags(histogram->tags());
210
2
      const std::string hist_tags = histogram->tags().empty() ? EMPTY_STRING : (tags + ",");
211

            
212
2
      const Stats::HistogramStatistics& stats = histogram->intervalStatistics();
213
2
      Stats::ConstSupportedBuckets& supported_quantiles = stats.supportedQuantiles();
214
2
      const std::vector<double>& computed_quantiles = stats.computedQuantiles();
215
22
      for (size_t i = 0; i < supported_quantiles.size(); ++i) {
216
20
        double quantile = supported_quantiles[i];
217
20
        double value = computed_quantiles[i];
218
20
        output.add(fmt::format("{0}{{{1}quantile=\"{2}\"}} {3:.32g}\n", prefixed_tag_extracted_name,
219
20
                               hist_tags, quantile, value));
220
20
      }
221

            
222
2
      output.add(fmt::format("{0}_sum{{{1}}} {2:.32g}\n", prefixed_tag_extracted_name, tags,
223
2
                             stats.sampleSum()));
224
2
      output.add(fmt::format("{0}_count{{{1}}} {2}\n", prefixed_tag_extracted_name, tags,
225
2
                             stats.sampleCount()));
226
2
    }
227
2
  }
228
};
229

            
230
class ProtobufFormat : public PrometheusStatsFormatter::OutputFormat {
231
public:
232
  static constexpr uint32_t kDefaultMaxNativeHistogramBuckets = 20;
233

            
234
  ProtobufFormat(absl::optional<uint32_t> native_histogram_max_buckets)
235
      : native_histogram_max_buckets_(
236
28
            native_histogram_max_buckets.value_or(kDefaultMaxNativeHistogramBuckets)) {}
237

            
238
  void generateOutput(Buffer::Instance& output, const std::vector<const Stats::Counter*>& counters,
239
681
                      const std::string& prefixed_tag_extracted_name) const override {
240
681
    generateNumericOutput(output, counters, prefixed_tag_extracted_name,
241
681
                          io::prometheus::client::MetricType::COUNTER);
242
681
  }
243

            
244
  // Return the prometheus output for a group of PrimitiveCounters.
245
  void generateOutput(Buffer::Instance& output,
246
                      const std::vector<const Stats::PrimitiveCounterSnapshot*>& counters,
247
2
                      const std::string& prefixed_tag_extracted_name) const override {
248
2
    generateNumericOutput(output, counters, prefixed_tag_extracted_name,
249
2
                          io::prometheus::client::MetricType::COUNTER);
250
2
  }
251

            
252
  // Return the prometheus output for a group of Gauges.
253
  void generateOutput(Buffer::Instance& output, const std::vector<const Stats::Gauge*>& gauges,
254
221
                      const std::string& prefixed_tag_extracted_name) const override {
255
221
    generateNumericOutput(output, gauges, prefixed_tag_extracted_name,
256
221
                          io::prometheus::client::MetricType::GAUGE);
257
221
  }
258

            
259
  // Returns the prometheus output for a group of TextReadouts.
260
  void generateOutput(Buffer::Instance& output,
261
                      const std::vector<const Stats::TextReadout*>& text_readouts,
262
2
                      const std::string& prefixed_tag_extracted_name) const override {
263
2
    ASSERT(!text_readouts.empty());
264

            
265
2
    io::prometheus::client::MetricFamily metric_family;
266
2
    metric_family.set_name(prefixed_tag_extracted_name);
267
2
    metric_family.set_type(io::prometheus::client::MetricType::GAUGE);
268
2
    metric_family.mutable_metric()->Reserve(text_readouts.size());
269

            
270
2
    for (const auto* text_readout : text_readouts) {
271
2
      auto* metric = metric_family.add_metric();
272
2
      addLabelsToMetric(metric, text_readout->tags());
273

            
274
      // Add text_value tag
275
2
      auto* text_label = metric->add_label();
276
2
      text_label->set_name("text_value");
277
2
      text_label->set_value(sanitizeValue(text_readout->value()));
278

            
279
      // Set gauge value to 0
280
2
      auto* gauge = metric->mutable_gauge();
281
2
      gauge->set_value(0);
282
2
    }
283

            
284
2
    writeDelimitedMessage(metric_family, output);
285
2
  }
286

            
287
  // Return the prometheus output for a group of PrimitiveGauges.
288
  void generateOutput(Buffer::Instance& output,
289
                      const std::vector<const Stats::PrimitiveGaugeSnapshot*>& gauges,
290
3
                      const std::string& prefixed_tag_extracted_name) const override {
291
3
    generateNumericOutput(output, gauges, prefixed_tag_extracted_name,
292
3
                          io::prometheus::client::MetricType::GAUGE);
293
3
  }
294

            
295
  // Return the prometheus output for a group of Histograms.
296
  void generateOutput(Buffer::Instance& output,
297
                      const std::vector<const Stats::ParentHistogram*>& histograms,
298
55
                      const std::string& prefixed_tag_extracted_name) const override {
299
55
    ASSERT(!histograms.empty());
300

            
301
55
    io::prometheus::client::MetricFamily metric_family;
302
55
    metric_family.set_name(prefixed_tag_extracted_name);
303

            
304
55
    switch (histogramType()) {
305
1
    case HistogramType::Summary:
306
1
      generateSummaryOutput(metric_family, histograms);
307
1
      break;
308
39
    case HistogramType::ClassicHistogram:
309
39
      generateHistogramOutput(metric_family, histograms);
310
39
      break;
311
15
    case HistogramType::NativeHistogram:
312
15
      generateNativeHistogramOutput(metric_family, histograms);
313
15
      break;
314
55
    }
315

            
316
55
    writeDelimitedMessage(metric_family, output);
317
55
  }
318

            
319
private:
320
  // Helper method to add labels to a metric from tags.
321
  void addLabelsToMetric(io::prometheus::client::Metric* metric,
322
1229
                         const std::vector<Stats::Tag>& tags) const {
323
1229
    metric->mutable_label()->Reserve(tags.size());
324
1244
    for (const auto& tag : tags) {
325
1002
      auto* label = metric->add_label();
326
1002
      label->set_name(sanitizeName(tag.name_));
327
1002
      label->set_value(sanitizeValue(tag.value_));
328
1002
    }
329
1229
  }
330

            
331
  template <class StatType>
332
  void generateNumericOutput(Buffer::Instance& output, const std::vector<const StatType*>& metrics,
333
                             const std::string& prefixed_tag_extracted_name,
334
907
                             io::prometheus::client::MetricType type) const {
335
907
    ASSERT(!metrics.empty());
336

            
337
907
    io::prometheus::client::MetricFamily metric_family;
338
907
    metric_family.set_name(prefixed_tag_extracted_name);
339
907
    metric_family.set_type(type);
340
907
    metric_family.mutable_metric()->Reserve(metrics.size());
341

            
342
1166
    for (const auto* metric : metrics) {
343
1166
      auto* prom_metric = metric_family.add_metric();
344
1166
      addLabelsToMetric(prom_metric, metric->tags());
345

            
346
      // Set value based on type
347
1166
      if (type == io::prometheus::client::MetricType::COUNTER) {
348
906
        auto* counter = prom_metric->mutable_counter();
349
906
        counter->set_value(metric->value());
350
906
      } else {
351
260
        auto* gauge = prom_metric->mutable_gauge();
352
260
        gauge->set_value(metric->value());
353
260
      }
354
1166
    }
355

            
356
907
    writeDelimitedMessage(metric_family, output);
357
907
  }
358

            
359
  void generateHistogramOutput(io::prometheus::client::MetricFamily& metric_family,
360
39
                               const std::vector<const Stats::ParentHistogram*>& histograms) const {
361
39
    metric_family.set_type(io::prometheus::client::MetricType::HISTOGRAM);
362
39
    metric_family.mutable_metric()->Reserve(histograms.size());
363

            
364
45
    for (const auto* histogram : histograms) {
365
45
      auto* metric = metric_family.add_metric();
366
45
      addLabelsToMetric(metric, histogram->tags());
367

            
368
45
      const Stats::HistogramStatistics& stats = histogram->cumulativeStatistics();
369
45
      Stats::ConstSupportedBuckets& supported_buckets = stats.supportedBuckets();
370
45
      const std::vector<uint64_t>& computed_buckets = stats.computedBuckets();
371

            
372
45
      auto* prom_histogram = metric->mutable_histogram();
373
45
      prom_histogram->set_sample_count(stats.sampleCount());
374
45
      prom_histogram->set_sample_sum(stats.sampleSum());
375

            
376
45
      prom_histogram->mutable_bucket()->Reserve(supported_buckets.size());
377
825
      for (size_t i = 0; i < supported_buckets.size(); ++i) {
378
780
        auto* bucket = prom_histogram->add_bucket();
379
780
        bucket->set_upper_bound(supported_buckets[i]);
380
780
        bucket->set_cumulative_count(computed_buckets[i]);
381
780
      }
382
45
    }
383
39
  }
384

            
385
  void generateSummaryOutput(io::prometheus::client::MetricFamily& metric_family,
386
1
                             const std::vector<const Stats::ParentHistogram*>& histograms) const {
387
1
    metric_family.set_type(io::prometheus::client::MetricType::SUMMARY);
388
1
    metric_family.mutable_metric()->Reserve(histograms.size());
389

            
390
1
    for (const auto* histogram : histograms) {
391
1
      auto* metric = metric_family.add_metric();
392
1
      addLabelsToMetric(metric, histogram->tags());
393

            
394
1
      const Stats::HistogramStatistics& stats = histogram->intervalStatistics();
395
1
      Stats::ConstSupportedBuckets& supported_quantiles = stats.supportedQuantiles();
396
1
      const std::vector<double>& computed_quantiles = stats.computedQuantiles();
397

            
398
1
      auto* summary = metric->mutable_summary();
399
1
      summary->set_sample_count(stats.sampleCount());
400
1
      summary->set_sample_sum(stats.sampleSum());
401

            
402
1
      summary->mutable_quantile()->Reserve(supported_quantiles.size());
403
11
      for (size_t i = 0; i < supported_quantiles.size(); ++i) {
404
10
        auto* quantile = summary->add_quantile();
405
10
        quantile->set_quantile(supported_quantiles[i]);
406
10
        quantile->set_value(computed_quantiles[i]);
407
10
      }
408
1
    }
409
1
  }
410

            
411
  // Set zero threshold - values below this go in zero bucket.
412
  // Since Histogram::recordValue() only accepts integers, the minimum non-zero value is 1.
413
  // Setting threshold to 0.5 ensures:
414
  // - Zeros go to zero bucket (0 < 0.5)
415
  // - Values >= 1 get positive bucket indices (1 > 0.5).
416
  // Using 0.5 avoids interpolation issues at bucket boundaries that occur with 1.0.
417
  // For Percent unit histograms, values are scaled by 1/PercentScale, so the threshold
418
  // must also be scaled accordingly.
419
  static constexpr double kNativeHistogramZeroThreshold = 0.5;
420

            
421
14
  static constexpr double nativeHistogramZeroThreshold(Stats::Histogram::Unit unit) {
422
14
    return (unit == Stats::Histogram::Unit::Percent)
423
14
               ? (kNativeHistogramZeroThreshold / Stats::Histogram::PercentScale)
424
14
               : kNativeHistogramZeroThreshold;
425
14
  }
426

            
427
  /**
428
   * Generates Prometheus native histogram output from Envoy's circllhist histograms.
429
   *
430
   * References for Prometheus native histogram format:
431
   *
432
   * https://prometheus.io/docs/specs/native_histograms/
433
   * https://docs.google.com/document/d/1VhtB_cGnuO2q_zqEMgtoaLDvJ_kFSXRXoE0Wo74JlSY/edit?tab=t.0
434
   *
435
   * Envoy uses circllhist (a log-linear histogram library) internally, which provides ~90 buckets
436
   * per order of magnitude with very high precision. Prometheus native histograms use exponential
437
   * buckets with base = 2^(2^(-schema)), where schema ranges from -4 (coarsest, 16x per bucket)
438
   * to 8 (finest, ~0.27% width per bucket).
439
   *
440
   * This code tries to map as accurately as possible from one format to the other.
441
   */
442
  void generateNativeHistogramOutput(
443
      io::prometheus::client::MetricFamily& metric_family,
444
15
      const std::vector<const Stats::ParentHistogram*>& histograms) const {
445
15
    metric_family.set_type(io::prometheus::client::MetricType::HISTOGRAM);
446

            
447
15
    for (const auto* histogram : histograms) {
448
15
      const Stats::HistogramStatistics& stats = histogram->cumulativeStatistics();
449

            
450
15
      auto* metric = metric_family.add_metric();
451
15
      addLabelsToMetric(metric, histogram->tags());
452

            
453
15
      auto* proto_histogram = metric->mutable_histogram();
454

            
455
      // Handle empty histogram case early to avoid unnecessary work.
456
      // Add a no-op span (offset 0, length 0) to distinguish from classic histograms.
457
15
      if (stats.sampleCount() == 0) {
458
1
        proto_histogram->set_schema(3); // Default schema
459
1
        proto_histogram->set_zero_count(0);
460
1
        auto* span = proto_histogram->add_positive_span();
461
1
        span->set_offset(0);
462
1
        span->set_length(0);
463
1
        continue;
464
1
      }
465

            
466
14
      proto_histogram->set_sample_count(stats.sampleCount());
467
14
      proto_histogram->set_sample_sum(stats.sampleSum());
468

            
469
14
      const double zero_threshold = nativeHistogramZeroThreshold(histogram->unit());
470
14
      proto_histogram->set_zero_threshold(zero_threshold);
471

            
472
14
      const auto detailed_buckets = histogram->detailedTotalBuckets();
473
14
      const auto [schema, needed_indices] = chooseNativeHistogramSchema(
474
14
          detailed_buckets, native_histogram_max_buckets_, zero_threshold);
475
14
      proto_histogram->set_schema(schema);
476

            
477
      // Count samples below zero_threshold as zero bucket
478
14
      const uint64_t zero_count = histogram->cumulativeCountLessThanOrEqualToValue(zero_threshold);
479
14
      proto_histogram->set_zero_count(zero_count);
480
14
      uint64_t prev_cumulative = zero_count;
481

            
482
14
      const double base = std::pow(2.0, std::pow(2.0, -schema));
483

            
484
      // Process needed indices and encode directly to protobuf spans and deltas.
485
      // We iterate over needed_indices, query cumulative counts, and build the
486
      // span/delta encoding.
487
14
      int32_t prev_nonzero_index = 0;
488
14
      int64_t prev_count = 0;
489
14
      bool first_nonzero = true;
490
14
      io::prometheus::client::BucketSpan* current_span = nullptr;
491
14
      uint32_t span_length = 0;
492

            
493
14
      proto_histogram->mutable_positive_delta()->Reserve(needed_indices.size());
494
148
      for (int32_t idx : needed_indices) {
495
148
        const double upper_bound = std::pow(base, idx + 1);
496
148
        uint64_t cumulative = histogram->cumulativeCountLessThanOrEqualToValue(upper_bound);
497
148
        uint64_t bucket_count = cumulative - prev_cumulative;
498
148
        prev_cumulative = cumulative;
499

            
500
148
        if (bucket_count == 0) {
501
78
          continue; // Skip zero-count buckets; gaps are handled by span encoding
502
78
        }
503

            
504
70
        const bool need_new_span = first_nonzero || (idx != prev_nonzero_index + 1);
505
70
        if (need_new_span) {
506
40
          if (current_span != nullptr) {
507
            // Finalize previous span if exists
508
27
            current_span->set_length(span_length);
509
27
          }
510

            
511
40
          current_span = proto_histogram->add_positive_span();
512
40
          if (first_nonzero) {
513
13
            current_span->set_offset(idx); // Offset from 0 for first span
514
13
            first_nonzero = false;
515
27
          } else {
516
27
            current_span->set_offset(idx - prev_nonzero_index - 1); // Gap from previous span
517
27
          }
518
40
          span_length = 0;
519
40
        }
520

            
521
        // Add delta-encoded count: the format takes the difference from the previous bucket
522
        // value, assuming that adjacent buckets often have similar values, and small numbers
523
        // encode smaller as protobuf varint.
524
70
        int64_t delta = static_cast<int64_t>(bucket_count) - prev_count;
525
70
        proto_histogram->add_positive_delta(delta);
526

            
527
70
        prev_nonzero_index = idx;
528
70
        prev_count = static_cast<int64_t>(bucket_count);
529
70
        span_length++;
530
70
      }
531

            
532
14
      if (current_span != nullptr) {
533
13
        current_span->set_length(span_length);
534
13
      }
535
14
    }
536
15
  }
537

            
538
  // Choose the highest-resolution schema that keeps the bucket count within max_buckets.
539
  // Returns both the schema and the computed bucket indices to avoid recomputing them.
540
  static std::pair<int8_t, std::set<int32_t>>
541
  chooseNativeHistogramSchema(const std::vector<Stats::ParentHistogram::Bucket>& detailed_buckets,
542
14
                              uint32_t max_buckets, double zero_threshold) {
543
    // Schema ranges from -4 (coarsest: 16x per bucket) to 8 (finest: ~0.27% per bucket). However,
544
    // we cap at schema 5 because circllhist has ~90 buckets per decade, which translates to ~27
545
    // buckets per doubling. This resolution falls between schema 4 (16 buckets/doubling) and schema
546
    // 5 (32 buckets/doubling). Using schemas higher than 5 would create artificial precision via
547
    // interpolation, not real accuracy gains.
548
    //
549
    // The default schema used is 4. Often schema 5 is more precision than is required, and because
550
    // the underlying data is at an accuracy between schemas 4 and 5, choose the lower value to
551
    // reduce resource usage.
552

            
553
    // Uncomment and use this if schema is every directly specified.
554
    // constexpr int8_t kSchemaMax = 5;
555

            
556
14
    constexpr int8_t kSchemaMin = -4;
557
14
    constexpr int8_t kSchemaDefault = 4;
558

            
559
41
    for (int8_t schema = kSchemaDefault; schema >= kSchemaMin; --schema) {
560
40
      absl::optional<std::set<int32_t>> indices = nativeHistogramBucketIndicesFromHistogramBuckets(
561
40
          detailed_buckets, schema, zero_threshold, max_buckets);
562
      // If it doesn't have a value, that means it exceeded `max_buckets`.
563
40
      if (indices.has_value()) {
564
13
        return {schema, std::move(*indices)};
565
13
      }
566
40
    }
567
    // Fallback if nothing fits - compute indices at coarsest schema without limit
568
1
    return {kSchemaMin, nativeHistogramBucketIndicesFromHistogramBuckets(detailed_buckets,
569
1
                                                                         kSchemaMin, zero_threshold)
570
1
                            .value()};
571
14
  }
572

            
573
  // For the vector of histogram buckets, return the set of all native histogram indices that
574
  // cover any part of the range of any of the buckets.
575
  //
576
  // If max_buckets is provided and the limit would be exceeded, returns nullopt.
577
  static absl::optional<std::set<int32_t>> nativeHistogramBucketIndicesFromHistogramBuckets(
578
      const std::vector<Stats::ParentHistogram::Bucket>& buckets, int8_t schema,
579
41
      double zero_threshold, absl::optional<uint32_t> max_buckets = absl::nullopt) {
580
41
    std::set<int32_t> indices;
581

            
582
41
    const double log_base = std::log(std::pow(2.0, std::pow(2.0, static_cast<double>(-schema))));
583

            
584
470
    for (const auto& bucket : buckets) {
585
470
      ASSERT(bucket.count_ > 0, "unexpected empty bucket");
586
470
      const double upper_bound = bucket.lower_bound_ + bucket.width_;
587
470
      if (upper_bound <= zero_threshold) {
588
9
        continue; // Entire bucket is in zero bucket range
589
9
      }
590

            
591
461
      ASSERT(bucket.lower_bound_ >= 0, "Envoy histograms only have unsigned integers recorded.");
592

            
593
      // Clamp lower bound to zero_threshold to prevent log(0).
594
461
      const double effective_lower = std::max(bucket.lower_bound_, zero_threshold);
595
      // Use ceil(...) - 1 to find the bucket containing effective_lower.
596
      // Prometheus bucket i covers (base^i, base^(i+1)], so value v is in bucket
597
      // ceil(log(v)/log(base)) - 1. This correctly handles boundary cases where
598
      // v = base^k exactly (it goes in bucket k-1, not k).
599
461
      const int32_t lower_index =
600
461
          static_cast<int32_t>(std::ceil(std::log(effective_lower) / log_base)) - 1;
601
461
      const int32_t upper_index = static_cast<int32_t>(std::ceil(std::log(upper_bound) / log_base));
602

            
603
1531
      for (int32_t idx = lower_index; idx <= upper_index; ++idx) {
604
1097
        indices.insert(idx);
605

            
606
        // Early termination if we've exceeded the limit
607
1097
        if (max_buckets.has_value() && indices.size() > *max_buckets) {
608
27
          return absl::nullopt;
609
27
        }
610
1097
      }
611
461
    }
612

            
613
14
    return indices;
614
41
  }
615

            
616
  // Write a varint-length-delimited protobuf message to the buffer.
617
964
  void writeDelimitedMessage(const Protobuf::MessageLite& message, Buffer::Instance& output) const {
618
964
    constexpr size_t kMaxVarintLength = 10; // This is documented, but not exported as a constant.
619

            
620
964
    const size_t length = message.ByteSizeLong();
621
964
    auto reservation = output.reserveSingleSlice(length + kMaxVarintLength);
622
964
    uint8_t* const reservation_start = reinterpret_cast<uint8_t*>(reservation.slice().mem_);
623

            
624
964
    uint8_t* const end_of_varint =
625
964
        Protobuf::io::CodedOutputStream::WriteVarint64ToArray(length, reservation_start);
626
964
    message.SerializeWithCachedSizesToArray(end_of_varint);
627

            
628
964
    ASSERT(end_of_varint >= reservation_start);
629
964
    const size_t varint_size = end_of_varint - reservation_start;
630
964
    ASSERT(varint_size <= kMaxVarintLength);
631
964
    reservation.commit(varint_size + length);
632
964
  }
633

            
634
  uint32_t native_histogram_max_buckets_{kDefaultMaxNativeHistogramBuckets};
635
};
636

            
637
/**
638
 * Processes a stat type (counter, gauge, histogram) by generating all output lines, sorting
639
 * them by tag-extracted metric name, and then outputting them in the correct sorted order into
640
 * response.
641
 *
642
 * @param response The buffer to put the output into.
643
 * @param used_only Whether to only output stats that are used.
644
 * @param regex A filter on which stats to output.
645
 * @param metrics The metrics to output stats for. This must contain all stats of the given type
646
 *        to be included in the same output.
647
 * @param generate_output A function which returns the output text for this metric.
648
 * @param type The name of the prometheus metric type for used in TYPE annotations.
649
 */
650
template <class StatType>
651
uint64_t outputStatType(Buffer::Instance& response, const StatsParams& params,
652
                        const std::vector<Stats::RefcountPtr<StatType>>& metrics,
653
                        const PrometheusStatsFormatter::OutputFormat& output_format,
654
256
                        const Stats::CustomStatNamespaces& custom_namespaces) {
655

            
656
  /*
657
   * From
658
   * https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#grouping-and-sorting:
659
   *
660
   * All lines for a given metric must be provided as one single group, with the optional HELP and
661
   * TYPE lines first (in no particular order). Beyond that, reproducible sorting in repeated
662
   * expositions is preferred but not required, i.e. do not sort if the computational cost is
663
   * prohibitive.
664
   */
665

            
666
  // This is an unsorted collection of dumb-pointers (no need to increment then decrement every
667
  // refcount; ownership is held throughout by `metrics`). It is unsorted for efficiency, but will
668
  // be sorted before producing the final output to satisfy the "preferred" ordering from the
669
  // prometheus spec: metrics will be sorted by their tags' textual representation, which will be
670
  // consistent across calls.
671
256
  using StatTypeUnsortedCollection = std::vector<const StatType*>;
672

            
673
  // Return early to avoid crashing when getting the symbol table from the first metric.
674
256
  if (metrics.empty()) {
675
149
    return 0;
676
149
  }
677

            
678
  // There should only be one symbol table for all of the stats in the admin
679
  // interface. If this assumption changes, the name comparisons in this function
680
  // will have to change to compare to convert all StatNames to strings before
681
  // comparison.
682
107
  const Stats::SymbolTable& global_symbol_table = metrics.front()->constSymbolTable();
683

            
684
  // Sorted collection of metrics sorted by their tagExtractedName, to satisfy the requirements
685
  // of the exposition format.
686
107
  std::map<Stats::StatName, StatTypeUnsortedCollection, Stats::StatNameLessThan> groups(
687
107
      global_symbol_table);
688

            
689
3690
  for (const auto& metric : metrics) {
690
3690
    ASSERT(&global_symbol_table == &metric->constSymbolTable());
691
3690
    if (!params.shouldShowMetric(*metric)) {
692
20
      continue;
693
20
    }
694
3670
    groups[metric->tagExtractedStatName()].push_back(metric.get());
695
3670
  }
696

            
697
107
  auto result = groups.size();
698
2879
  for (auto& group : groups) {
699
2879
    const absl::optional<std::string> prefixed_tag_extracted_name =
700
2879
        PrometheusStatsFormatter::metricName(global_symbol_table.toString(group.first),
701
2879
                                             custom_namespaces);
702
2879
    if (!prefixed_tag_extracted_name.has_value()) {
703
1
      --result;
704
1
      continue;
705
1
    }
706

            
707
    // Sort before producing the final output to satisfy the "preferred" ordering from the
708
    // prometheus spec: metrics will be sorted by their tags' textual representation, which will
709
    // be consistent across calls.
710
2878
    std::sort(group.second.begin(), group.second.end(), MetricLessThan());
711

            
712
2878
    output_format.generateOutput(response, group.second, prefixed_tag_extracted_name.value());
713
2878
  }
714
107
  return result;
715
256
}
716

            
717
template <class StatType, class OutputFormat>
718
uint64_t outputPrimitiveStatType(Buffer::Instance& response, const StatsParams& params,
719
                                 const std::vector<StatType>& metrics,
720
                                 const OutputFormat& output_format,
721
128
                                 const Stats::CustomStatNamespaces& custom_namespaces) {
722

            
723
  /*
724
   * From
725
   * https:*github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#grouping-and-sorting:
726
   *
727
   * All lines for a given metric must be provided as one single group, with the optional HELP and
728
   * TYPE lines first (in no particular order). Beyond that, reproducible sorting in repeated
729
   * expositions is preferred but not required, i.e. do not sort if the computational cost is
730
   * prohibitive.
731
   */
732

            
733
  // This is an unsorted collection of dumb-pointers (no need to increment then decrement every
734
  // refcount; ownership is held throughout by `metrics`). It is unsorted for efficiency, but will
735
  // be sorted before producing the final output to satisfy the "preferred" ordering from the
736
  // prometheus spec: metrics will be sorted by their tags' textual representation, which will be
737
  // consistent across calls.
738
128
  using StatTypeUnsortedCollection = std::vector<const StatType*>;
739

            
740
  // Return early to avoid crashing when getting the symbol table from the first metric.
741
128
  if (metrics.empty()) {
742
118
    return 0;
743
118
  }
744

            
745
  // Sorted collection of metrics sorted by their tagExtractedName, to satisfy the requirements
746
  // of the exposition format.
747
10
  std::map<std::string, StatTypeUnsortedCollection> groups;
748

            
749
45
  for (const auto& metric : metrics) {
750
45
    if (!params.shouldShowMetric(metric)) {
751
14
      continue;
752
14
    }
753
31
    groups[metric.tagExtractedName()].push_back(&metric);
754
31
  }
755

            
756
10
  auto result = groups.size();
757
11
  for (auto& group : groups) {
758
11
    const absl::optional<std::string> prefixed_tag_extracted_name =
759
11
        PrometheusStatsFormatter::metricName(group.first, custom_namespaces);
760
11
    if (!prefixed_tag_extracted_name.has_value()) {
761
      --result;
762
      continue;
763
    }
764

            
765
    // Sort before producing the final output to satisfy the "preferred" ordering from the
766
    // prometheus spec: metrics will be sorted by their tags' textual representation, which will
767
    // be consistent across calls.
768
11
    std::sort(group.second.begin(), group.second.end(), PrimitiveMetricSnapshotLessThan());
769

            
770
11
    output_format.generateOutput(response, group.second, prefixed_tag_extracted_name.value());
771
11
  }
772
10
  return result;
773
128
}
774

            
775
// Determine the format based on Accept header, using first-match priority.
776
// Per HTTP spec, clients SHOULD send media types in priority order.
777
// Text format is only selected if explicitly requested as version 0.0.4 or as fallback.
778
// Returns true if protobuf format should be used, false for text format.
779
22
bool useProtobufFormat(const StatsParams& params, const Http::RequestHeaderMap& headers) {
780
22
  bool use_protobuf = false; // Default to using the text format.
781

            
782
22
  if (auto prom_format = params.query_.getFirstValue("prom_protobuf"); prom_format.has_value()) {
783
1
    return true;
784
1
  }
785

            
786
  // Iterate through Accept headers in order and find the first supported format
787
21
  headers.get(Http::CustomHeaders::get().Accept)
788
21
      .iterate([&](const Http::HeaderEntry& accept_header) -> Http::HeaderMap::Iterate {
789
5
        absl::string_view accept_value = accept_header.value().getStringView();
790

            
791
        // Split by comma to handle multiple media types in one header
792
5
        std::vector<absl::string_view> media_types = absl::StrSplit(accept_value, ',');
793

            
794
5
        for (absl::string_view entry : media_types) {
795
          // Strip leading/trailing whitespace
796
5
          entry = absl::StripAsciiWhitespace(entry);
797

            
798
          // Extract the media type (before any semicolon)
799
5
          size_t semicolon_pos = entry.find(';');
800
5
          absl::string_view media_type =
801
5
              (semicolon_pos != absl::string_view::npos) ? entry.substr(0, semicolon_pos) : entry;
802

            
803
5
          if (media_type == "application/vnd.google.protobuf") {
804
4
            use_protobuf = true;
805
4
            return Http::HeaderMap::Iterate::Break;
806
4
          }
807

            
808
1
          if (media_type == "text/plain") {
809
1
            use_protobuf = false;
810
1
            return Http::HeaderMap::Iterate::Break;
811
1
          }
812
1
        }
813
        return Http::HeaderMap::Iterate::Continue;
814
5
      });
815

            
816
  // If no match found, default to text format for backward compatibility
817
21
  return use_protobuf;
818
22
}
819

            
820
} // namespace
821

            
822
2472
std::string PrometheusStatsFormatter::formattedTags(const std::vector<Stats::Tag>& tags) {
823
2472
  std::vector<std::string> buf;
824
2472
  buf.reserve(tags.size());
825
2507
  for (const Stats::Tag& tag : tags) {
826
2023
    buf.push_back(fmt::format("{}=\"{}\"", sanitizeName(tag.name_), sanitizeValue(tag.value_)));
827
2023
  }
828
2472
  return absl::StrJoin(buf, ",");
829
2472
}
830

            
831
absl::Status PrometheusStatsFormatter::validateParams(const StatsParams& params,
832
19
                                                      const Http::RequestHeaderMap& headers) {
833
19
  absl::Status result;
834
19
  switch (params.histogram_buckets_mode_) {
835
1
  case Utility::HistogramBucketsMode::Summary:
836
16
  case Utility::HistogramBucketsMode::Unset:
837
17
  case Utility::HistogramBucketsMode::Cumulative:
838
17
    result = absl::OkStatus();
839
17
    break;
840
1
  case Utility::HistogramBucketsMode::PrometheusNative:
841
1
    if (useProtobufFormat(params, headers)) {
842
      result = absl::OkStatus();
843
1
    } else {
844
1
      result = absl::InvalidArgumentError("unsupported prometheusnative histogram type when not "
845
1
                                          "using protobuf exposition format");
846
1
    }
847
1
    break;
848
  case Utility::HistogramBucketsMode::Detailed:
849
1
  case Utility::HistogramBucketsMode::Disjoint:
850
1
    result = absl::InvalidArgumentError("unsupported prometheus histogram bucket mode");
851
1
    break;
852
19
  }
853
19
  return result;
854
19
}
855

            
856
absl::optional<std::string>
857
PrometheusStatsFormatter::metricName(const std::string& extracted_name,
858
2895
                                     const Stats::CustomStatNamespaces& custom_namespaces) {
859
2895
  const absl::optional<absl::string_view> custom_namespace_stripped =
860
2895
      custom_namespaces.stripRegisteredPrefix(extracted_name);
861
2895
  if (custom_namespace_stripped.has_value()) {
862
    // This case the name has a custom namespace, and it is a custom metric.
863
5
    const std::string sanitized_name = sanitizeName(custom_namespace_stripped.value());
864
    // We expose these metrics without modifying (e.g. without "envoy_"),
865
    // so we have to check the "user-defined" stat name complies with the Prometheus naming
866
    // convention. Specifically the name must start with the "[a-zA-Z_]" pattern.
867
    // All the characters in sanitized_name are already in "[a-zA-Z0-9_]" pattern
868
    // thanks to sanitizeName above, so the only thing we have to do is check
869
    // if it does not start with digits.
870
5
    if (sanitized_name.empty() || absl::ascii_isdigit(sanitized_name.front())) {
871
2
      return absl::nullopt;
872
2
    }
873
3
    return sanitized_name;
874
5
  }
875

            
876
  // If it does not have a custom namespace, add namespacing prefix to avoid conflicts, as per best
877
  // practice: https://prometheus.io/docs/practices/naming/#metric-names Also, naming conventions on
878
  // https://prometheus.io/docs/concepts/data_model/
879
2890
  return absl::StrCat("envoy_", sanitizeName(extracted_name));
880
2895
}
881

            
882
uint64_t PrometheusStatsFormatter::generateWithOutputFormat(
883
    const std::vector<Stats::CounterSharedPtr>& counters,
884
    const std::vector<Stats::GaugeSharedPtr>& gauges,
885
    const std::vector<Stats::ParentHistogramSharedPtr>& histograms,
886
    const std::vector<Stats::TextReadoutSharedPtr>& text_readouts,
887
    const Upstream::ClusterManager& cluster_manager, Buffer::Instance& response,
888
    const StatsParams& params, const Stats::CustomStatNamespaces& custom_namespaces,
889
64
    OutputFormat& output_format) {
890

            
891
64
  OutputFormat::HistogramType hist_type;
892

            
893
  // Validation of bucket modes is handled separately.
894
64
  switch (params.histogram_buckets_mode_) {
895
3
  case Utility::HistogramBucketsMode::Summary:
896
3
    hist_type = OutputFormat::HistogramType::Summary;
897
3
    break;
898
45
  case Utility::HistogramBucketsMode::Unset:
899
46
  case Utility::HistogramBucketsMode::Cumulative:
900
46
    hist_type = OutputFormat::HistogramType::ClassicHistogram;
901
46
    break;
902
15
  case Utility::HistogramBucketsMode::PrometheusNative:
903
15
    hist_type = OutputFormat::HistogramType::NativeHistogram;
904
15
    break;
905
  // "Detailed" and "Disjoint" don't make sense for prometheus histogram semantics. These types were
906
  // have been filtered out in validateParams().
907
  case Utility::HistogramBucketsMode::Detailed:
908
  case Utility::HistogramBucketsMode::Disjoint:
909
    hist_type = OutputFormat::HistogramType::ClassicHistogram;
910
    IS_ENVOY_BUG("unsupported prometheus histogram bucket mode");
911
    break;
912
64
  }
913

            
914
64
  output_format.setHistogramType(hist_type);
915

            
916
64
  uint64_t metric_name_count = 0;
917
64
  metric_name_count +=
918
64
      outputStatType<Stats::Counter>(response, params, counters, output_format, custom_namespaces);
919

            
920
64
  metric_name_count +=
921
64
      outputStatType<Stats::Gauge>(response, params, gauges, output_format, custom_namespaces);
922

            
923
64
  metric_name_count += outputStatType<Stats::TextReadout>(response, params, text_readouts,
924
64
                                                          output_format, custom_namespaces);
925

            
926
64
  metric_name_count += outputStatType<Stats::ParentHistogram>(response, params, histograms,
927
64
                                                              output_format, custom_namespaces);
928

            
929
  // Note: This assumes that there is no overlap in stat name between per-endpoint stats and all
930
  // other stats. If this is not true, then the counters/gauges for per-endpoint need to be combined
931
  // with the above counter/gauge calls so that stats can be properly grouped.
932
64
  std::vector<Stats::PrimitiveCounterSnapshot> host_counters;
933
64
  std::vector<Stats::PrimitiveGaugeSnapshot> host_gauges;
934
64
  Upstream::HostUtility::forEachHostMetric(
935
64
      cluster_manager,
936
64
      [&](Stats::PrimitiveCounterSnapshot&& metric) {
937
18
        host_counters.emplace_back(std::move(metric));
938
18
      },
939
64
      [&](Stats::PrimitiveGaugeSnapshot&& metric) { host_gauges.emplace_back(std::move(metric)); });
940

            
941
64
  metric_name_count +=
942
64
      outputPrimitiveStatType(response, params, host_counters, output_format, custom_namespaces);
943
64
  metric_name_count +=
944
64
      outputPrimitiveStatType(response, params, host_gauges, output_format, custom_namespaces);
945

            
946
64
  return metric_name_count;
947
64
}
948

            
949
uint64_t PrometheusStatsFormatter::statsAsPrometheusText(
950
    const std::vector<Stats::CounterSharedPtr>& counters,
951
    const std::vector<Stats::GaugeSharedPtr>& gauges,
952
    const std::vector<Stats::ParentHistogramSharedPtr>& histograms,
953
    const std::vector<Stats::TextReadoutSharedPtr>& text_readouts,
954
    const Upstream::ClusterManager& cluster_manager, Buffer::Instance& response,
955
36
    const StatsParams& params, const Stats::CustomStatNamespaces& custom_namespaces) {
956

            
957
36
  TextFormat output_format;
958
36
  return generateWithOutputFormat(counters, gauges, histograms, text_readouts, cluster_manager,
959
36
                                  response, params, custom_namespaces, output_format);
960
36
}
961

            
962
uint64_t PrometheusStatsFormatter::statsAsPrometheusProtobuf(
963
    const std::vector<Stats::CounterSharedPtr>& counters,
964
    const std::vector<Stats::GaugeSharedPtr>& gauges,
965
    const std::vector<Stats::ParentHistogramSharedPtr>& histograms,
966
    const std::vector<Stats::TextReadoutSharedPtr>& text_readouts,
967
    const Upstream::ClusterManager& cluster_manager, Http::ResponseHeaderMap& response_headers,
968
    Buffer::Instance& response, const StatsParams& params,
969
28
    const Stats::CustomStatNamespaces& custom_namespaces) {
970

            
971
28
  response_headers.setReferenceContentType(
972
28
      "application/vnd.google.protobuf; "
973
28
      "proto=io.prometheus.client.MetricFamily; encoding=delimited");
974

            
975
28
  ProtobufFormat output_format(params.native_histogram_max_buckets_);
976
28
  return generateWithOutputFormat(counters, gauges, histograms, text_readouts, cluster_manager,
977
28
                                  response, params, custom_namespaces, output_format);
978
28
}
979

            
980
uint64_t PrometheusStatsFormatter::statsAsPrometheus(
981
    const std::vector<Stats::CounterSharedPtr>& counters,
982
    const std::vector<Stats::GaugeSharedPtr>& gauges,
983
    const std::vector<Stats::ParentHistogramSharedPtr>& histograms,
984
    const std::vector<Stats::TextReadoutSharedPtr>& text_readouts,
985
    const Upstream::ClusterManager& cluster_manager, const Http::RequestHeaderMap& request_headers,
986
    Http::ResponseHeaderMap& response_headers, Buffer::Instance& response,
987
21
    const StatsParams& params, const Stats::CustomStatNamespaces& custom_namespaces) {
988

            
989
21
  return useProtobufFormat(params, request_headers)
990
21
             ? statsAsPrometheusProtobuf(counters, gauges, histograms, text_readouts,
991
5
                                         cluster_manager, response_headers, response, params,
992
5
                                         custom_namespaces)
993
21
             : statsAsPrometheusText(counters, gauges, histograms, text_readouts, cluster_manager,
994
16
                                     response, params, custom_namespaces);
995
21
}
996

            
997
} // namespace Server
998
} // namespace Envoy