fix(telemetry): unify request ids and log them on every request
ZhenYi committed
Modified app/src/main.rs app/src/main.rs +17 −18 View app/src/main.rs
@@ -147,24 +147,23 @@
147147 .error_handler(export::http::json_error),
148148 )
149149 .app_data(web::PayloadConfig::new(webapi::MAX_REQUEST_BODY_BYTES));
150 app.wrap(auth::session::SessionMiddleware::new(
151 session_config.clone(),
152 session_store.clone(),
153 ))
154 .wrap(auth::envelope::EnvelopeContext)
155 .wrap(webapi::build_cors(&state.config.web))
156 .wrap(from_fn(export::middleware::trace_request))
157 .wrap(oltp::actix::HttpTelemetry::new("gitdata"))
158 .wrap(from_fn(webapi::middleware::revalidate::revalidate))
159 .wrap(from_fn(webapi::middleware::csrf::csrf))
160 .service(web::resource("/api/docs").route(web::get().to(|| async {
161 actix_web::HttpResponse::Found()
162 .insert_header((actix_web::http::header::LOCATION, "/api/docs/"))
163 .finish()
164 })))
165 .service(SwaggerUi::new("/api/docs/{_:.*}").url("/api/docs/openapi.json", doc.clone()))
166 .service(webapi::routes(&deps))
167 .default_service(web::to(webapi::assets::serve))
150 app.wrap(from_fn(webapi::middleware::revalidate::revalidate))
151 .wrap(auth::session::SessionMiddleware::new(
152 session_config.clone(),
153 session_store.clone(),
154 ))
155 .wrap(webapi::build_cors(&state.config.web))
156 .wrap(from_fn(webapi::middleware::csrf::csrf))
157 .wrap(oltp::actix::HttpTelemetry::new("gitdata"))
158 .wrap(from_fn(export::middleware::trace_request))
159 .service(web::resource("/api/docs").route(web::get().to(|| async {
160 actix_web::HttpResponse::Found()
161 .insert_header((actix_web::http::header::LOCATION, "/api/docs/"))
162 .finish()
163 })))
164 .service(SwaggerUi::new("/api/docs/{_:.*}").url("/api/docs/openapi.json", doc.clone()))
165 .service(webapi::routes(&deps))
166 .default_service(web::to(webapi::assets::serve))
168167 })
169168 .disable_signals()
170169 .bind(addr)
Modified lib/envelope/src/context.rs lib/envelope/src/context.rs +9 −0 View lib/envelope/src/context.rs
@@ -13,3 +13,12 @@
1313pub fn request_id() -> Option<String> {
1414 REQUEST_ID.try_with(Clone::clone).ok()
1515}
16
17/// Prefers an explicitly passed id, then the id of the current request scope.
18pub fn resolve_request_id(explicit: Option<&str>) -> String {
19 explicit
20 .filter(|value| !value.is_empty())
21 .map(str::to_owned)
22 .or_else(request_id)
23 .unwrap_or_default()
24}
Modified lib/envelope/src/lib.rs lib/envelope/src/lib.rs +1 −3 View lib/envelope/src/lib.rs
@@ -1,7 +1,5 @@
11pub mod context;
2pub mod middleware;
32pub mod response;
43
5pub use context::{request_id, with_request_id};
6pub use middleware::EnvelopeContext;
4pub use context::{request_id, resolve_request_id, with_request_id};
75pub use response::{Envelope, NoData};
Deleted lib/envelope/src/middleware.rs lib/envelope/src/middleware.rs +0 −102
@@ -1,102 +0,0 @@
1use std::future::{Ready, ready};
2use std::rc::Rc;
3use std::task::{Context, Poll};
4
5use ::session::{REQUEST_ID_HEADER, RequestId, validate_request_id};
6use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
7use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue};
8use actix_web::{Error, HttpMessage};
9use futures_util::future::LocalBoxFuture;
10use uuid::Uuid;
11
12use crate::context;
13
14#[derive(Debug, Clone, Copy, Default)]
15pub struct EnvelopeContext;
16
17impl<S, B> Transform<S, ServiceRequest> for EnvelopeContext
18where
19 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
20 B: 'static,
21{
22 type Response = ServiceResponse<B>;
23 type Error = Error;
24 type Transform = EnvelopeContextMiddleware<S>;
25 type InitError = ();
26 type Future = Ready<Result<Self::Transform, Self::InitError>>;
27
28 fn new_transform(&self, service: S) -> Self::Future {
29 ready(Ok(EnvelopeContextMiddleware {
30 service: Rc::new(service),
31 }))
32 }
33}
34
35pub struct EnvelopeContextMiddleware<S> {
36 service: Rc<S>,
37}
38
39impl<S, B> Service<ServiceRequest> for EnvelopeContextMiddleware<S>
40where
41 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
42 B: 'static,
43{
44 type Response = ServiceResponse<B>;
45 type Error = Error;
46 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
47
48 fn poll_ready(&self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
49 self.service.poll_ready(context)
50 }
51
52 fn call(&self, request: ServiceRequest) -> Self::Future {
53 let service = Rc::clone(&self.service);
54 Box::pin(async move {
55 let request_id = {
56 let request = &request;
57 request
58 .extensions()
59 .get::<RequestId>()
60 .map(|value| value.0.clone())
61 .or_else(|| {
62 validate_request_id(
63 request
64 .headers()
65 .get(REQUEST_ID_HEADER)
66 .and_then(|value| value.to_str().ok()),
67 )
68 })
69 .unwrap_or_else(|| Uuid::now_v7().to_string())
70 };
71 request
72 .extensions_mut()
73 .insert(RequestId(request_id.clone()));
74
75 context::with_request_id(request_id.clone(), async move {
76 let mut response = service.call(request).await?;
77 insert_header(response.headers_mut(), REQUEST_ID_HEADER, &request_id);
78 {
79 let headers = response.headers_mut();
80 const X_CONTENT_TYPE_OPTIONS: HeaderName =
81 HeaderName::from_static("x-content-type-options");
82 const REFERRER_POLICY: HeaderName = HeaderName::from_static("referrer-policy");
83 const CSP: HeaderName = HeaderName::from_static("content-security-policy");
84 headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
85 headers.insert(
86 REFERRER_POLICY,
87 HeaderValue::from_static("strict-origin-when-cross-origin"),
88 );
89 headers.insert(CSP, HeaderValue::from_static("frame-ancestors 'none'"));
90 }
91 Ok(response)
92 })
93 .await
94 })
95 }
96}
97
98fn insert_header(headers: &mut HeaderMap, name: &'static str, value: &str) {
99 if let Ok(value) = HeaderValue::from_str(value) {
100 headers.insert(HeaderName::from_static(name), value);
101 }
102}
Modified lib/export/src/middleware.rs lib/export/src/middleware.rs +11 −9 View lib/export/src/middleware.rs
@@ -7,9 +7,9 @@
77use actix_web::{Error, HttpMessage};
88use uuid::Uuid;
99
10pub const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
10pub use envelope::context::resolve_request_id;
1111
12const MAX_INBOUND_ID_LEN: usize = 128;
12pub const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
1313
1414#[derive(Debug, Clone)]
1515pub struct RequestId(pub String);
@@ -45,12 +45,15 @@
4545 .extensions()
4646 .get::<::session::RequestId>()
4747 .map(|value| value.0.clone())
48 .or_else(|| inbound_id(request.headers().get(&REQUEST_ID_HEADER)))
48 .or_else(|| ::session::validate_request_id(header(&request)))
4949 .unwrap_or_else(new_request_id);
5050
5151 request
5252 .extensions_mut()
5353 .insert(RequestId(request_id.clone()));
54 request
55 .extensions_mut()
56 .insert(::session::RequestId(request_id.clone()));
5457
5558 let response =
5659 envelope::context::with_request_id(request_id.clone(), next.call(request)).await?;
@@ -61,10 +64,9 @@
6164 Ok(response)
6265}
6366
64fn inbound_id(value: Option<&HeaderValue>) -> Option<String> {
65 let value = value?.to_str().ok()?.trim();
66 if value.is_empty() || value.len() > MAX_INBOUND_ID_LEN {
67 return None;
68 }
69 Some(value.to_string())
67fn header(request: &ServiceRequest) -> Option<&str> {
68 request
69 .headers()
70 .get(&REQUEST_ID_HEADER)
71 .and_then(|value| value.to_str().ok())
7072}
Modified lib/oltp/src/actix.rs lib/oltp/src/actix.rs +9 −5 View lib/oltp/src/actix.rs
@@ -70,10 +70,14 @@
7070 let metrics = self.metrics;
7171 let method = request.method().as_str().to_owned();
7272 let path = request.path().to_owned();
73 let request_id = request
74 .extensions()
75 .get::<session::RequestId>()
76 .map(|value| value.0.clone())
73 let request_id = envelope::context::request_id()
74 .or_else(|| {
75 request
76 .extensions()
77 .get::<session::RequestId>()
78 .map(|value| value.0.clone())
79 })
80 .filter(|value| !value.is_empty())
7781 .unwrap_or_default();
7882 let traceparent = request
7983 .headers()
@@ -130,7 +134,7 @@
130134/// 静态资源单独降级:一次页面加载会带出几十个 chunk,按 info 打印会淹没请求日志。
131135/// 探针路径:编排器高频轮询,降到 DEBUG 以免淹没业务日志。
132136const QUIET_PREFIX: &str = "/assets/";
133const PROBE_PATHS: [&str; 2] = ["/health", "/ready"];
137const PROBE_PATHS: [&str; 2] = ["/healthz", "/readyz"];
134138
135139fn log(method: &str, path: &str, status: u16, elapsed: f64, request_id: &str) {
136140 let elapsed_ms = ((elapsed * 1_000.0) * 1_000.0).round() / 1_000.0;
Modified lib/session/src/error.rs lib/session/src/error.rs +16 −5 View lib/session/src/error.rs
@@ -91,16 +91,17 @@
9191 }
9292}
9393
94impl ResponseError for SessionError {
95 fn status_code(&self) -> StatusCode {
94impl SessionError {
95 fn status(&self) -> StatusCode {
9696 match self {
9797 Self::Unauthorized | Self::Purged => StatusCode::UNAUTHORIZED,
9898 _ => StatusCode::INTERNAL_SERVER_ERROR,
9999 }
100100 }
101101
102 fn error_response(&self) -> HttpResponse {
103 let status = self.status_code();
102 /// Envelope response for this error, annotated with the request id.
103 pub fn response(&self, request_id: &str) -> HttpResponse {
104 let status = self.status();
104105 let error_code = match self {
105106 Self::Unauthorized => "unauthenticated",
106107 Self::Purged => "session_revoked",
@@ -117,11 +118,21 @@
117118 "data": null,
118119 "error": null,
119120 "error_code": error_code,
120 "request_id": null,
121 "request_id": request_id,
121122 }))
122123 }
123124}
124125
126impl ResponseError for SessionError {
127 fn status_code(&self) -> StatusCode {
128 self.status()
129 }
130
131 fn error_response(&self) -> HttpResponse {
132 self.response("")
133 }
134}
135
125136impl From<CacheError> for SessionError {
126137 fn from(value: CacheError) -> Self {
127138 Self::Store(value)
Modified lib/session/src/guard.rs lib/session/src/guard.rs +13 −4 View lib/session/src/guard.rs
@@ -4,6 +4,7 @@
44use actix_web::{Error, HttpMessage};
55
66use crate::error::SessionError;
7use crate::extract::RequestId;
78use crate::state::Session;
89
910pub fn require_uid(session: &Session) -> Result<i64, SessionError> {
@@ -17,11 +18,19 @@
1718where
1819 B: MessageBody + 'static,
1920{
20 let session = request
21 let authorized = request
2122 .extensions()
2223 .get::<Session>()
23 .cloned()
24 .ok_or(SessionError::Unauthorized)?;
25 require_uid(&session)?;
24 .and_then(|session| session.uid())
25 .is_some();
26 if !authorized {
27 let request_id = request
28 .extensions()
29 .get::<RequestId>()
30 .map(|value| value.0.clone())
31 .unwrap_or_default();
32 let response = SessionError::Unauthorized.response(&request_id);
33 return Ok(request.into_response(response));
34 }
2635 Ok(next.call(request).await?.map_into_boxed_body())
2736}
Modified lib/webapi/src/middleware/csrf.rs lib/webapi/src/middleware/csrf.rs +1 −1 View lib/webapi/src/middleware/csrf.rs
@@ -59,7 +59,7 @@
5959 "data": null,
6060 "error": null,
6161 "error_code": "csrf_rejected",
62 "request_id": null,
62 "request_id": export::middleware::request_id(),
6363 })))
6464 .map_into_boxed_body());
6565 }
Modified lib/webapi/src/middleware/revalidate.rs lib/webapi/src/middleware/revalidate.rs +1 −1 View lib/webapi/src/middleware/revalidate.rs
@@ -86,7 +86,7 @@
8686 "data": null,
8787 "error": null,
8888 "error_code": "session_revoked",
89 "request_id": null,
89 "request_id": export::middleware::request_id(),
9090 }));
9191 append_cookie(
9292 &mut response,
Modified lib/webapi/src/lib.rs lib/webapi/src/lib.rs +2 −1 View lib/webapi/src/lib.rs
@@ -28,7 +28,8 @@
2828 Method::DELETE,
2929 Method::OPTIONS,
3030 ])
31 .allowed_headers(["content-type", "authorization", "x-request-id"]);
31 .allowed_headers(["content-type", "authorization", "x-request-id"])
32 .expose_headers(["x-request-id"]);
3233 if web.cors_allow_credentials {
3334 cors = cors.supports_credentials();
3435 }