KUDU-3375 Expose Prometheus Metrics in Kudu

This patch introduces an endpoint which emits server level metrics in
Prometheus Text Explosion format for Kudu. The endpoint can be found at
/metrics_prometheus path of the webserver.

More information on the Prometheus format can be found at:
https://prometheus.io/docs/instrumenting/exposition_formats

Prefix kudu_ has been added to all of the metric names according to
Prometheus naming convention to specify the domain and differentiate
from any other metrics belonging to different services. Following the
kudu_ prefix either master_ or tserver_ prefix is attached based on the
server type that is exposing the metrics. In order to differentiate
between different master and tablet servers running on the same host
Prometheus automatically attaches an instance label which holds the
"hostname:portname".

eg: kudu_master_metric{instance="localhost:8765", job="kudu-master", unit_type="unit"}

Not all of the Kudu metric names follow the Prometheus metric naming
recommendations which state metric units should be attached at the end
of the metric name. In Kudu some metrics have these units at the end of
the metric name, some in the middle of the metric and for some metrics
it is unclear what the metric unit is just by looking at the metric
name. Due to this and also because Kudu supports more Metric Units
than base Prometheus Metric units, metric units are represented as
Prometheus labels for each metric. This also makes it possible to keep
time based units more accurate as Milliseconds, Nanoseconds and etc.
do not need to be converted to seconds which is the base time unit in
Prometheus.

More information on Prometheus Metric units and naming conventions
can be found at:
https://prometheus.io/docs/practices/naming/#metric-names

Tested by feeding all of the generated metrics to a prometheus server
running on localhost. Added unit tests to each metric type.

Change-Id: I4b2d01bd56f6f0f1b6f31cbce15e671d16521739
Reviewed-on: http://gerrit.cloudera.org:8080/19133
Tested-by: Kudu Jenkins
Reviewed-by: Alexey Serbin <alexey@apache.org>
diff --git a/src/kudu/server/default_path_handlers.cc b/src/kudu/server/default_path_handlers.cc
index 682db40..7b2ecc2 100644
--- a/src/kudu/server/default_path_handlers.cc
+++ b/src/kudu/server/default_path_handlers.cc
@@ -25,11 +25,13 @@
 #include <functional>
 #include <memory>
 #include <string>
+#include <type_traits>
 #include <unordered_map>
 #include <vector>
 
 #include <gflags/gflags.h>
 #include <glog/logging.h>
+#include "kudu/util/prometheus_writer.h"
 #include "kudu/util/version_info.h"
 #include "kudu/util/version_info.pb.h"
 
@@ -510,6 +512,13 @@
   }
 }
 
+static void WriteMetricsAsPrometheus(const MetricRegistry* const metrics,
+                                     const Webserver::WebRequest& /*req*/,
+                                     Webserver::PrerenderedWebResponse* resp) {
+  PrometheusWriter writer(&resp->output);
+  WARN_NOT_OK(metrics->WriteAsPrometheus(&writer), "Couldn't write Prometheus metrics over HTTP");
+}
+
 void RegisterMetricsJsonHandler(Webserver* webserver, const MetricRegistry* const metrics) {
   auto callback = [metrics](const Webserver::WebRequest& req,
                             Webserver::PrerenderedWebResponse* resp) {
@@ -518,7 +527,7 @@
   bool not_styled = false;
   bool not_on_nav_bar = false;
   bool is_on_nav_bar = true;
-  webserver->RegisterPrerenderedPathHandler("/metrics", "Metrics", callback,
+  webserver->RegisterPrerenderedPathHandler("/metrics", "JSON Metrics", callback,
                                             not_styled, is_on_nav_bar);
 
   // The old name -- this is preserved for compatibility with older releases of
@@ -527,4 +536,15 @@
                                             not_styled, not_on_nav_bar);
 }
 
+void RegisterMetricsPrometheusHandler(Webserver* webserver, const MetricRegistry* const metrics) {
+  auto callback = [metrics](const Webserver::WebRequest& req,
+                            Webserver::PrerenderedWebResponse* resp) {
+    WriteMetricsAsPrometheus(metrics, req, resp);
+  };
+  constexpr bool not_styled = false;
+  constexpr bool is_on_nav_bar = true;
+  webserver->RegisterPrerenderedPathHandler("/metrics_prometheus", "Prometheus Metrics", callback,
+                                            not_styled, is_on_nav_bar);
+}
+
 } // namespace kudu
diff --git a/src/kudu/server/default_path_handlers.h b/src/kudu/server/default_path_handlers.h
index 1a58915..aadf00f 100644
--- a/src/kudu/server/default_path_handlers.h
+++ b/src/kudu/server/default_path_handlers.h
@@ -32,8 +32,10 @@
 void AddPostInitializedDefaultPathHandlers(Webserver* webserver);
 
 // Adds an endpoint to get metrics in JSON format.
-void RegisterMetricsJsonHandler(Webserver* webserver, const MetricRegistry* const metrics);
+void RegisterMetricsJsonHandler(Webserver* webserver, const MetricRegistry* metrics);
 
+// Adds an endpoint to get metrics in Prometheus format.
+void RegisterMetricsPrometheusHandler(Webserver* webserver, const MetricRegistry* metrics);
 } // namespace kudu
 
 #endif // KUDU_SERVER_DEFAULT_PATH_HANDLERS_H
diff --git a/src/kudu/server/server_base.cc b/src/kudu/server/server_base.cc
index b3aee09..5c90295 100644
--- a/src/kudu/server/server_base.cc
+++ b/src/kudu/server/server_base.cc
@@ -969,6 +969,7 @@
     AddPostInitializedDefaultPathHandlers(web_server_.get());
     AddRpczPathHandlers(messenger_, web_server_.get());
     RegisterMetricsJsonHandler(web_server_.get(), metric_registry_.get());
+    RegisterMetricsPrometheusHandler(web_server_.get(), metric_registry_.get());
     TracingPathHandlers::RegisterHandlers(web_server_.get());
     web_server_->set_footer_html(FooterHtml());
     web_server_->SetStartupComplete(true);
diff --git a/src/kudu/util/CMakeLists.txt b/src/kudu/util/CMakeLists.txt
index e1713e1..3a1fab5 100644
--- a/src/kudu/util/CMakeLists.txt
+++ b/src/kudu/util/CMakeLists.txt
@@ -221,6 +221,7 @@
   pb_util.cc
   pb_util-internal.cc
   process_memory.cc
+  prometheus_writer.cc
   random_util.cc
   rolling_log.cc
   rw_mutex.cc
diff --git a/src/kudu/util/metrics-test.cc b/src/kudu/util/metrics-test.cc
index e80a2d5..2ab2d15 100644
--- a/src/kudu/util/metrics-test.cc
+++ b/src/kudu/util/metrics-test.cc
@@ -41,6 +41,7 @@
 #include "kudu/util/jsonwriter.h"
 #include "kudu/util/monotime.h"
 #include "kudu/util/oid_generator.h"
+#include "kudu/util/prometheus_writer.h"
 #include "kudu/util/random.h"
 #include "kudu/util/status.h"
 #include "kudu/util/test_macros.h"
@@ -131,6 +132,22 @@
   ASSERT_EQ(0, c->value());
 }
 
+TEST_F(MetricsTest, CounterPrometheusTest) {
+  scoped_refptr<Counter> requests =
+    new Counter(&METRIC_test_counter);
+
+  std::ostringstream output;
+  string prefix;
+  PrometheusWriter writer(&output);
+  ASSERT_OK(requests->WriteAsPrometheus(&writer, prefix));
+
+  const string expected_output = "# HELP test_counter Description of test counter\n"
+                                 "# TYPE test_counter counter\n"
+                                 "test_counter{unit_type=\"requests\"} 0\n";
+
+  ASSERT_EQ(expected_output, output.str());
+}
+
 METRIC_DEFINE_gauge_string(test_entity, test_string_gauge, "Test string Gauge",
                            MetricUnit::kState, "Description of string Gauge",
                            kudu::MetricLevel::kInfo);
@@ -186,6 +203,18 @@
             state_for_merge->unique_values());
 }
 
+TEST_F(MetricsTest, StringGaugePrometheusTest) {
+  scoped_refptr<StringGauge> state =
+    new StringGauge(&METRIC_test_string_gauge, "Healthy");
+
+  std::ostringstream output;
+  string prefix;
+  PrometheusWriter writer(&output);
+  ASSERT_OK(state->WriteAsPrometheus(&writer, prefix));
+  // String Gauges are not representable in Prometheus, empty output is expected
+  ASSERT_EQ("", output.str());
+}
+
 METRIC_DEFINE_gauge_double(test_entity, test_mean_gauge, "Test mean Gauge",
                            MetricUnit::kUnits, "Description of mean Gauge",
                            kudu::MetricLevel::kInfo);
@@ -260,10 +289,33 @@
   ASSERT_EQ(value, kTotalSum/kTotalCount);
 }
 
+
+TEST_F(MetricsTest, MeanGaugePrometheusTest) {
+  scoped_refptr<MeanGauge> average_usage =
+    METRIC_test_mean_gauge.InstantiateMeanGauge(entity_);
+
+  std::ostringstream output;
+  string prefix;
+  PrometheusWriter writer(&output);
+  ASSERT_OK(average_usage->WriteAsPrometheus(&writer, prefix));
+
+  const string expected_output = "# HELP test_mean_gauge Description of mean Gauge\n"
+                                 "# TYPE test_mean_gauge gauge\n"
+                                 "test_mean_gauge{unit_type=\"units\"} 0\n"
+                                 "test_mean_gauge_count{unit_type=\"units\"} 0\n"
+                                 "test_mean_gauge_sum{unit_type=\"units\"} 0\n";
+
+  ASSERT_EQ(expected_output, output.str());
+}
+
 METRIC_DEFINE_gauge_uint64(test_entity, test_gauge, "Test uint64 Gauge",
                            MetricUnit::kBytes, "Description of Test Gauge",
                            kudu::MetricLevel::kInfo);
 
+METRIC_DEFINE_gauge_bool(test_entity, test_gauge_bool, "Test boolean Gauge",
+                           MetricUnit::kState, "Description of Test boolean Gauge",
+                           kudu::MetricLevel::kInfo);
+
 TEST_F(MetricsTest, SimpleAtomicGaugeTest) {
   scoped_refptr<AtomicGauge<uint64_t> > mem_usage =
     METRIC_test_gauge.Instantiate(entity_, 0);
@@ -323,6 +375,38 @@
   ASSERT_EQ(1, start_time_for_merge->value());
 }
 
+TEST_F(MetricsTest, AtomicGaugePrometheusTest) {
+  scoped_refptr<AtomicGauge<uint64_t> > mem_usage =
+    METRIC_test_gauge.Instantiate(entity_, 0);
+
+  std::ostringstream output;
+  string prefix;
+  PrometheusWriter writer(&output);
+  ASSERT_OK(mem_usage->WriteAsPrometheus(&writer, prefix));
+
+  const string expected_output = "# HELP test_gauge Description of Test Gauge\n"
+                                 "# TYPE test_gauge gauge\n"
+                                 "test_gauge{unit_type=\"bytes\"} 0\n";
+
+  ASSERT_EQ(expected_output, output.str());
+}
+
+TEST_F(MetricsTest, AtomicGaugeBooleanPrometheusTest) {
+  scoped_refptr<AtomicGauge<bool> > clock_extrapolating =
+    METRIC_test_gauge_bool.Instantiate(entity_, false);
+
+  std::ostringstream output;
+  string prefix;
+  PrometheusWriter writer(&output);
+  ASSERT_OK(clock_extrapolating->WriteAsPrometheus(&writer, prefix));
+
+  const string expected_output = "# HELP test_gauge_bool Description of Test boolean Gauge\n"
+                                 "# TYPE test_gauge_bool gauge\n"
+                                 "test_gauge_bool{unit_type=\"state\"} 0\n";
+
+  ASSERT_EQ(expected_output, output.str());
+}
+
 METRIC_DEFINE_gauge_int64(test_entity, test_func_gauge, "Test Function Gauge",
                           MetricUnit::kBytes, "Test Gauge 2",
                           kudu::MetricLevel::kInfo);
@@ -462,6 +546,24 @@
   ASSERT_EQ(12345, gauge->value());
 }
 
+TEST_F(MetricsTest, FunctionGaugePrometheusTest) {
+  int metric_val = 1000;
+  scoped_refptr<FunctionGauge<int64_t> > gauge =
+    METRIC_test_func_gauge.InstantiateFunctionGauge(
+        entity_, [&metric_val]() { return MyFunction(&metric_val); });
+
+  std::ostringstream output;
+  string prefix;
+  PrometheusWriter writer(&output);
+  ASSERT_OK(gauge->WriteAsPrometheus(&writer, prefix));
+
+  const string expected_output = "# HELP test_func_gauge Test Gauge 2\n"
+                                 "# TYPE test_func_gauge gauge\n"
+                                 "test_func_gauge{unit_type=\"bytes\"} 1000\n";
+
+  ASSERT_EQ(expected_output, output.str());
+}
+
 METRIC_DEFINE_gauge_uint64(test_entity, counter_as_gauge, "Gauge exposed as Counter",
                            MetricUnit::kBytes, "Gauge exposed as Counter",
                            kudu::MetricLevel::kInfo,
@@ -515,6 +617,28 @@
   ASSERT_EQ(18, hist_for_merge->histogram()->TotalSum());
 }
 
+TEST_F(MetricsTest, HistogramPrometheusTest) {
+  scoped_refptr<Histogram> hist = METRIC_test_hist.Instantiate(entity_);
+
+  std::ostringstream output;
+  PrometheusWriter writer(&output);
+  string prefix;
+  ASSERT_OK(hist->WriteAsPrometheus(&writer, prefix));
+
+  const string expected_output = "# HELP test_hist foo\n"
+                                 "# TYPE test_hist histogram\n"
+                                 "test_hist_bucket{unit_type=\"milliseconds\", le=\"0.75\"} 0\n"
+                                 "test_hist_bucket{unit_type=\"milliseconds\", le=\"0.95\"} 0\n"
+                                 "test_hist_bucket{unit_type=\"milliseconds\", le=\"0.99\"} 0\n"
+                                 "test_hist_bucket{unit_type=\"milliseconds\", le=\"0.999\"} 0\n"
+                                 "test_hist_bucket{unit_type=\"milliseconds\", le=\"0.9999\"} 0\n"
+                                 "test_hist_bucket{unit_type=\"milliseconds\", le=\"+Inf\"} 0\n"
+                                 "test_hist_sum{unit_type=\"milliseconds\"} 0\n"
+                                 "test_hist_count{unit_type=\"milliseconds\"} 0\n";
+
+  ASSERT_EQ(expected_output, output.str());
+}
+
 TEST_F(MetricsTest, JsonPrintTest) {
   scoped_refptr<Counter> test_counter = METRIC_test_counter.Instantiate(entity_);
   test_counter->Increment();
diff --git a/src/kudu/util/metrics.cc b/src/kudu/util/metrics.cc
index 663ffe6..d60cde0 100644
--- a/src/kudu/util/metrics.cc
+++ b/src/kudu/util/metrics.cc
@@ -48,6 +48,7 @@
 using std::unordered_set;
 using std::vector;
 using strings::Substitute;
+using strings::SubstituteAndAppend;
 
 template<typename Collection>
 void WriteMetricsToJson(JsonWriter* writer,
@@ -68,6 +69,16 @@
   writer->EndArray();
 }
 
+template<typename Collection>
+void WriteMetricsToPrometheus(PrometheusWriter* writer,
+                              const Collection& metrics,
+                              const std::string& prefix) {
+  for (const auto& [name, val] : metrics) {
+    WARN_NOT_OK(val->WriteAsPrometheus(writer, prefix),
+                Substitute("Failed to write $0 as Prometheus", name));
+  }
+}
+
 void WriteToJson(JsonWriter* writer,
                  const MergedEntityMetrics &merged_entity_metrics,
                  const MetricJsonOptions &opts) {
@@ -392,6 +403,40 @@
   return Status::OK();
 }
 
+Status MetricEntity::WriteAsPrometheus(PrometheusWriter* writer) const {
+  MetricMap metrics;
+  AttributeMap attrs;
+  MetricFilters filters;
+  filters.entity_level = "debug";
+  const string master_prefix = "kudu_master_";
+  const string tserver_prefix = "kudu_tserver_";
+  const string master_server = "kudu.master";
+  const string tablet_server = "kudu.tabletserver";
+  // Empty filters results in getting all the metrics for this MetricEntity.
+  const auto s = GetMetricsAndAttrs(filters, &metrics, &attrs);
+  if (s.IsNotFound()) {
+    // Status::NotFound is returned when this entity has been filtered, treat it
+    // as OK, and skip printing it.
+    return Status::OK();
+  }
+  RETURN_NOT_OK(s);
+  // Only emit server level metrics
+  if (strcmp(prototype_->name(), "server") == 0) {
+    if (id_ == master_server) {
+      // attach kudu_master_ as prefix to metrics
+      WriteMetricsToPrometheus(writer, metrics, master_prefix);
+      return Status::OK();
+    }
+    if (id_ == tablet_server) {
+      // attach kudu_tserver_ as prefix to metrics
+      WriteMetricsToPrometheus(writer, metrics, tserver_prefix);
+      return Status::OK();
+    }
+  }
+
+  return Status::NotFound("Entity is not relevant to Prometheus");
+}
+
 Status MetricEntity::CollectTo(MergedEntityMetrics* collections,
                                const MetricFilters& filters,
                                const MetricMergeRules& merge_rules) const {
@@ -540,6 +585,22 @@
   return Status::OK();
 }
 
+Status MetricRegistry::WriteAsPrometheus(PrometheusWriter* writer) const {
+  EntityMap entities;
+  {
+    std::lock_guard<simple_spinlock> l(lock_);
+    entities = entities_;
+  }
+  for (const auto& e : entities) {
+    WARN_NOT_OK(e.second->WriteAsPrometheus(writer),
+                Substitute("Failed to write entity $0 as Prometheus", e.second->id()));
+  }
+
+  entities.clear(); // necessary to deref metrics we just dumped before doing retirement scan.
+  const_cast<MetricRegistry*>(this)->RetireOldMetrics();
+  return Status::OK();
+}
+
 void MetricRegistry::RetireOldMetrics() {
   std::lock_guard<simple_spinlock> l(lock_);
   for (auto it = entities_.begin(); it != entities_.end();) {
@@ -674,6 +735,12 @@
   }
 }
 
+void MetricPrototype::WriteFields(PrometheusWriter* writer, const string &prefix) const {
+  writer->WriteEntry(Substitute("# HELP $0$1 $2\n# TYPE $3$4 $5\n",
+                                prefix, name(), description(),
+                                prefix, name(), MetricType::Name(type())));
+}
+
 //
 // FunctionGaugeDetacher
 //
@@ -751,6 +818,14 @@
   return Status::OK();
 }
 
+Status Gauge::WriteAsPrometheus(PrometheusWriter* writer, const std::string& prefix) const {
+  prototype_->WriteFields(writer, prefix);
+
+  WriteValue(writer, prefix);
+
+  return Status::OK();
+}
+
 //
 // StringGauge
 //
@@ -822,6 +897,24 @@
   writer->String(value());
 }
 
+// A string gauge's value can be anything, but Prometheus does not support
+// non-numeric values for gauges with exception of {+,-}Inf and NaN
+// (see https://prometheus.io/docs/instrumenting/exposition_formats/).
+// DCHECK() is added to make sure this method is not called from anywhere,
+// but overriding it is necessary since Gauge::WriteValue() is a pure virtual one.
+// An alternative could be defining a empty implementation for Gauge::WriteValue()
+// virtual method and not adding this empty override here.
+void StringGauge::WriteValue(PrometheusWriter* writer, const std::string& prefix) const {
+  DCHECK(false);
+}
+
+Status StringGauge::WriteAsPrometheus(PrometheusWriter* /*writer*/,
+                                      const std::string& /*prefix*/) const {
+  // Prometheus doesn't support string gauges.
+  // This function ensures that output written to Prometheus is empty.
+  return Status::OK();
+}
+
 //
 // MeanGauge
 //
@@ -883,6 +976,21 @@
   writer->Double(total_count());
 }
 
+void MeanGauge::WriteValue(PrometheusWriter* writer, const std::string& prefix) const {
+  auto output = Substitute("$0$1{unit_type=\"$2\"} $3\n", prefix, prototype_->name(),
+                           MetricUnit::Name(prototype_->unit()), value());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\"} $4\n",
+                       prefix, prototype_->name(), "_count",
+                       MetricUnit::Name(prototype_->unit()), total_count());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\"} $4\n",
+                       prefix, prototype_->name(), "_sum",
+                       MetricUnit::Name(prototype_->unit()), total_sum());
+
+  writer->WriteEntry(output);
+}
+
 //
 // Counter
 //
@@ -921,6 +1029,15 @@
   return Status::OK();
 }
 
+Status Counter::WriteAsPrometheus(PrometheusWriter* writer, const std::string& prefix) const {
+  prototype_->WriteFields(writer, prefix);
+
+  writer->WriteEntry(Substitute("$0$1{unit_type=\"$2\"} $3\n", prefix, prototype_->name(),
+                                MetricUnit::Name(prototype_->unit()), value()));
+
+  return Status::OK();
+}
+
 /////////////////////////////////////////////////
 // HistogramPrototype
 /////////////////////////////////////////////////
@@ -977,6 +1094,61 @@
   return Status::OK();
 }
 
+Status Histogram::WriteAsPrometheus(PrometheusWriter* writer, const std::string& prefix) const {
+  prototype_->WriteFields(writer, prefix);
+  std::string output = "";
+  MetricJsonOptions opts;
+  // Snapshot is taken to preserve the consistency across metrics and
+  // requirements given by Prometheus. The value for the _bucket in +Inf
+  // quantile needs to be equal to the total _count
+  HistogramSnapshotPB snapshot;
+  RETURN_NOT_OK(GetHistogramSnapshotPB(&snapshot, opts));
+
+  output = Substitute("$0$1$2{unit_type=\"$3\", le=\"0.75\"} $4\n",
+                       prefix, prototype_->name(), "_bucket",
+                       MetricUnit::Name(prototype_->unit()),
+                       snapshot.percentile_75());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\", le=\"0.95\"} $4\n",
+                       prefix, prototype_->name(), "_bucket",
+                       MetricUnit::Name(prototype_->unit()),
+                       snapshot.percentile_95());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\", le=\"0.99\"} $4\n",
+                       prefix, prototype_->name(), "_bucket",
+                       MetricUnit::Name(prototype_->unit()),
+                       snapshot.percentile_99());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\", le=\"0.999\"} $4\n",
+                       prefix, prototype_->name(), "_bucket",
+                       MetricUnit::Name(prototype_->unit()),
+                       snapshot.percentile_99_9());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\", le=\"0.9999\"} $4\n",
+                       prefix, prototype_->name(), "_bucket",
+                       MetricUnit::Name(prototype_->unit()),
+                       snapshot.percentile_99_99());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\", le=\"+Inf\"} $4\n",
+                       prefix, prototype_->name(), "_bucket",
+                       MetricUnit::Name(prototype_->unit()),
+                       snapshot.total_count());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\"} $4\n",
+                       prefix, prototype_->name(), "_sum",
+                       MetricUnit::Name(prototype_->unit()),
+                       snapshot.total_sum());
+
+  SubstituteAndAppend(&output, "$0$1$2{unit_type=\"$3\"} $4\n",
+                       prefix, prototype_->name(), "_count",
+                       MetricUnit::Name(prototype_->unit()),
+                       snapshot.total_count());
+
+  writer->WriteEntry(output);
+
+  return Status::OK();
+}
+
 Status Histogram::GetHistogramSnapshotPB(HistogramSnapshotPB* snapshot_pb,
                                          const MetricJsonOptions& opts) const {
   snapshot_pb->set_name(prototype_->name());
diff --git a/src/kudu/util/metrics.h b/src/kudu/util/metrics.h
index db8bccb..28e49b8 100644
--- a/src/kudu/util/metrics.h
+++ b/src/kudu/util/metrics.h
@@ -249,6 +249,7 @@
 #include "kudu/gutil/map-util.h"
 #include "kudu/gutil/port.h"
 #include "kudu/gutil/ref_counted.h"
+#include "kudu/gutil/strings/substitute.h"
 #include "kudu/util/atomic.h"
 #include "kudu/util/hdr_histogram.h"
 #include "kudu/util/jsonwriter.h" // IWYU pragma: keep
@@ -256,6 +257,7 @@
 #include "kudu/util/monotime.h"
 #include "kudu/util/status.h"
 #include "kudu/util/striped64.h"
+#include "kudu/util/prometheus_writer.h"
 
 // Define a new entity type.
 //
@@ -581,6 +583,8 @@
   void WriteFields(JsonWriter* writer,
                    const MetricJsonOptions& opts) const;
 
+  void WriteFields(PrometheusWriter* writer, const std::string& prefix) const;
+
  protected:
   explicit MetricPrototype(CtorArgs args);
   virtual ~MetricPrototype() {
@@ -665,6 +669,8 @@
   // See MetricRegistry::WriteAsJson()
   Status WriteAsJson(JsonWriter* writer, const MetricJsonOptions& opts) const;
 
+  Status WriteAsPrometheus(PrometheusWriter* writer) const;
+
   // Collect metrics of this entity to 'collections'. Metrics will be filtered by 'filters',
   // and will be merged under the rule of 'merge_rules'.
   Status CollectTo(MergedEntityMetrics* collections,
@@ -756,6 +762,8 @@
   // All metrics must be able to render themselves as JSON.
   virtual Status WriteAsJson(JsonWriter* writer,
                              const MetricJsonOptions& opts) const = 0;
+  // All metrics must be able to render themselves as Prometheus.
+  virtual Status WriteAsPrometheus(PrometheusWriter* writer, const std::string& prefix) const = 0;
 
   const MetricPrototype* prototype() const { return prototype_; }
 
@@ -877,6 +885,8 @@
   // output of this function.
   Status WriteAsJson(JsonWriter* writer, const MetricJsonOptions& opts) const;
 
+  // Writes metrics in this registry to given Prometheus 'writer'.
+  Status WriteAsPrometheus(PrometheusWriter* writer) const;
   // For each registered entity, retires orphaned metrics. If an entity has no more
   // metrics and there are no external references, entities are removed as well.
   //
@@ -1015,12 +1025,13 @@
   explicit Gauge(const MetricPrototype* prototype)
     : Metric(prototype) {
   }
-  virtual ~Gauge() {}
-  virtual Status WriteAsJson(JsonWriter* w,
-                             const MetricJsonOptions& opts) const OVERRIDE;
+  ~Gauge() override {}
+  Status WriteAsJson(JsonWriter* w, const MetricJsonOptions& opts) const OVERRIDE;
 
+  Status WriteAsPrometheus(PrometheusWriter* w, const std::string& prefix) const OVERRIDE;
  protected:
   virtual void WriteValue(JsonWriter* writer) const = 0;
+  virtual void WriteValue(PrometheusWriter* writer, const std::string& prefix) const = 0;
  private:
   DISALLOW_COPY_AND_ASSIGN(Gauge);
 };
@@ -1042,7 +1053,10 @@
 
  protected:
   FRIEND_TEST(MetricsTest, SimpleStringGaugeForMergeTest);
-  virtual void WriteValue(JsonWriter* writer) const OVERRIDE;
+  FRIEND_TEST(MetricsTest, StringGaugePrometheusTest);
+  Status WriteAsPrometheus(PrometheusWriter* w, const std::string& prefix) const OVERRIDE;
+  void WriteValue(JsonWriter* writer) const OVERRIDE;
+  void WriteValue(PrometheusWriter* writer, const std::string& prefix) const OVERRIDE;
   void FillUniqueValuesUnlocked();
   std::unordered_set<std::string> unique_values();
  private:
@@ -1071,7 +1085,8 @@
   void MergeFrom(const scoped_refptr<Metric>& other) override;
 
  protected:
-  virtual void WriteValue(JsonWriter* writer) const override;
+  void WriteValue(JsonWriter* writer) const override;
+  void WriteValue(PrometheusWriter* writer, const std::string& prefix) const override;
  private:
   double total_sum_;
   double total_count_;
@@ -1144,9 +1159,29 @@
     }
   }
  protected:
-  virtual void WriteValue(JsonWriter* writer) const OVERRIDE {
+  void WriteValue(JsonWriter* writer) const OVERRIDE {
     writer->Value(value());
   }
+
+  void WriteValue(PrometheusWriter* writer,const std::string& prefix) const OVERRIDE {
+    std::string output = "";
+
+    // If Boolean Gauge, convert false/true to 0/1 for Prometheus
+    if constexpr (std::is_same_v<T, bool> ) {
+      int check = 0;
+      if (value() == true) {
+        check = 1;
+      } else {
+        check = 0;
+      }
+      output = strings::Substitute("$0$1{unit_type=\"$2\"} $3\n", prefix, prototype_->name(),
+                                   MetricUnit::Name(prototype_->unit()), check);
+    } else {
+      output = strings::Substitute("$0$1{unit_type=\"$2\"} $3\n", prefix, prototype_->name(),
+                                   MetricUnit::Name(prototype_->unit()), value());
+    }
+    writer->WriteEntry(output);
+  }
  private:
   AtomicInt<int64_t> value_;
   MergeType type_;
@@ -1235,10 +1270,28 @@
     return function_();
   }
 
-  virtual void WriteValue(JsonWriter* writer) const OVERRIDE {
+  void WriteValue(JsonWriter* writer) const OVERRIDE {
     writer->Value(value());
   }
 
+  void WriteValue(PrometheusWriter* writer, const std::string& prefix) const OVERRIDE {
+    std::string output = "";
+    // If Boolean Gauge, convert false/true to 0/1 for Prometheus
+    if constexpr (std::is_same_v<T, bool> ) {
+      int check = 0;
+      if (value() == true) {
+        check = 1;
+      } else {
+        check = 0;
+      }
+      output = strings::Substitute("$0$1{unit_type=\"$2\"} $3\n", prefix, prototype_->name(),
+                                   MetricUnit::Name(prototype_->unit()), check);
+    } else {
+      output = strings::Substitute("$0$1{unit_type=\"$2\"} $3\n", prefix, prototype_->name(),
+                                   MetricUnit::Name(prototype_->unit()), value());
+    }
+    writer->WriteEntry(output);
+  }
   // Reset this FunctionGauge to return a specific value.
   // This should be used during destruction. If you want a settable
   // Gauge, use a normal Gauge instead of a FunctionGauge.
@@ -1357,10 +1410,11 @@
   int64_t value() const;
   void Increment();
   void IncrementBy(int64_t amount);
-  virtual Status WriteAsJson(JsonWriter* w,
-                             const MetricJsonOptions& opts) const OVERRIDE;
+  Status WriteAsJson(JsonWriter* w, const MetricJsonOptions& opts) const OVERRIDE;
 
-  virtual bool IsUntouched() const override {
+  Status WriteAsPrometheus(PrometheusWriter* w, const std::string& prefix) const OVERRIDE;
+
+  bool IsUntouched() const override {
     return value() == 0;
   }
 
@@ -1385,6 +1439,7 @@
   FRIEND_TEST(MetricsTest, SimpleCounterTest);
   FRIEND_TEST(MetricsTest, SimpleCounterMergeTest);
   FRIEND_TEST(MultiThreadedMetricsTest, CounterIncrementTest);
+  FRIEND_TEST(MetricsTest, CounterPrometheusTest);
   friend class MetricEntity;
 
   explicit Counter(const CounterPrototype* proto);
@@ -1431,8 +1486,9 @@
   // or IncrementBy()).
   uint64_t TotalCount() const;
 
-  virtual Status WriteAsJson(JsonWriter* w,
-                             const MetricJsonOptions& opts) const OVERRIDE;
+  Status WriteAsJson(JsonWriter* w, const MetricJsonOptions& opts) const OVERRIDE;
+
+  Status WriteAsPrometheus(PrometheusWriter* w, const std::string& prefix) const OVERRIDE;
 
   // Returns a snapshot of this histogram including the bucketed values and counts.
   Status GetHistogramSnapshotPB(HistogramSnapshotPB* snapshot_pb,
diff --git a/src/kudu/util/prometheus_writer.cc b/src/kudu/util/prometheus_writer.cc
new file mode 100644
index 0000000..d68f88b
--- /dev/null
+++ b/src/kudu/util/prometheus_writer.cc
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "kudu/util/prometheus_writer.h"
+
+#include <sstream>
+
+void PrometheusWriter::WriteEntry(const std::string& data) {
+  *output_ << data;
+}
diff --git a/src/kudu/util/prometheus_writer.h b/src/kudu/util/prometheus_writer.h
new file mode 100644
index 0000000..528438f
--- /dev/null
+++ b/src/kudu/util/prometheus_writer.h
@@ -0,0 +1,31 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+#pragma once
+
+#include <iosfwd>
+#include <string>
+
+class PrometheusWriter {
+
+ public:
+  explicit PrometheusWriter(std::ostringstream* output): output_(output) {}
+
+  void WriteEntry(const std::string& data);
+
+ private:
+  std::ostringstream* output_;
+};