#![deny(missing_docs)]
use async_trait::async_trait;
use churust_core::{AppBuilder, Call, Middleware, Next, Phase, Plugin, Response};
use std::sync::Arc;
use std::time::Instant;
use tracing::Level;
#[derive(Debug, Clone)]
pub struct CallLogging {
level: Level,
}
impl Default for CallLogging {
fn default() -> Self {
Self { level: Level::INFO }
}
}
impl CallLogging {
pub fn new() -> Self {
Self::default()
}
pub fn level(mut self, level: Level) -> Self {
self.level = level;
self
}
}
impl Plugin for CallLogging {
fn install(self: Box<Self>, app: &mut AppBuilder) {
app.add_middleware_in(
Phase::Monitoring,
Arc::new(LogMiddleware { level: self.level }),
);
}
}
struct LogMiddleware {
level: Level,
}
#[async_trait]
impl Middleware for LogMiddleware {
async fn handle(&self, mut call: Call, next: Next) -> Response {
let method = call.method().clone();
let path = call.path().to_string();
let id = RequestId::from_call(&call);
let trace_id = id.trace_id.clone();
let request_id = id.request_id.clone();
call.insert(id);
let start = Instant::now();
let mut res = next.run(call).await;
let latency_ms = start.elapsed().as_millis();
let status = res.status.as_u16();
if let Ok(v) = http::HeaderValue::from_str(&request_id) {
res.headers
.insert(http::header::HeaderName::from_static("x-request-id"), v);
}
match self.level {
Level::ERROR => {
tracing::error!(%method, path, status, latency_ms, request_id, trace_id, "request")
}
Level::WARN => {
tracing::warn!(%method, path, status, latency_ms, request_id, trace_id, "request")
}
Level::INFO => {
tracing::info!(%method, path, status, latency_ms, request_id, trace_id, "request")
}
Level::DEBUG => {
tracing::debug!(%method, path, status, latency_ms, request_id, trace_id, "request")
}
Level::TRACE => {
tracing::trace!(%method, path, status, latency_ms, request_id, trace_id, "request")
}
}
res
}
}
#[derive(Debug, Clone)]
pub struct RequestId {
pub request_id: String,
pub trace_id: String,
}
impl RequestId {
fn from_call(call: &Call) -> Self {
let inbound = call.header("traceparent").and_then(|v| {
let parts: Vec<&str> = v.split('-').collect();
(parts.len() >= 3
&& parts[1].len() == 32
&& parts[1].chars().all(|c| c.is_ascii_hexdigit())
&& parts[1].chars().any(|c| c != '0'))
.then(|| parts[1].to_string())
});
Self {
request_id: gen_hex(16),
trace_id: inbound.unwrap_or_else(|| gen_hex(32)),
}
}
}
fn gen_hex(n: usize) -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id() as u64;
let mut out = String::with_capacity(n);
let mut state = nanos ^ (seq.wrapping_mul(0x9E37_79B9_7F4A_7C15)) ^ (pid << 32);
while out.len() < n {
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
let v = state.wrapping_mul(0x2545_F491_4F6C_DD1D);
out.push_str(&format!("{v:016x}"));
}
out.truncate(n);
out
}
#[cfg(test)]
mod tests {
use super::*;
use churust_core::{App, Churust, TestClient};
use http::StatusCode;
fn app() -> App {
Churust::server()
.install(CallLogging::new())
.routing(|r| {
r.get("/", |_c: Call| async { "ok" });
})
.build()
}
#[tokio::test]
async fn logging_is_transparent() {
let client = TestClient::new(app());
let res = client.get("/").send().await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text(), "ok");
}
}