1use http::{Request, Response};
2use http_body::{Body, Frame};
3use pin_project_lite::pin_project;
4use rust_zero_core::{
5 CounterVec, GaugeVec, HistogramOptions, HistogramVec, Metrics, MetricsError, VectorOptions,
6};
7use std::{
8 collections::BTreeSet,
9 future::Future,
10 pin::Pin,
11 sync::Arc,
12 task::{Context, Poll},
13 time::Instant,
14};
15use tower::{Layer, Service};
16
17const UNKNOWN_METHOD: &str = "unknown";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RpcMetricMode {
22 Client,
23 Server,
24}
25
26impl RpcMetricMode {
27 fn name(self) -> &'static str {
28 match self {
29 Self::Client => "client",
30 Self::Server => "server",
31 }
32 }
33}
34
35#[derive(Clone)]
41pub struct RpcMetrics {
42 requests: CounterVec,
43 duration: HistogramVec,
44 in_flight: GaugeVec,
45 methods: Arc<BTreeSet<String>>,
46}
47
48impl RpcMetrics {
49 pub fn new<I, S>(
50 metrics: &Metrics,
51 namespace: impl Into<String>,
52 mode: RpcMetricMode,
53 methods: I,
54 ) -> Result<Self, MetricsError>
55 where
56 I: IntoIterator<Item = S>,
57 S: Into<String>,
58 {
59 let namespace = namespace.into();
60 let subsystem = format!("rpc_{}", mode.name());
61 let options = |name, help| {
62 VectorOptions::new(name, help)
63 .with_namespace(namespace.clone())
64 .with_subsystem(subsystem.clone())
65 };
66
67 Ok(Self {
68 requests: metrics.counter_vec(
69 options("requests_total", "Completed gRPC requests")
70 .with_labels(["method", "code"]),
71 )?,
72 duration: metrics.histogram_vec(
73 HistogramOptions::new("", "").with_vector_options(
74 options("request_duration_seconds", "gRPC request duration")
75 .with_labels(["method", "code"]),
76 ),
77 )?,
78 in_flight: metrics.gauge_vec(
79 options("requests_in_flight", "In-flight gRPC requests").with_labels(["method"]),
80 )?,
81 methods: Arc::new(methods.into_iter().map(Into::into).collect()),
82 })
83 }
84
85 fn method(&self, path: &str) -> String {
86 if self.methods.contains(path) {
87 path.to_owned()
88 } else {
89 UNKNOWN_METHOD.to_owned()
90 }
91 }
92
93 fn start(&self, path: &str) -> RpcObservation {
94 let method = self.method(path);
95 self.in_flight
96 .inc(&[&method])
97 .expect("gRPC in-flight metric labels are fixed");
98 RpcObservation {
99 metrics: self.clone(),
100 method,
101 started_at: Instant::now(),
102 finished: false,
103 }
104 }
105}
106
107struct RpcObservation {
108 metrics: RpcMetrics,
109 method: String,
110 started_at: Instant,
111 finished: bool,
112}
113
114impl RpcObservation {
115 fn finish(&mut self, code: &str) {
116 if self.finished {
117 return;
118 }
119 self.finished = true;
120 let labels = [self.method.as_str(), code];
121 self.metrics
122 .requests
123 .inc(&labels)
124 .expect("gRPC request metric labels are fixed");
125 self.metrics
126 .duration
127 .observe(self.started_at.elapsed().as_secs_f64(), &labels)
128 .expect("gRPC duration metric labels and observation are valid");
129 self.metrics
130 .in_flight
131 .dec(&[&self.method])
132 .expect("gRPC in-flight metric labels are fixed");
133 }
134}
135
136impl Drop for RpcObservation {
137 fn drop(&mut self) {
138 self.finish("cancelled");
139 }
140}
141
142#[derive(Clone)]
144pub struct RpcMetricsLayer {
145 metrics: RpcMetrics,
146}
147
148impl RpcMetricsLayer {
149 pub fn new(metrics: RpcMetrics) -> Self {
150 Self { metrics }
151 }
152}
153
154impl<S> Layer<S> for RpcMetricsLayer {
155 type Service = RpcMetricsService<S>;
156
157 fn layer(&self, inner: S) -> Self::Service {
158 RpcMetricsService {
159 inner,
160 metrics: self.metrics.clone(),
161 }
162 }
163}
164
165#[derive(Clone)]
166pub struct RpcMetricsService<S> {
167 inner: S,
168 metrics: RpcMetrics,
169}
170
171impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for RpcMetricsService<S>
172where
173 S: Service<Request<ReqBody>, Response = Response<ResBody>> + Send + 'static,
174 S::Future: Send + 'static,
175 S::Error: Send + 'static,
176 ReqBody: Send + 'static,
177 ResBody: Body + Send + 'static,
178{
179 type Response = Response<RpcMetricsBody<ResBody>>;
180 type Error = S::Error;
181 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
182
183 fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
184 self.inner.poll_ready(context)
185 }
186
187 fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
188 let mut observation = self.metrics.start(request.uri().path());
189 let future = self.inner.call(request);
190 Box::pin(async move {
191 match future.await {
192 Ok(response) => {
193 let header_code = grpc_code(response.headers())
194 .map(str::to_owned)
195 .or_else(|| (!response.status().is_success()).then(|| "http_error".into()));
196 let (parts, body) = response.into_parts();
197 let mut wrapped = RpcMetricsBody {
198 inner: body,
199 observation: Some(observation),
200 };
201 if let Some(code) = header_code {
202 wrapped.finish(&code);
203 }
204 Ok(Response::from_parts(parts, wrapped))
205 }
206 Err(error) => {
207 observation.finish("transport_error");
208 Err(error)
209 }
210 }
211 })
212 }
213}
214
215pin_project! {
216 pub struct RpcMetricsBody<B> {
217 #[pin]
218 inner: B,
219 observation: Option<RpcObservation>,
220 }
221
222 impl<B> PinnedDrop for RpcMetricsBody<B> {
223 fn drop(this: Pin<&mut Self>) {
224 let this = this.project();
225 this.observation.take();
227 }
228 }
229}
230
231impl<B> RpcMetricsBody<B> {
232 fn finish(&mut self, code: &str) {
233 if let Some(mut observation) = self.observation.take() {
234 observation.finish(code);
235 }
236 }
237}
238
239impl<B> Body for RpcMetricsBody<B>
240where
241 B: Body,
242{
243 type Data = B::Data;
244 type Error = B::Error;
245
246 fn poll_frame(
247 self: Pin<&mut Self>,
248 context: &mut Context<'_>,
249 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
250 let mut this = self.project();
251 match this.inner.as_mut().poll_frame(context) {
252 Poll::Ready(Some(Ok(frame))) => {
253 if let Some(code) = frame.trailers_ref().and_then(grpc_code) {
254 if let Some(mut observation) = this.observation.take() {
255 observation.finish(code);
256 }
257 }
258 Poll::Ready(Some(Ok(frame)))
259 }
260 Poll::Ready(Some(Err(error))) => {
261 if let Some(mut observation) = this.observation.take() {
262 observation.finish("body_error");
263 }
264 Poll::Ready(Some(Err(error)))
265 }
266 Poll::Ready(None) => {
267 if let Some(mut observation) = this.observation.take() {
268 observation.finish("0");
269 }
270 Poll::Ready(None)
271 }
272 Poll::Pending => Poll::Pending,
273 }
274 }
275
276 fn is_end_stream(&self) -> bool {
277 self.inner.is_end_stream()
278 }
279
280 fn size_hint(&self) -> http_body::SizeHint {
281 self.inner.size_hint()
282 }
283}
284
285fn grpc_code(headers: &http::HeaderMap) -> Option<&str> {
286 headers
287 .get("grpc-status")
288 .and_then(|value| value.to_str().ok())
289 .map(|code| match code {
290 "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"
291 | "13" | "14" | "15" | "16" => code,
292 _ => "invalid",
293 })
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use http_body::Frame;
300 use std::{convert::Infallible, future::Ready};
301
302 #[derive(Clone)]
303 struct Reply {
304 code: &'static str,
305 }
306
307 impl Service<Request<()>> for Reply {
308 type Response = Response<OneFrame>;
309 type Error = Infallible;
310 type Future = Ready<Result<Self::Response, Self::Error>>;
311
312 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
313 Poll::Ready(Ok(()))
314 }
315
316 fn call(&mut self, _: Request<()>) -> Self::Future {
317 std::future::ready(Ok(Response::new(OneFrame(Some(self.code)))))
318 }
319 }
320
321 struct OneFrame(Option<&'static str>);
322
323 impl Body for OneFrame {
324 type Data = &'static [u8];
325 type Error = Infallible;
326
327 fn poll_frame(
328 mut self: Pin<&mut Self>,
329 _: &mut Context<'_>,
330 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
331 Poll::Ready(self.0.take().map(|code| {
332 let mut trailers = http::HeaderMap::new();
333 trailers.insert("grpc-status", code.parse().unwrap());
334 Ok(Frame::trailers(trailers))
335 }))
336 }
337 }
338
339 #[tokio::test]
340 async fn records_trailer_status_and_bounds_unknown_methods() {
341 use tower::ServiceExt;
342
343 let registry = Metrics::new();
344 let metrics = RpcMetrics::new(
345 ®istry,
346 "users",
347 RpcMetricMode::Server,
348 ["/users.Users/Get"],
349 )
350 .unwrap();
351 let mut service = RpcMetricsLayer::new(metrics).layer(Reply { code: "5" });
352 let response = service
353 .ready()
354 .await
355 .unwrap()
356 .call(Request::builder().uri("/attacker/value").body(()).unwrap())
357 .await
358 .unwrap();
359 let mut body = Box::pin(response.into_body());
360 std::future::poll_fn(|context| body.as_mut().poll_frame(context)).await;
361
362 let rendered = registry.render();
363 assert!(
364 rendered.contains("users_rpc_server_requests_total{method=\"unknown\",code=\"5\"} 1")
365 );
366 assert!(rendered.contains("users_rpc_server_requests_in_flight{method=\"unknown\"} 0"));
367 assert!(!rendered.contains("attacker"));
368 }
369}