use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Instant;
use autumn_web::actuator::{MetricFamily, MetricKind, MetricSample};
use crate::metrics::{
Observed, Outcome, Samples, Series, UNKNOWN, code_name, family, is_method_name, unlabelled,
};
pub mod names {
pub const HANDLED: &str = "grpc_client_handled_total";
pub const SECONDS_SUM: &str = "grpc_client_handling_seconds_sum";
pub const SECONDS_COUNT: &str = "grpc_client_handling_seconds_count";
pub const IN_FLIGHT: &str = "grpc_client_in_flight";
}
pub struct ClientMetrics {
enabled: bool,
max_series: usize,
series: Mutex<Series>,
in_flight: AtomicI64,
}
impl ClientMetrics {
pub fn new(enabled: bool, max_series: usize) -> Self {
Self {
enabled,
max_series: max_series.max(1),
series: Mutex::new(Series::default()),
in_flight: AtomicI64::new(0),
}
}
pub fn samples(&self) -> (Samples, f64) {
let samples = self
.series
.lock()
.unwrap_or_else(PoisonError::into_inner)
.samples();
#[allow(clippy::cast_precision_loss)]
let in_flight = self.in_flight.load(Ordering::Acquire) as f64;
(samples, in_flight)
}
}
fn is_service_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 128
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'.')
}
fn labels(path: &str) -> (String, String) {
let mut parts = path.trim_start_matches('/').splitn(2, '/');
let service = parts.next().unwrap_or_default();
let method = parts.next().unwrap_or_default();
if !is_service_name(service) {
return (UNKNOWN.to_owned(), UNKNOWN.to_owned());
}
let method = if is_method_name(method) {
method
} else {
UNKNOWN
};
(service.to_owned(), method.to_owned())
}
pub struct ClientCall {
metrics: Arc<ClientMetrics>,
service: String,
method: String,
started: Instant,
pub outcome: Outcome,
}
impl ClientCall {
pub fn start(metrics: Arc<ClientMetrics>, path: &str) -> Self {
let (service, method) = labels(path);
metrics.in_flight.fetch_add(1, Ordering::AcqRel);
Self {
metrics,
service,
method,
started: Instant::now(),
outcome: Outcome::default(),
}
}
}
impl ClientCall {
pub const fn fail(&mut self, code: tonic::Code) {
self.outcome.trailer_code = Some(code as i32);
}
}
impl Observed for ClientCall {
fn outcome(&mut self) -> &mut Outcome {
&mut self.outcome
}
}
impl Drop for ClientCall {
fn drop(&mut self) {
if self.metrics.enabled {
let seconds = self.started.elapsed().as_secs_f64();
self.metrics
.series
.lock()
.unwrap_or_else(PoisonError::into_inner)
.record(
std::mem::take(&mut self.service),
std::mem::take(&mut self.method),
code_name(self.outcome.code()),
seconds,
self.metrics.max_series,
);
}
self.metrics.in_flight.fetch_sub(1, Ordering::AcqRel);
}
}
pub fn families<'a>(
clients: impl Iterator<Item = (&'a str, &'a ClientMetrics)>,
) -> Vec<MetricFamily> {
let mut handled = Vec::new();
let mut sums = Vec::new();
let mut counts = Vec::new();
let mut in_flight = Vec::new();
for (name, metrics) in clients {
let label = |mut sample: MetricSample| {
sample
.labels
.insert(0, ("client".to_owned(), name.to_owned()));
sample
};
let (samples, open) = metrics.samples();
handled.extend(samples.handled.into_iter().map(label));
sums.extend(samples.sums.into_iter().map(label));
counts.extend(samples.counts.into_iter().map(label));
in_flight.push(label(unlabelled(open)));
}
vec![
family(
names::HANDLED,
"gRPC client calls completed, by client, service, method and status code.",
MetricKind::Counter,
handled,
),
family(
names::SECONDS_SUM,
"Total gRPC client call time in seconds.",
MetricKind::Counter,
sums,
),
family(
names::SECONDS_COUNT,
"gRPC client calls timed.",
MetricKind::Counter,
counts,
),
family(
names::IN_FLIGHT,
"gRPC client calls in progress.",
MetricKind::Gauge,
in_flight,
),
]
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod tests {
use super::*;
#[test]
fn labels_come_from_valid_names_only() {
let own = |a: &str, b: &str| (a.to_owned(), b.to_owned());
assert_eq!(labels("/a.B/Call"), own("a.B", "Call"));
assert_eq!(labels("/a.B/bad-name"), own("a.B", UNKNOWN));
assert_eq!(labels("/a.B/"), own("a.B", UNKNOWN));
assert_eq!(labels("/a b/C"), own(UNKNOWN, UNKNOWN));
assert_eq!(labels(""), own(UNKNOWN, UNKNOWN));
let long = format!("/{}/C", "s".repeat(129));
assert_eq!(labels(&long), own(UNKNOWN, UNKNOWN));
}
#[test]
fn disabled_metrics_still_count_in_flight() {
let metrics = Arc::new(ClientMetrics::new(false, 10));
let call = ClientCall::start(metrics.clone(), "/a.B/C");
assert_eq!(metrics.samples().1, 1.0);
drop(call);
let (samples, open) = metrics.samples();
assert_eq!(open, 0.0);
assert!(samples.handled.is_empty());
}
}