You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2025/03/27 04:27:27 UTC

(opendal) branch main updated: refactor: Migrate PrometheusClientLayer to support IO metrics (#5887)

This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 034052acc refactor: Migrate PrometheusClientLayer to support IO metrics (#5887)
034052acc is described below

commit 034052acc679b35a2b6abc1de8ecc7813e672f0c
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Thu Mar 27 12:27:22 2025 +0800

    refactor: Migrate PrometheusClientLayer to support IO metrics (#5887)
    
    Signed-off-by: Xuanwo <gi...@xuanwo.io>
---
 core/src/layers/prometheus_client.rs | 650 ++++++++++++++++-------------------
 1 file changed, 289 insertions(+), 361 deletions(-)

diff --git a/core/src/layers/prometheus_client.rs b/core/src/layers/prometheus_client.rs
index de5c1f4a1..07c03d552 100644
--- a/core/src/layers/prometheus_client.rs
+++ b/core/src/layers/prometheus_client.rs
@@ -16,7 +16,6 @@
 // under the License.
 
 use std::fmt;
-use std::sync::Arc;
 use std::time::Duration;
 
 use prometheus_client::encoding::EncodeLabel;
@@ -25,9 +24,11 @@ use prometheus_client::encoding::LabelSetEncoder;
 use prometheus_client::metrics::counter::Counter;
 use prometheus_client::metrics::family::Family;
 use prometheus_client::metrics::family::MetricConstructor;
-use prometheus_client::metrics::histogram::exponential_buckets;
+use prometheus_client::metrics::gauge::Gauge;
 use prometheus_client::metrics::histogram::Histogram;
+use prometheus_client::registry::Metric;
 use prometheus_client::registry::Registry;
+use prometheus_client::registry::Unit;
 
 use crate::layers::observe;
 use crate::raw::*;
@@ -85,58 +86,8 @@ pub struct PrometheusClientLayer {
 
 impl PrometheusClientLayer {
     /// Create a [`PrometheusClientLayerBuilder`] to set the configuration of metrics.
-    ///
-    /// # Default Configuration
-    ///
-    /// - `operation_duration_seconds_buckets`: `exponential_buckets(0.01, 2.0, 16)`
-    /// - `operation_bytes_buckets`: `exponential_buckets(1.0, 2.0, 16)`
-    /// - `path_label`: `0`
-    ///
-    /// # Examples
-    ///
-    /// ```no_run
-    /// # use log::debug;
-    /// # use opendal::layers::PrometheusClientLayer;
-    /// # use opendal::services;
-    /// # use opendal::Operator;
-    /// # use opendal::Result;
-    ///
-    /// # #[tokio::main]
-    /// # async fn main() -> Result<()> {
-    /// // Pick a builder and configure it.
-    /// let builder = services::Memory::default();
-    /// let mut registry = prometheus_client::registry::Registry::default();
-    ///
-    /// let duration_seconds_buckets =
-    ///     prometheus_client::metrics::histogram::exponential_buckets(0.01, 2.0, 16).collect();
-    /// let bytes_buckets =
-    ///     prometheus_client::metrics::histogram::exponential_buckets(1.0, 2.0, 16).collect();
-    /// let op = Operator::new(builder)?
-    ///     .layer(
-    ///         PrometheusClientLayer::builder()
-    ///             .operation_duration_seconds_buckets(duration_seconds_buckets)
-    ///             .operation_bytes_buckets(bytes_buckets)
-    ///             .path_label(0)
-    ///             .register(&mut registry),
-    ///     )
-    ///     .finish();
-    /// debug!("operator: {op:?}");
-    ///
-    /// Ok(())
-    /// # }
-    /// ```
     pub fn builder() -> PrometheusClientLayerBuilder {
-        let operation_duration_seconds_buckets = exponential_buckets(0.01, 2.0, 16).collect();
-        let operation_bytes_buckets = exponential_buckets(1.0, 2.0, 16).collect();
-        let http_request_duration_seconds_buckets = exponential_buckets(0.01, 2.0, 16).collect();
-        let http_request_bytes_buckets = exponential_buckets(1.0, 2.0, 16).collect();
-        PrometheusClientLayerBuilder::new(
-            operation_duration_seconds_buckets,
-            operation_bytes_buckets,
-            http_request_duration_seconds_buckets,
-            http_request_bytes_buckets,
-            0,
-        )
+        PrometheusClientLayerBuilder::default()
     }
 }
 
@@ -150,217 +101,73 @@ impl<A: Access> Layer<A> for PrometheusClientLayer {
 
 /// [`PrometheusClientLayerBuilder`] is a config builder to build a [`PrometheusClientLayer`].
 pub struct PrometheusClientLayerBuilder {
-    operation_duration_seconds_buckets: Vec<f64>,
-    operation_bytes_buckets: Vec<f64>,
-    http_request_duration_seconds_buckets: Vec<f64>,
-    http_request_bytes_buckets: Vec<f64>,
-    path_label_level: usize,
+    bytes_buckets: Vec<f64>,
+    bytes_rate_buckets: Vec<f64>,
+    entries_buckets: Vec<f64>,
+    entries_rate_buckets: Vec<f64>,
+    duration_seconds_buckets: Vec<f64>,
+    ttfb_buckets: Vec<f64>,
 }
 
-impl PrometheusClientLayerBuilder {
-    fn new(
-        operation_duration_seconds_buckets: Vec<f64>,
-        operation_bytes_buckets: Vec<f64>,
-        http_request_duration_seconds_buckets: Vec<f64>,
-        http_request_bytes_buckets: Vec<f64>,
-        path_label_level: usize,
-    ) -> Self {
+impl Default for PrometheusClientLayerBuilder {
+    fn default() -> Self {
         Self {
-            operation_duration_seconds_buckets,
-            operation_bytes_buckets,
-            http_request_duration_seconds_buckets,
-            http_request_bytes_buckets,
-            path_label_level,
+            bytes_buckets: observe::DEFAULT_BYTES_BUCKETS.to_vec(),
+            bytes_rate_buckets: observe::DEFAULT_BYTES_RATE_BUCKETS.to_vec(),
+            entries_buckets: observe::DEFAULT_ENTRIES_BUCKETS.to_vec(),
+            entries_rate_buckets: observe::DEFAULT_ENTRIES_RATE_BUCKETS.to_vec(),
+            duration_seconds_buckets: observe::DEFAULT_DURATION_SECONDS_BUCKETS.to_vec(),
+            ttfb_buckets: observe::DEFAULT_TTFB_BUCKETS.to_vec(),
         }
     }
+}
 
-    /// Set buckets for `operation_duration_seconds` histogram.
-    ///
-    /// # Examples
-    ///
-    /// ```no_run
-    /// # use log::debug;
-    /// # use opendal::layers::PrometheusClientLayer;
-    /// # use opendal::services;
-    /// # use opendal::Operator;
-    /// # use opendal::Result;
-    ///
-    /// # #[tokio::main]
-    /// # async fn main() -> Result<()> {
-    /// // Pick a builder and configure it.
-    /// let builder = services::Memory::default();
-    /// let mut registry = prometheus_client::registry::Registry::default();
-    ///
-    /// let buckets =
-    ///     prometheus_client::metrics::histogram::exponential_buckets(0.01, 2.0, 16).collect();
-    /// let op = Operator::new(builder)?
-    ///     .layer(
-    ///         PrometheusClientLayer::builder()
-    ///             .operation_duration_seconds_buckets(buckets)
-    ///             .register(&mut registry),
-    ///     )
-    ///     .finish();
-    /// debug!("operator: {op:?}");
-    ///
-    /// Ok(())
-    /// # }
-    /// ```
-    pub fn operation_duration_seconds_buckets(mut self, buckets: Vec<f64>) -> Self {
+impl PrometheusClientLayerBuilder {
+    /// Set buckets for bytes related histogram like `operation_bytes`.
+    pub fn bytes_buckets(mut self, buckets: Vec<f64>) -> Self {
         if !buckets.is_empty() {
-            self.operation_duration_seconds_buckets = buckets;
+            self.bytes_buckets = buckets;
         }
         self
     }
 
-    /// Set buckets for `operation_bytes` histogram.
-    ///
-    /// # Examples
-    ///
-    /// ```no_run
-    /// # use log::debug;
-    /// # use opendal::layers::PrometheusClientLayer;
-    /// # use opendal::services;
-    /// # use opendal::Operator;
-    /// # use opendal::Result;
-    ///
-    /// # #[tokio::main]
-    /// # async fn main() -> Result<()> {
-    /// // Pick a builder and configure it.
-    /// let builder = services::Memory::default();
-    /// let mut registry = prometheus_client::registry::Registry::default();
-    ///
-    /// let buckets =
-    ///     prometheus_client::metrics::histogram::exponential_buckets(1.0, 2.0, 16).collect();
-    /// let op = Operator::new(builder)?
-    ///     .layer(
-    ///         PrometheusClientLayer::builder()
-    ///             .operation_bytes_buckets(buckets)
-    ///             .register(&mut registry),
-    ///     )
-    ///     .finish();
-    /// debug!("operator: {op:?}");
-    ///
-    /// Ok(())
-    /// # }
-    /// ```
-    pub fn operation_bytes_buckets(mut self, buckets: Vec<f64>) -> Self {
+    /// Set buckets for bytes rate related histogram like `operation_bytes_rate`.
+    pub fn bytes_rate_buckets(mut self, buckets: Vec<f64>) -> Self {
         if !buckets.is_empty() {
-            self.operation_bytes_buckets = buckets;
+            self.bytes_rate_buckets = buckets;
         }
         self
     }
 
-    /// Set buckets for `http_request_duration_seconds` histogram.
-    ///
-    /// # Examples
-    ///
-    /// ```no_run
-    /// # use log::debug;
-    /// # use opendal::layers::PrometheusClientLayer;
-    /// # use opendal::services;
-    /// # use opendal::Operator;
-    /// # use opendal::Result;
-    ///
-    /// # #[tokio::main]
-    /// # async fn main() -> Result<()> {
-    /// // Pick a builder and configure it.
-    /// let builder = services::Memory::default();
-    /// let mut registry = prometheus_client::registry::Registry::default();
-    ///
-    /// let buckets =
-    ///     prometheus_client::metrics::histogram::exponential_buckets(0.01, 2.0, 16).collect();
-    /// let op = Operator::new(builder)?
-    ///     .layer(
-    ///         PrometheusClientLayer::builder()
-    ///             .http_request_duration_seconds_buckets(buckets)
-    ///             .register(&mut registry),
-    ///     )
-    ///     .finish();
-    /// debug!("operator: {op:?}");
-    ///
-    /// Ok(())
-    /// # }
-    /// ```
-    pub fn http_request_duration_seconds_buckets(mut self, buckets: Vec<f64>) -> Self {
+    /// Set buckets for entries related histogram like `operation_entries`.
+    pub fn entries_buckets(mut self, buckets: Vec<f64>) -> Self {
         if !buckets.is_empty() {
-            self.http_request_duration_seconds_buckets = buckets;
+            self.entries_buckets = buckets;
         }
         self
     }
 
-    /// Set buckets for `operation_bytes` histogram.
-    ///
-    /// # Examples
-    ///
-    /// ```no_run
-    /// # use log::debug;
-    /// # use opendal::layers::PrometheusClientLayer;
-    /// # use opendal::services;
-    /// # use opendal::Operator;
-    /// # use opendal::Result;
-    ///
-    /// # #[tokio::main]
-    /// # async fn main() -> Result<()> {
-    /// // Pick a builder and configure it.
-    /// let builder = services::Memory::default();
-    /// let mut registry = prometheus_client::registry::Registry::default();
-    ///
-    /// let buckets =
-    ///     prometheus_client::metrics::histogram::exponential_buckets(1.0, 2.0, 16).collect();
-    /// let op = Operator::new(builder)?
-    ///     .layer(
-    ///         PrometheusClientLayer::builder()
-    ///             .http_request_bytes_buckets(buckets)
-    ///             .register(&mut registry),
-    ///     )
-    ///     .finish();
-    /// debug!("operator: {op:?}");
-    ///
-    /// Ok(())
-    /// # }
-    /// ```
-    pub fn http_request_bytes_buckets(mut self, buckets: Vec<f64>) -> Self {
+    /// Set buckets for entries rate related histogram like `operation_entries_rate`.
+    pub fn entries_rate_buckets(mut self, buckets: Vec<f64>) -> Self {
         if !buckets.is_empty() {
-            self.http_request_bytes_buckets = buckets;
+            self.entries_rate_buckets = buckets;
         }
         self
     }
 
-    /// Set the level of path label.
-    ///
-    /// - level = 0: we will ignore the path label.
-    /// - level > 0: the path label will be the path split by "/" and get the last n level,
-    ///   if n=1 and input path is "abc/def/ghi", and then we will get "abc/" as the path label.
-    ///
-    /// # Examples
-    ///
-    /// ```no_run
-    /// # use log::debug;
-    /// # use opendal::layers::PrometheusClientLayer;
-    /// # use opendal::services;
-    /// # use opendal::Operator;
-    /// # use opendal::Result;
-    ///
-    /// # #[tokio::main]
-    /// # async fn main() -> Result<()> {
-    /// // Pick a builder and configure it.
-    /// let builder = services::Memory::default();
-    /// let mut registry = prometheus_client::registry::Registry::default();
-    ///
-    /// let op = Operator::new(builder)?
-    ///     .layer(
-    ///         PrometheusClientLayer::builder()
-    ///             .path_label(1)
-    ///             .register(&mut registry),
-    ///     )
-    ///     .finish();
-    /// debug!("operator: {op:?}");
-    ///
-    /// Ok(())
-    /// # }
-    /// ```
-    pub fn path_label(mut self, level: usize) -> Self {
-        self.path_label_level = level;
+    /// Set buckets for duration seconds related histogram like `operation_duration_seconds`.
+    pub fn duration_seconds_buckets(mut self, buckets: Vec<f64>) -> Self {
+        if !buckets.is_empty() {
+            self.duration_seconds_buckets = buckets;
+        }
+        self
+    }
+
+    /// Set buckets for ttfb related histogram like `operation_ttfb_seconds`.
+    pub fn ttfb_buckets(mut self, buckets: Vec<f64>) -> Self {
+        if !buckets.is_empty() {
+            self.ttfb_buckets = buckets;
+        }
         self
     }
 
@@ -390,60 +197,168 @@ impl PrometheusClientLayerBuilder {
     /// # }
     /// ```
     pub fn register(self, registry: &mut Registry) -> PrometheusClientLayer {
-        let operation_duration_seconds =
+        let operation_bytes =
             Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
-                buckets: self.operation_duration_seconds_buckets,
+                buckets: self.bytes_buckets.clone(),
             });
-        let operation_bytes =
+        let operation_bytes_rate =
+            Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
+                buckets: self.bytes_rate_buckets.clone(),
+            });
+        let operation_entries =
             Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
-                buckets: self.operation_bytes_buckets,
+                buckets: self.entries_buckets.clone(),
+            });
+        let operation_entries_rate =
+            Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
+                buckets: self.entries_rate_buckets.clone(),
+            });
+        let operation_duration_seconds =
+            Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
+                buckets: self.duration_seconds_buckets.clone(),
             });
         let operation_errors_total = Family::<OperationLabels, Counter>::default();
-        let http_request_duration_seconds =
+        let operation_executing = Family::<OperationLabels, Gauge>::default();
+        let operation_ttfb_seconds =
             Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
-                buckets: self.http_request_duration_seconds_buckets,
+                buckets: self.ttfb_buckets.clone(),
             });
+
+        let http_executing = Family::<OperationLabels, Gauge>::default();
         let http_request_bytes =
             Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
-                buckets: self.http_request_bytes_buckets,
+                buckets: self.bytes_buckets.clone(),
+            });
+        let http_request_bytes_rate =
+            Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
+                buckets: self.bytes_rate_buckets.clone(),
+            });
+        let http_request_duration_seconds =
+            Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
+                buckets: self.duration_seconds_buckets.clone(),
+            });
+        let http_response_bytes =
+            Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
+                buckets: self.bytes_buckets.clone(),
+            });
+        let http_response_bytes_rate =
+            Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
+                buckets: self.bytes_rate_buckets.clone(),
             });
+        let http_response_duration_seconds =
+            Family::<OperationLabels, Histogram, _>::new_with_constructor(HistogramConstructor {
+                buckets: self.duration_seconds_buckets.clone(),
+            });
+        let http_connection_errors_total = Family::<OperationLabels, Counter>::default();
+        let http_status_errors_total = Family::<OperationLabels, Counter>::default();
 
-        registry.register(
-            observe::METRIC_OPERATION_DURATION_SECONDS.name(),
-            observe::METRIC_OPERATION_DURATION_SECONDS.help(),
-            operation_duration_seconds.clone(),
-        );
-        registry.register(
-            observe::METRIC_OPERATION_BYTES.name(),
-            observe::METRIC_OPERATION_BYTES.help(),
+        register_metric(
+            registry,
             operation_bytes.clone(),
+            observe::MetricValue::OperationBytes(0),
         );
-        // `prometheus-client` will automatically add `_total` suffix into the name of counter
-        // metrics, so we can't use `METRIC_OPERATION_ERRORS_TOTAL.name()` here.
-        registry.register(
-            "opendal_operation_errors",
-            observe::METRIC_OPERATION_ERRORS_TOTAL.help(),
-            operation_errors_total.clone(),
+        register_metric(
+            registry,
+            operation_bytes_rate.clone(),
+            observe::MetricValue::OperationBytesRate(0.0),
+        );
+        register_metric(
+            registry,
+            operation_entries.clone(),
+            observe::MetricValue::OperationEntries(0),
         );
-        registry.register(
-            observe::METRIC_HTTP_REQUEST_DURATION_SECONDS.name(),
-            observe::METRIC_HTTP_REQUEST_DURATION_SECONDS.help(),
+        register_metric(
+            registry,
+            operation_entries_rate.clone(),
+            observe::MetricValue::OperationEntriesRate(0.0),
+        );
+        register_metric(
+            registry,
             operation_duration_seconds.clone(),
+            observe::MetricValue::OperationDurationSeconds(Duration::default()),
         );
-        registry.register(
-            observe::METRIC_HTTP_REQUEST_BYTES.name(),
-            observe::METRIC_HTTP_REQUEST_BYTES.help(),
-            operation_bytes.clone(),
+        register_metric(
+            registry,
+            operation_errors_total.clone(),
+            observe::MetricValue::OperationErrorsTotal,
+        );
+        register_metric(
+            registry,
+            operation_executing.clone(),
+            observe::MetricValue::OperationExecuting(0),
+        );
+        register_metric(
+            registry,
+            operation_ttfb_seconds.clone(),
+            observe::MetricValue::OperationTtfbSeconds(Duration::default()),
+        );
+
+        register_metric(
+            registry,
+            http_executing.clone(),
+            observe::MetricValue::HttpExecuting(0),
+        );
+        register_metric(
+            registry,
+            http_request_bytes.clone(),
+            observe::MetricValue::HttpRequestBytes(0),
+        );
+        register_metric(
+            registry,
+            http_request_bytes_rate.clone(),
+            observe::MetricValue::HttpRequestBytesRate(0.0),
+        );
+        register_metric(
+            registry,
+            http_request_duration_seconds.clone(),
+            observe::MetricValue::HttpRequestDurationSeconds(Duration::default()),
+        );
+        register_metric(
+            registry,
+            http_response_bytes.clone(),
+            observe::MetricValue::HttpResponseBytes(0),
+        );
+        register_metric(
+            registry,
+            http_response_bytes_rate.clone(),
+            observe::MetricValue::HttpResponseBytesRate(0.0),
+        );
+        register_metric(
+            registry,
+            http_response_duration_seconds.clone(),
+            observe::MetricValue::HttpResponseDurationSeconds(Duration::default()),
+        );
+        register_metric(
+            registry,
+            http_connection_errors_total.clone(),
+            observe::MetricValue::HttpConnectionErrorsTotal,
+        );
+        register_metric(
+            registry,
+            http_status_errors_total.clone(),
+            observe::MetricValue::HttpStatusErrorsTotal,
         );
 
         PrometheusClientLayer {
             interceptor: PrometheusClientInterceptor {
-                operation_duration_seconds,
                 operation_bytes,
+                operation_bytes_rate,
+                operation_entries,
+                operation_entries_rate,
+                operation_duration_seconds,
                 operation_errors_total,
-                http_request_duration_seconds,
+                operation_executing,
+                operation_ttfb_seconds,
+
+                http_executing,
                 http_request_bytes,
-                path_label_level: self.path_label_level,
+                http_request_bytes_rate,
+                http_request_duration_seconds,
+                http_response_bytes,
+                http_response_bytes_rate,
+                http_response_duration_seconds,
+                http_connection_errors_total,
+                http_status_errors_total,
             },
         }
     }
@@ -462,114 +377,127 @@ impl MetricConstructor<Histogram> for HistogramConstructor {
 
 #[derive(Clone, Debug)]
 pub struct PrometheusClientInterceptor {
-    operation_duration_seconds: Family<OperationLabels, Histogram, HistogramConstructor>,
     operation_bytes: Family<OperationLabels, Histogram, HistogramConstructor>,
+    operation_bytes_rate: Family<OperationLabels, Histogram, HistogramConstructor>,
+    operation_entries: Family<OperationLabels, Histogram, HistogramConstructor>,
+    operation_entries_rate: Family<OperationLabels, Histogram, HistogramConstructor>,
+    operation_duration_seconds: Family<OperationLabels, Histogram, HistogramConstructor>,
     operation_errors_total: Family<OperationLabels, Counter>,
-    http_request_duration_seconds: Family<OperationLabels, Histogram, HistogramConstructor>,
+    operation_executing: Family<OperationLabels, Gauge>,
+    operation_ttfb_seconds: Family<OperationLabels, Histogram, HistogramConstructor>,
+
+    http_executing: Family<OperationLabels, Gauge>,
     http_request_bytes: Family<OperationLabels, Histogram, HistogramConstructor>,
-    path_label_level: usize,
+    http_request_bytes_rate: Family<OperationLabels, Histogram, HistogramConstructor>,
+    http_request_duration_seconds: Family<OperationLabels, Histogram, HistogramConstructor>,
+    http_response_bytes: Family<OperationLabels, Histogram, HistogramConstructor>,
+    http_response_bytes_rate: Family<OperationLabels, Histogram, HistogramConstructor>,
+    http_response_duration_seconds: Family<OperationLabels, Histogram, HistogramConstructor>,
+    http_connection_errors_total: Family<OperationLabels, Counter>,
+    http_status_errors_total: Family<OperationLabels, Counter>,
 }
 
 impl observe::MetricsIntercept for PrometheusClientInterceptor {
-    fn observe_operation_duration_seconds(
-        &self,
-        info: Arc<AccessorInfo>,
-        path: &str,
-        op: Operation,
-        duration: Duration,
-    ) {
-        self.operation_duration_seconds
-            .get_or_create(&OperationLabels {
-                info,
-                operation: op,
-                path: observe::path_label_value(path, self.path_label_level).map(Into::into),
-                error: None,
-            })
-            .observe(duration.as_secs_f64())
-    }
-
-    fn observe_operation_bytes(
-        &self,
-        info: Arc<AccessorInfo>,
-        path: &str,
-        op: Operation,
-        bytes: usize,
-    ) {
-        self.operation_bytes
-            .get_or_create(&OperationLabels {
-                info,
-                operation: op,
-                path: observe::path_label_value(path, self.path_label_level).map(Into::into),
-                error: None,
-            })
-            .observe(bytes as f64)
-    }
-
-    fn observe_operation_errors_total(
-        &self,
-        info: Arc<AccessorInfo>,
-        path: &str,
-        op: Operation,
-        error: ErrorKind,
-    ) {
-        self.operation_errors_total
-            .get_or_create(&OperationLabels {
-                info,
-                operation: op,
-                path: observe::path_label_value(path, self.path_label_level).map(Into::into),
-                error: Some(error.into_static()),
-            })
-            .inc();
-    }
-
-    fn observe_http_request_duration_seconds(
-        &self,
-        info: Arc<AccessorInfo>,
-        op: Operation,
-        duration: Duration,
-    ) {
-        self.http_request_duration_seconds
-            .get_or_create(&OperationLabels {
-                info,
-                operation: op,
-                path: None,
-                error: None,
-            })
-            .observe(duration.as_secs_f64())
-    }
+    fn observe(&self, labels: observe::MetricLabels, value: observe::MetricValue) {
+        let labels = OperationLabels(labels);
+        match value {
+            observe::MetricValue::OperationBytes(v) => self
+                .operation_bytes
+                .get_or_create(&labels)
+                .observe(v as f64),
+            observe::MetricValue::OperationBytesRate(v) => {
+                self.operation_bytes_rate.get_or_create(&labels).observe(v)
+            }
+            observe::MetricValue::OperationEntries(v) => self
+                .operation_entries
+                .get_or_create(&labels)
+                .observe(v as f64),
+            observe::MetricValue::OperationEntriesRate(v) => self
+                .operation_entries_rate
+                .get_or_create(&labels)
+                .observe(v),
+            observe::MetricValue::OperationDurationSeconds(v) => self
+                .operation_duration_seconds
+                .get_or_create(&labels)
+                .observe(v.as_secs_f64()),
+            observe::MetricValue::OperationErrorsTotal => {
+                self.operation_errors_total.get_or_create(&labels).inc();
+            }
+            observe::MetricValue::OperationExecuting(v) => {
+                self.operation_executing
+                    .get_or_create(&labels)
+                    .inc_by(v as i64);
+            }
+            observe::MetricValue::OperationTtfbSeconds(v) => self
+                .operation_ttfb_seconds
+                .get_or_create(&labels)
+                .observe(v.as_secs_f64()),
 
-    fn observe_http_request_bytes(&self, info: Arc<AccessorInfo>, op: Operation, bytes: usize) {
-        self.http_request_bytes
-            .get_or_create(&OperationLabels {
-                info,
-                operation: op,
-                path: None,
-                error: None,
-            })
-            .observe(bytes as f64)
+            observe::MetricValue::HttpExecuting(v) => {
+                self.http_executing.get_or_create(&labels).inc_by(v as i64);
+            }
+            observe::MetricValue::HttpRequestBytes(v) => self
+                .http_request_bytes
+                .get_or_create(&labels)
+                .observe(v as f64),
+            observe::MetricValue::HttpRequestBytesRate(v) => self
+                .http_request_bytes_rate
+                .get_or_create(&labels)
+                .observe(v),
+            observe::MetricValue::HttpRequestDurationSeconds(v) => self
+                .http_request_duration_seconds
+                .get_or_create(&labels)
+                .observe(v.as_secs_f64()),
+            observe::MetricValue::HttpResponseBytes(v) => self
+                .http_response_bytes
+                .get_or_create(&labels)
+                .observe(v as f64),
+            observe::MetricValue::HttpResponseBytesRate(v) => self
+                .http_response_bytes_rate
+                .get_or_create(&labels)
+                .observe(v),
+            observe::MetricValue::HttpResponseDurationSeconds(v) => self
+                .http_response_duration_seconds
+                .get_or_create(&labels)
+                .observe(v.as_secs_f64()),
+            observe::MetricValue::HttpConnectionErrorsTotal => {
+                self.http_connection_errors_total
+                    .get_or_create(&labels)
+                    .inc();
+            }
+            observe::MetricValue::HttpStatusErrorsTotal => {
+                self.http_status_errors_total.get_or_create(&labels).inc();
+            }
+        };
     }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
-struct OperationLabels {
-    info: Arc<AccessorInfo>,
-    operation: Operation,
-    path: Option<String>,
-    error: Option<&'static str>,
-}
+struct OperationLabels(observe::MetricLabels);
 
 impl EncodeLabelSet for OperationLabels {
     fn encode(&self, mut encoder: LabelSetEncoder) -> Result<(), fmt::Error> {
-        (observe::LABEL_SCHEME, self.info.scheme().into_static()).encode(encoder.encode_label())?;
-        (observe::LABEL_NAMESPACE, self.info.name().as_ref()).encode(encoder.encode_label())?;
-        (observe::LABEL_ROOT, self.info.root().as_ref()).encode(encoder.encode_label())?;
-        (observe::LABEL_OPERATION, self.operation.into_static()).encode(encoder.encode_label())?;
-        if let Some(path) = &self.path {
-            (observe::LABEL_PATH, path.as_str()).encode(encoder.encode_label())?;
+        (observe::LABEL_SCHEME, self.0.scheme.into_static()).encode(encoder.encode_label())?;
+        (observe::LABEL_NAMESPACE, self.0.namespace.as_ref()).encode(encoder.encode_label())?;
+        (observe::LABEL_ROOT, self.0.root.as_ref()).encode(encoder.encode_label())?;
+        (observe::LABEL_OPERATION, self.0.operation).encode(encoder.encode_label())?;
+
+        if let Some(error) = &self.0.error {
+            (observe::LABEL_ERROR, error.into_static()).encode(encoder.encode_label())?;
         }
-        if let Some(error) = self.error {
-            (observe::LABEL_ERROR, error).encode(encoder.encode_label())?;
+        if let Some(code) = &self.0.status_code {
+            (observe::LABEL_STATUS_CODE, code.as_str()).encode(encoder.encode_label())?;
         }
         Ok(())
     }
 }
+
+fn register_metric(registry: &mut Registry, metric: impl Metric, value: observe::MetricValue) {
+    let ((name, unit), help) = (value.name_with_unit(), value.help());
+
+    if let Some(unit) = unit {
+        registry.register_with_unit(name, help, Unit::Other(unit.to_string()), metric);
+    } else {
+        registry.register(name, help, metric);
+    }
+}