mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-17 10:17:55 +00:00
refactor(request-id): align contracts and lock field names (#3454)
* refactor(request-id): align header log and trace contracts * test(contract): lock external request-id field names * chore(deps): drop unused rustfs-ecstore links Co-Authored-By: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -28,7 +28,6 @@ categories = ["web-programming", "development-tools", "asynchronous", "api-bindi
|
||||
[dependencies]
|
||||
rustfs-targets = { workspace = true }
|
||||
rustfs-config = { workspace = true, features = ["audit", "constants", "server-config-model"] }
|
||||
rustfs-ecstore = { workspace = true }
|
||||
rustfs-s3-types = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
const-str = { workspace = true }
|
||||
|
||||
@@ -160,6 +160,8 @@ pub struct AuditEntry {
|
||||
pub api: ApiDetails,
|
||||
#[serde(rename = "remotehost", skip_serializing_if = "Option::is_none")]
|
||||
pub remote_host: Option<String>,
|
||||
// Historical external audit contract: keep `requestID` instead of normalizing
|
||||
// this field to `request_id` or `request-id`.
|
||||
#[serde(rename = "requestID", skip_serializing_if = "Option::is_none")]
|
||||
pub request_id: Option<String>,
|
||||
#[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
|
||||
@@ -315,3 +317,29 @@ impl AuditEntryBuilder {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
fn audit_entry_serializes_historical_request_id_field_name() {
|
||||
let entry = AuditEntryBuilder::new(
|
||||
"1",
|
||||
EventName::ObjectCreatedPut,
|
||||
"s3",
|
||||
ApiDetailsBuilder::new()
|
||||
.name("PutObject")
|
||||
.status("OK")
|
||||
.status_code(200)
|
||||
.build(),
|
||||
)
|
||||
.request_id("req-audit-123")
|
||||
.build();
|
||||
|
||||
let value = serde_json::to_value(entry).expect("audit entry should serialize");
|
||||
assert_eq!(value["requestID"], Value::String("req-audit-123".to_string()));
|
||||
assert!(value.get("request_id").is_none(), "historical audit contract must not expose request_id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ pub struct QuotaErrorResponse {
|
||||
pub message: String,
|
||||
#[serde(rename = "Resource")]
|
||||
pub resource: String,
|
||||
// External quota error contract follows the existing PascalCase error schema.
|
||||
#[serde(rename = "RequestId")]
|
||||
pub request_id: String,
|
||||
#[serde(rename = "HostId")]
|
||||
@@ -168,6 +169,7 @@ impl QuotaErrorResponse {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Legacy format: quota, created_at, updated_at (no quota_type)
|
||||
#[test]
|
||||
@@ -217,4 +219,19 @@ mod tests {
|
||||
assert_eq!(q.quota, Some(1073741824));
|
||||
assert_eq!(q.quota_type, QuotaType::Hard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quota_error_response_serializes_request_id_as_pascal_case_contract() {
|
||||
let response = QuotaErrorResponse::new(
|
||||
&QuotaError::InvalidConfig {
|
||||
reason: "bad quota".to_string(),
|
||||
},
|
||||
"req-quota-123",
|
||||
"host-quota-1",
|
||||
);
|
||||
|
||||
let value = serde_json::to_value(response).expect("quota error response should serialize");
|
||||
assert_eq!(value["RequestId"], Value::String("req-quota-123".to_string()));
|
||||
assert!(value.get("request_id").is_none(), "quota error contract must not expose request_id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ pub struct ErrorResponse {
|
||||
pub bucket_name: String,
|
||||
pub key: String,
|
||||
pub resource: String,
|
||||
// External S3-style error response contract: keep `RequestId`.
|
||||
#[serde(rename = "RequestId")]
|
||||
pub request_id: String,
|
||||
pub host_id: String,
|
||||
pub region: String,
|
||||
@@ -302,3 +304,29 @@ pub fn err_api_not_supported(message: &str) -> ErrorResponse {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
fn error_response_serializes_request_id_as_pascal_case_contract() {
|
||||
let response = ErrorResponse {
|
||||
code: S3ErrorCode::InvalidArgument,
|
||||
message: "bad request".to_string(),
|
||||
bucket_name: "bucket".to_string(),
|
||||
key: "key".to_string(),
|
||||
resource: "/bucket/key".to_string(),
|
||||
request_id: "req-xml-123".to_string(),
|
||||
host_id: "host-1".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
server: "rustfs".to_string(),
|
||||
status_code: StatusCode::BAD_REQUEST,
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(response).expect("error response should serialize");
|
||||
assert_eq!(value["RequestId"], Value::String("req-xml-123".to_string()));
|
||||
assert!(value.get("request_id").is_none(), "external error contract must not expose request_id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,12 +134,42 @@ impl tonic::service::Interceptor for TonicInterceptor {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use opentelemetry::global;
|
||||
use opentelemetry::trace::{SpanContext, TraceContextExt, TraceFlags, TraceId, TraceState, TracerProvider as _};
|
||||
use opentelemetry_sdk::propagation::TraceContextPropagator;
|
||||
use opentelemetry_sdk::trace::SdkTracerProvider;
|
||||
use tonic::service::Interceptor;
|
||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
use tracing_subscriber::{Registry, layer::SubscriberExt};
|
||||
|
||||
fn ensure_test_rpc_secret() {
|
||||
let _ = rustfs_credentials::GLOBAL_RUSTFS_RPC_SECRET.set("test-rpc-secret".to_string());
|
||||
}
|
||||
|
||||
fn with_trace_parent<F>(trace_id_hex: &str, f: F)
|
||||
where
|
||||
F: FnOnce(),
|
||||
{
|
||||
global::set_text_map_propagator(TraceContextPropagator::new());
|
||||
|
||||
let provider = SdkTracerProvider::builder().build();
|
||||
let tracer = provider.tracer("rpc-client-tests");
|
||||
let subscriber = Registry::default().with(tracing_opentelemetry::layer().with_tracer(tracer));
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
let span = tracing::info_span!("rpc-client-test-span");
|
||||
let trace_id = TraceId::from_hex(trace_id_hex).expect("trace id should be valid hex");
|
||||
let span_id = opentelemetry::trace::SpanId::from_hex("0102030405060708").expect("span id should be valid hex");
|
||||
let parent = SpanContext::new(trace_id, span_id, TraceFlags::SAMPLED, true, TraceState::default());
|
||||
span.set_parent(opentelemetry::Context::new().with_remote_span_context(parent))
|
||||
.expect("failed to set parent context");
|
||||
let _guard = span.enter();
|
||||
|
||||
f();
|
||||
});
|
||||
let _ = provider.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_interceptor_keeps_auth_headers() {
|
||||
ensure_test_rpc_secret();
|
||||
@@ -166,4 +196,21 @@ mod tests {
|
||||
assert!(!v.as_encoded_bytes().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_interceptor_injects_traceparent_metadata() {
|
||||
ensure_test_rpc_secret();
|
||||
let mut interceptor = TonicSignatureInterceptor;
|
||||
let req = tonic::Request::new(());
|
||||
|
||||
with_trace_parent("4bf92f3577b34da6a3ce929d0e0e4736", || {
|
||||
let req = interceptor.call(req).expect("interceptor call should succeed");
|
||||
let traceparent = req
|
||||
.metadata()
|
||||
.get("traceparent")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.expect("traceparent metadata should be injected");
|
||||
assert!(traceparent.starts_with("00-4bf92f3577b34da6a3ce929d0e0e4736-"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@
|
||||
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use opentelemetry::{global, propagation::Injector, trace::TraceContextExt};
|
||||
pub(crate) use rustfs_utils::http::headers::REQUEST_ID_HEADER;
|
||||
use tracing::Span;
|
||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
|
||||
pub(crate) const REQUEST_ID_HEADER: &str = "x-request-id";
|
||||
|
||||
struct HttpHeaderInjector<'a> {
|
||||
headers: &'a mut HeaderMap,
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ const LOG_SUBSYSTEM_LOCAL_LOGGING: &str = "local_logging";
|
||||
const EVENT_LOCAL_LOGGING_STATE: &str = "local_logging_state";
|
||||
const EVENT_LOG_CLEANER_STATE: &str = "log_cleaner_state";
|
||||
const STDERR_WARNING_PREFIX: &str = "[WARN]";
|
||||
const REQUEST_ID: &str = "request-id";
|
||||
const REQUEST_ID_CANONICAL: &str = "request_id";
|
||||
const REQUEST_ID_COMPAT: &str = "request-id";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RequestIdJsonFormat<T> {
|
||||
@@ -106,9 +107,15 @@ where
|
||||
let trimmed = buffer.trim_end();
|
||||
let mut payload: JsonValue = serde_json::from_str(trimmed).map_err(|_| fmt::Error)?;
|
||||
if let Some(object) = payload.as_object_mut() {
|
||||
let request_id = request_id.expect("checked is_some");
|
||||
object
|
||||
.entry(REQUEST_ID.to_string())
|
||||
.or_insert_with(|| JsonValue::String(request_id.expect("checked is_some")));
|
||||
.entry(REQUEST_ID_CANONICAL.to_string())
|
||||
.or_insert_with(|| JsonValue::String(request_id.clone()));
|
||||
// Keep a top-level compatibility alias for operators or downstream
|
||||
// queries that already rely on the earlier hyphenated field name.
|
||||
object
|
||||
.entry(REQUEST_ID_COMPAT.to_string())
|
||||
.or_insert_with(|| JsonValue::String(request_id));
|
||||
}
|
||||
|
||||
let serialized = serde_json::to_string(&payload).map_err(|_| fmt::Error)?;
|
||||
@@ -131,7 +138,11 @@ where
|
||||
let extensions = span.extensions();
|
||||
let formatted_fields = extensions.get::<tracing_subscriber::fmt::FormattedFields<JsonFields>>()?;
|
||||
let fields: BTreeMap<String, JsonValue> = serde_json::from_str(&formatted_fields.fields).ok()?;
|
||||
if let Some(value) = fields.get(REQUEST_ID).and_then(JsonValue::as_str) {
|
||||
if let Some(value) = fields
|
||||
.get(REQUEST_ID_CANONICAL)
|
||||
.or_else(|| fields.get(REQUEST_ID_COMPAT))
|
||||
.and_then(JsonValue::as_str)
|
||||
{
|
||||
request_id = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
@@ -842,6 +853,7 @@ mod tests {
|
||||
let parsed = render_json_log_with_request_span();
|
||||
|
||||
assert_eq!(parsed["request_id"], Value::String("req-123".to_string()));
|
||||
assert_eq!(parsed["request-id"], Value::String("req-123".to_string()));
|
||||
assert_eq!(parsed["message"], Value::String("inside request span".to_string()));
|
||||
assert_eq!(parsed["span"]["request_id"], Value::String("req-123".to_string()));
|
||||
}
|
||||
@@ -851,6 +863,7 @@ mod tests {
|
||||
let parsed = render_json_log_with_recovery_monitor_child_span();
|
||||
|
||||
assert_eq!(parsed["request_id"], Value::String("req-parent".to_string()));
|
||||
assert_eq!(parsed["request-id"], Value::String("req-parent".to_string()));
|
||||
assert_eq!(parsed["message"], Value::String("inside recovery monitor".to_string()));
|
||||
assert_eq!(parsed["span"]["name"], Value::String("recovery-monitor".to_string()));
|
||||
assert_eq!(parsed["span"]["kind"], Value::String("remote_disk".to_string()));
|
||||
|
||||
@@ -13,7 +13,6 @@ documentation = "https://docs.rs/rustfs-targets/latest/rustfs_targets/"
|
||||
|
||||
[dependencies]
|
||||
rustfs-config = { workspace = true, features = ["notify", "constants", "audit", "server-config-model"] }
|
||||
rustfs-ecstore = { workspace = true }
|
||||
rustfs-extension-schema = { workspace = true }
|
||||
rustfs-tls-runtime = { workspace = true }
|
||||
rustfs-s3-types = { workspace = true }
|
||||
|
||||
@@ -151,7 +151,8 @@ pub const AMZ_ENCRYPTION_KMS: &str = "aws:kms";
|
||||
pub const AMZ_SIGNATURE_V2: &str = "Signature";
|
||||
pub const AMZ_ACCESS_KEY_ID: &str = "AWSAccessKeyId";
|
||||
|
||||
// Response request id.
|
||||
// Request id headers.
|
||||
pub const REQUEST_ID_HEADER: &str = "x-request-id";
|
||||
pub const AMZ_REQUEST_ID: &str = "x-amz-request-id";
|
||||
pub const AMZ_REQUEST_HOST_ID: &str = "x-amz-id-2";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user