//! gRPC 请求日志:包装 tonic 的服务栈,为每次调用打印一行结构化日志。 //! //! gRPC 的错误码放在 HTTP/2 的 trailer 里,所以响应体也必须包一层,等到 trailer //! 到达后才能拿到真实的 `grpc-status`。这里同时兼容把状态写在响应头的情况 //! (反射、健康检查与部分异常路径)。 use std::fmt; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use std::time::Instant; use bytes::Bytes; use envelope::{LEGACY_REQUEST_ID_HEADER, REQUEST_ID_HEADER, new_request_id, validate_request_id}; use http::header::{HeaderName, HeaderValue}; use http::{HeaderMap, Request, Response}; use http_body::{Body, Frame, SizeHint}; use tower::{Layer, Service}; use tracing::Instrument; /// 调用方身份头(可选,缺省表示匿名调用)。 const SERVICE_ID_HEADER: &str = "gitdata-service-id"; const GRPC_STATUS: &str = "grpc-status"; /// gRPC 请求日志层,通过 `tonic::transport::Server::layer` 装配。 #[derive(Clone, Copy, Debug, Default)] pub struct GrpcRequestLog; impl Layer for GrpcRequestLog { type Service = GrpcRequestLogService; fn layer(&self, inner: S) -> Self::Service { GrpcRequestLogService { inner } } } #[derive(Clone, Copy, Debug)] pub struct GrpcRequestLogService { inner: S, } impl Service> for GrpcRequestLogService where S: Service, Response = Response>, S::Future: Send + 'static, ResBody: Body + Send + 'static, { type Response = Response>; type Error = S::Error; type Future = Pin> + Send>>; fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll> { self.inner.poll_ready(context) } fn call(&mut self, mut request: Request) -> Self::Future { let request_id = { let headers = request.headers(); headers .get(REQUEST_ID_HEADER) .or_else(|| headers.get(LEGACY_REQUEST_ID_HEADER)) .and_then(|value| value.to_str().ok()) .and_then(validate_request_id) .unwrap_or_else(new_request_id) }; if let Ok(value) = HeaderValue::from_str(&request_id) { request .headers_mut() .insert(HeaderName::from_static(REQUEST_ID_HEADER), value); } let call = Call::new(&request, request_id.clone()); let span = tracing::info_span!( "grpc.server.request", "otel.kind" = "server", "rpc.system" = "grpc", "rpc.service" = %call.service, "rpc.method" = %call.method, "gitdata.service_id" = %call.caller, request_id = %request_id, ); let traceparent = request .headers() .get(crate::TRACEPARENT) .and_then(|value| value.to_str().ok()) .map(str::to_owned); crate::set_parent_from_traceparent(&span, traceparent.as_deref()); let started = Instant::now(); let future = self.inner.call(request); let context_id = request_id.clone(); Box::pin(envelope::context::with_request_id( Some(context_id), async move { let response = future.await?; let (mut parts, body) = response.into_parts(); if let Ok(value) = HeaderValue::from_str(&request_id) { parts .headers .insert(HeaderName::from_static(REQUEST_ID_HEADER), value); } let header_status = status_of(&parts.headers); // 错误响应(含未认证、反射失败)不带响应体,trailer 永远不会被轮询到, // 所以结束流的情况必须在包装时就记一笔。 let ended = body.is_end_stream(); let mut logged = LoggedBody { inner: Box::pin(body), call, started, header_status, logged: false, }; if ended { logged.finish(header_status); } Ok(Response::from_parts(parts, logged)) } .instrument(span), )) } } /// 一次调用的静态信息,在请求进栈时抓取。 #[derive(Clone, Debug)] struct Call { method: String, service: String, request_id: String, caller: String, } impl Call { fn new(request: &Request, request_id: String) -> Self { let path = request.uri().path(); let (service, method) = match path.rsplit_once('/') { Some((service, method)) => ( service.trim_start_matches('/').to_owned(), method.to_owned(), ), None => (String::new(), path.to_owned()), }; Self { method, service, request_id, caller: header(request.headers(), SERVICE_ID_HEADER), } } fn emit(&self, status: Option, elapsed_ms: f64) { let code = status.unwrap_or(-1); let duration_ms = (elapsed_ms * 1_000.0).round() / 1_000.0; let caller = if self.caller.is_empty() { "anonymous" } else { self.caller.as_str() }; let request_id = if self.request_id.is_empty() { "-" } else { self.request_id.as_str() }; if code == 0 { tracing::info!( "rpc.service" = %self.service, "rpc.method" = %self.method, "rpc.system" = "grpc", "rpc.grpc.status_code" = code, "duration_ms" = duration_ms, "gitdata.service_id" = caller, "request_id" = request_id, "grpc request" ); } else { tracing::warn!( "rpc.service" = %self.service, "rpc.method" = %self.method, "rpc.system" = "grpc", "rpc.grpc.status_code" = code, "duration_ms" = duration_ms, "gitdata.service_id" = caller, "request_id" = request_id, "grpc request failed" ); } } } /// 透传响应体,等 trailer 出来时补上一条日志。 pub struct LoggedBody { inner: Pin>, call: Call, started: Instant, header_status: Option, logged: bool, } impl> LoggedBody { fn finish(&mut self, status: Option) { if self.logged { return; } self.logged = true; let elapsed_ms = self.started.elapsed().as_secs_f64() * 1_000.0; self.call.emit(status, elapsed_ms); } } impl> Body for LoggedBody { type Data = Bytes; type Error = B::Error; fn poll_frame( self: Pin<&mut Self>, context: &mut Context<'_>, ) -> Poll, Self::Error>>> { // `Pin>` 让整体始终 Unpin,无需 unsafe 投影。 let this = self.get_mut(); let polled = this.inner.as_mut().poll_frame(context); match &polled { Poll::Ready(Some(Ok(frame))) => { if let Some(trailers) = frame.trailers_ref() { this.finish(status_of(trailers).or(this.header_status)); } } Poll::Ready(Some(Err(_))) => this.finish(this.header_status), Poll::Ready(None) => this.finish(this.header_status), Poll::Pending => {} } polled } fn is_end_stream(&self) -> bool { self.inner.is_end_stream() } fn size_hint(&self) -> SizeHint { self.inner.size_hint() } } impl fmt::Debug for LoggedBody { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("LoggedBody") .field("call", &self.call) .field("header_status", &self.header_status) .field("logged", &self.logged) .finish_non_exhaustive() } } fn header(headers: &HeaderMap, name: &str) -> String { headers .get(name) .and_then(|value| value.to_str().ok()) .unwrap_or_default() .to_owned() } fn status_of(headers: &HeaderMap) -> Option { headers .get(GRPC_STATUS) .and_then(|value| value.to_str().ok()) .and_then(|value| value.trim().parse().ok()) }