Implement forward proxy with OpenTelemetry observability

This commit is contained in:
2026-10-28 10:38:00 +00:00
commit d34b3e10f7
11 changed files with 2594 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
use std::time::Duration;
use opentelemetry::{
KeyValue,
metrics::{Counter, Histogram, Meter, UpDownCounter},
};
#[derive(Clone)]
pub(crate) struct Metrics {
requests: Counter<u64>,
request_duration: Histogram<f64>,
active_requests: UpDownCounter<i64>,
upstream_duration: Histogram<f64>,
errors: Counter<u64>,
active_tunnels: UpDownCounter<i64>,
tunnel_duration: Histogram<f64>,
tunnel_bytes: Counter<u64>,
}
impl Metrics {
pub fn new(meter: Meter) -> Self {
Self {
requests: meter
.u64_counter("hermes.proxy.requests")
.with_description("Number of HTTP requests and CONNECT handshakes handled")
.with_unit("{request}")
.build(),
request_duration: meter
.f64_histogram("http.server.request.duration")
.with_description("Duration of downstream HTTP request handling")
.with_unit("s")
.build(),
active_requests: meter
.i64_up_down_counter("http.server.active_requests")
.with_description("Number of HTTP requests currently being handled")
.with_unit("{request}")
.build(),
upstream_duration: meter
.f64_histogram("http.client.request.duration")
.with_description("Duration of upstream HTTP requests through response headers")
.with_unit("s")
.build(),
errors: meter
.u64_counter("hermes.proxy.errors")
.with_description("Number of proxy failures")
.with_unit("{error}")
.build(),
active_tunnels: meter
.i64_up_down_counter("hermes.proxy.tunnels.active")
.with_description("Number of active CONNECT tunnels")
.with_unit("{tunnel}")
.build(),
tunnel_duration: meter
.f64_histogram("hermes.proxy.tunnel.duration")
.with_description("Duration of completed CONNECT tunnels")
.with_unit("s")
.build(),
tunnel_bytes: meter
.u64_counter("hermes.proxy.tunnel.bytes")
.with_description("Bytes relayed through completed CONNECT tunnels")
.with_unit("By")
.build(),
}
}
pub fn request_started(&self) -> ActiveRequest {
self.active_requests.add(1, &[]);
ActiveRequest {
metrics: self.clone(),
}
}
pub fn request_finished(
&self,
method: &str,
status: u16,
error_type: Option<&str>,
duration: Duration,
) {
let mut attributes = vec![
KeyValue::new("http.request.method", method.to_owned()),
KeyValue::new("http.response.status_code", i64::from(status)),
];
if let Some(error_type) = error_type {
attributes.push(KeyValue::new("error.type", error_type.to_owned()));
}
self.requests.add(1, &attributes);
self.request_duration
.record(duration.as_secs_f64(), &attributes);
}
pub fn upstream_finished(
&self,
method: &str,
status: Option<u16>,
error_type: Option<&str>,
duration: Duration,
) {
let mut attributes = vec![KeyValue::new("http.request.method", method.to_owned())];
if let Some(status) = status {
attributes.push(KeyValue::new(
"http.response.status_code",
i64::from(status),
));
}
if let Some(error_type) = error_type {
attributes.push(KeyValue::new("error.type", error_type.to_owned()));
}
self.upstream_duration
.record(duration.as_secs_f64(), &attributes);
}
pub fn error(&self, phase: &str, error_type: &str) {
self.errors.add(
1,
&[
KeyValue::new("proxy.phase", phase.to_owned()),
KeyValue::new("error.type", error_type.to_owned()),
],
);
}
pub fn tunnel_started(&self) -> ActiveTunnel {
self.active_tunnels.add(1, &[]);
ActiveTunnel {
metrics: self.clone(),
}
}
pub fn tunnel_finished(
&self,
duration: Duration,
client_to_upstream: u64,
upstream_to_client: u64,
error_type: Option<&str>,
) {
let attributes = error_type
.map(|value| vec![KeyValue::new("error.type", value.to_owned())])
.unwrap_or_default();
self.tunnel_duration
.record(duration.as_secs_f64(), &attributes);
self.tunnel_bytes.add(
client_to_upstream,
&[KeyValue::new("direction", "client_to_upstream")],
);
self.tunnel_bytes.add(
upstream_to_client,
&[KeyValue::new("direction", "upstream_to_client")],
);
}
}
pub(crate) struct ActiveRequest {
metrics: Metrics,
}
impl Drop for ActiveRequest {
fn drop(&mut self) {
self.metrics.active_requests.add(-1, &[]);
}
}
pub(crate) struct ActiveTunnel {
metrics: Metrics,
}
impl Drop for ActiveTunnel {
fn drop(&mut self) {
self.metrics.active_tunnels.add(-1, &[]);
}
}