use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tonic::body::Body;
use tonic::codegen::http;
use tonic::codegen::Service;
use tracing::Instrument as _;
use crate::auth::AUDIT_TARGET;
pub const REQUEST_ID_METADATA_KEY: &str = "gunnar-request-id";
const REQUEST_ID_HEADER: http::HeaderName = http::HeaderName::from_static(REQUEST_ID_METADATA_KEY);
pub type RequestIdSource = fn() -> u64;
pub fn render_request_id(id: u64) -> String {
format!("{id:016x}")
}
#[derive(Clone, Copy, Debug)]
pub struct RequestSpanLayer {
ids: RequestIdSource,
}
impl RequestSpanLayer {
pub fn new(ids: RequestIdSource) -> RequestSpanLayer {
RequestSpanLayer { ids }
}
}
impl<S> tower_layer::Layer<S> for RequestSpanLayer {
type Service = RequestSpan<S>;
fn layer(&self, inner: S) -> RequestSpan<S> {
RequestSpan {
inner,
ids: self.ids,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct RequestSpan<S> {
inner: S,
ids: RequestIdSource,
}
impl<S, ResBody> Service<http::Request<Body>> for RequestSpan<S>
where
S: Service<http::Request<Body>, Response = http::Response<ResBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
{
type Response = http::Response<ResBody>;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, S::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut request: http::Request<Body>) -> Self::Future {
let id = (self.ids)();
let rendered = render_request_id(id);
let method = request.uri().path().to_owned();
if let Some(called) = crate::auth::method_name_from_path(&method) {
request
.extensions_mut()
.insert(crate::auth::CalledMethod(called));
}
let span = tracing::info_span!(
target: AUDIT_TARGET,
"grpc.request",
req = %rendered,
method = %method,
);
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
Box::pin(
async move {
tracing::debug!(
target: AUDIT_TARGET,
event = "grpc.request.begin",
"a control-plane call arrived"
);
let started = std::time::Instant::now();
let mut response = inner.call(request).await;
let elapsed_ms = started.elapsed().as_millis() as u64;
match &mut response {
Ok(response) => {
let http_status = response.status().as_u16();
let grpc_status = response
.headers()
.get("grpc-status")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<i32>().ok());
let grpc_message = response
.headers()
.get("grpc-message")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
response.headers_mut().insert(
REQUEST_ID_HEADER,
http::HeaderValue::from_str(&rendered)
.expect("a request id is ascii hex"),
);
let growing = crate::quiet::growth_hold(grpc_status, grpc_message.as_deref());
let enrolling = grpc_status == Some(14) && grpc_message.as_deref().is_some_and(|m| m.contains("key-enrolling"));
if enrolling {
tracing::info!(
target: AUDIT_TARGET,
event = "grpc.request.done",
http_status,
grpc_status,
elapsed_ms,
"a control-plane call was deferred: the caller's key was fed to this box and is being enrolled"
);
} else if growing {
tracing::info!(
target: AUDIT_TARGET,
event = "grpc.request.done",
http_status,
grpc_status,
elapsed_ms,
"a control-plane call was deferred: this box is growing its data set"
);
} else if crate::quiet::unverified_answer(grpc_status, grpc_message.as_deref()) && crate::quiet::loud("grpc.request.done:not-verified").is_none() {
tracing::info!(
target: AUDIT_TARGET,
event = "grpc.request.done",
http_status,
grpc_status,
grpc_message,
elapsed_ms,
"a control-plane call was refused (said at WARN this minute already)"
);
} else if grpc_status.is_some_and(|status| status != 0) {
tracing::warn!(
target: AUDIT_TARGET,
event = "grpc.request.done",
http_status,
grpc_status,
grpc_message,
elapsed_ms,
"a control-plane call was refused"
);
} else {
tracing::info!(
target: AUDIT_TARGET,
event = "grpc.request.done",
http_status,
grpc_status,
elapsed_ms,
"a control-plane call was served"
);
}
}
Err(_) => {
tracing::warn!(
target: AUDIT_TARGET,
event = "grpc.request.failed",
elapsed_ms,
"the gRPC stack returned an error instead of a response"
);
}
}
response
}
.instrument(span),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_id_renders_fixed_width_lower_case_hex() {
assert_eq!(render_request_id(0), "0000000000000000");
assert_eq!(render_request_id(1), "0000000000000001");
assert_eq!(render_request_id(u64::MAX), "ffffffffffffffff");
assert_eq!(render_request_id(0xdead_beef).len(), 16);
assert!(render_request_id(0xABCD_EF01_2345_6789)
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
#[test]
fn the_metadata_key_is_a_legal_grpc_text_key() {
assert_eq!(
REQUEST_ID_METADATA_KEY,
REQUEST_ID_METADATA_KEY.to_ascii_lowercase()
);
assert!(!REQUEST_ID_METADATA_KEY.ends_with("-bin"));
assert_eq!(REQUEST_ID_HEADER.as_str(), REQUEST_ID_METADATA_KEY);
}
#[tokio::test]
async fn the_span_layer_hands_the_called_method_down_to_the_interceptor() {
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct Spy(Arc<Mutex<Option<Option<crate::auth::CalledMethod>>>>);
impl Service<http::Request<Body>> for Spy {
type Response = http::Response<Body>;
type Error = std::convert::Infallible;
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, request: http::Request<Body>) -> Self::Future {
*self.0.lock().expect("the spy") = Some(
request
.extensions()
.get::<crate::auth::CalledMethod>()
.cloned(),
);
std::future::ready(Ok(http::Response::new(Body::empty())))
}
}
let seen = Arc::new(Mutex::new(None));
let mut service =
tower_layer::Layer::layer(&RequestSpanLayer::new(|| 1), Spy(Arc::clone(&seen)));
let request = http::Request::builder()
.uri("http://gunnar.invalid/gunnar.v1.Access/AddKey")
.body(Body::empty())
.expect("a request");
service.call(request).await.expect("infallible");
assert_eq!(
seen.lock().expect("the spy").clone(),
Some(Some(crate::auth::CalledMethod("Access.AddKey".to_owned()))),
"the method the router dispatches on did not reach the gate"
);
let seen = Arc::new(Mutex::new(None));
let mut service =
tower_layer::Layer::layer(&RequestSpanLayer::new(|| 2), Spy(Arc::clone(&seen)));
let request = http::Request::builder()
.uri("http://gunnar.invalid/healthz")
.body(Body::empty())
.expect("a request");
service.call(request).await.expect("infallible");
assert_eq!(
seen.lock().expect("the spy").clone(),
Some(None),
"a non-method path was given a method name"
);
}
#[tokio::test]
async fn a_growing_boxs_deferral_is_info_and_any_other_refusal_stays_warn() {
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct Answers(&'static str, &'static str);
impl Service<http::Request<Body>> for Answers {
type Response = http::Response<Body>;
type Error = std::convert::Infallible;
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _: http::Request<Body>) -> Self::Future {
let mut r = http::Response::new(Body::empty());
r.headers_mut().insert("grpc-status", http::HeaderValue::from_static(self.0));
r.headers_mut().insert("grpc-message", http::HeaderValue::from_static(self.1));
std::future::ready(Ok(r))
}
}
#[derive(Clone, Default)]
struct Heard(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for Heard {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let heard = Heard::default();
let h = heard.clone();
let sub = tracing_subscriber::fmt().with_writer(move || h.clone()).with_ansi(false).finish();
let _g = tracing::subscriber::set_default(sub);
let off_stripe = "unavailable:%20the%20control%20store%20is%20off%20the%20stripe%20while%20the%20data%20set%20grows%20(the%20transaction%20log%20is%20released%20for%20the%20remount);%20writes%20are%20refused%20until%20/data%20is%20mounted%20again%20%E2%80%94%20retry%20in%20a%20few%20minutes";
for (status, message) in [("14", "%5Bprimary-growing%5D%20this%20box's%20data%20set%20is%20being%20grown"), ("7", "principal%20x%20has%20no%20grant"), ("14", off_stripe)] {
let mut service = tower_layer::Layer::layer(&RequestSpanLayer::new(|| 3), Answers(status, message));
let request = http::Request::builder().uri("http://gunnar.invalid/gunnar.v1.Mirror/LogFetch").body(Body::empty()).expect("a request");
service.call(request).await.expect("infallible");
}
let said = String::from_utf8(heard.0.lock().unwrap().clone()).unwrap();
let growing: Vec<&str> = said.lines().filter(|l| l.contains("grpc_status=14")).collect();
assert!(growing.len() == 2 && growing.iter().all(|l| l.contains(" INFO ")), "the growing box's deferral is INFO, in either words (a WARN per retried login trips the log gate's spam rule):\n{said}");
let refused: Vec<&str> = said.lines().filter(|l| l.contains("grpc_status=7")).collect();
assert!(refused.len() == 1 && refused[0].contains(" WARN "), "any other refusal stays WARN:\n{said}");
}
}