mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 16:07:05 +00:00
refactor(server): consolidate request transport logs (#3354)
This commit is contained in:
+184
-79
@@ -23,7 +23,8 @@ use crate::server::{
|
|||||||
hybrid::hybrid,
|
hybrid::hybrid,
|
||||||
layer::{
|
layer::{
|
||||||
BodylessStatusFixLayer, ConditionalCorsLayer, EmptyBodyContentLengthCompatLayer, HeadRequestBodyFixLayer,
|
BodylessStatusFixLayer, ConditionalCorsLayer, EmptyBodyContentLengthCompatLayer, HeadRequestBodyFixLayer,
|
||||||
ObjectAttributesEtagFixLayer, PublicHealthEndpointLayer, RedirectLayer, RequestContextLayer, S3ErrorMessageCompatLayer,
|
ObjectAttributesEtagFixLayer, PublicHealthEndpointLayer, RedirectLayer, RequestContextLayer, RequestLoggingLayer,
|
||||||
|
S3ErrorMessageCompatLayer,
|
||||||
},
|
},
|
||||||
tls_material::{
|
tls_material::{
|
||||||
TlsAcceptorHolder, TlsHandshakeFailureKind, build_acceptor_from_loaded, load_tls_material, spawn_reload_loop,
|
TlsAcceptorHolder, TlsHandshakeFailureKind, build_acceptor_from_loaded, load_tls_material, spawn_reload_loop,
|
||||||
@@ -66,7 +67,7 @@ use tower_http::catch_panic::CatchPanicLayer;
|
|||||||
use tower_http::compression::CompressionLayer;
|
use tower_http::compression::CompressionLayer;
|
||||||
use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
|
use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::{Span, debug, error, info, instrument, warn};
|
use tracing::{Span, debug, error, info, instrument, trace, warn};
|
||||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||||
|
|
||||||
const LABEL_HTTP_METHOD: &str = "method";
|
const LABEL_HTTP_METHOD: &str = "method";
|
||||||
@@ -79,6 +80,13 @@ const METRIC_HTTP_SERVER_REQUEST_BODY_BYTES_TOTAL: &str = "rustfs_http_server_re
|
|||||||
const METRIC_HTTP_SERVER_REQUEST_BODY_SIZE_BYTES: &str = "rustfs_http_server_request_body_size_bytes";
|
const METRIC_HTTP_SERVER_REQUEST_BODY_SIZE_BYTES: &str = "rustfs_http_server_request_body_size_bytes";
|
||||||
const METRIC_HTTP_SERVER_RESPONSE_BODY_BYTES_TOTAL: &str = "rustfs_http_server_response_body_bytes_total";
|
const METRIC_HTTP_SERVER_RESPONSE_BODY_BYTES_TOTAL: &str = "rustfs_http_server_response_body_bytes_total";
|
||||||
const METRIC_HTTP_SERVER_RESPONSE_BODY_SIZE_BYTES: &str = "rustfs_http_server_response_body_size_bytes";
|
const METRIC_HTTP_SERVER_RESPONSE_BODY_SIZE_BYTES: &str = "rustfs_http_server_response_body_size_bytes";
|
||||||
|
const LOG_COMPONENT_SERVER: &str = "server";
|
||||||
|
const LOG_SUBSYSTEM_HTTP: &str = "http";
|
||||||
|
const LOG_SUBSYSTEM_TRANSPORT: &str = "transport";
|
||||||
|
const LOG_SUBSYSTEM_TLS: &str = "tls";
|
||||||
|
const EVENT_TLS_HANDSHAKE_FAILED: &str = "tls_handshake_failed";
|
||||||
|
const EVENT_HTTP_TRANSPORT_CLOSED: &str = "http_transport_closed";
|
||||||
|
const EVENT_HTTP_TRANSPORT_FAILED: &str = "http_transport_failed";
|
||||||
|
|
||||||
static ACTIVE_HTTP_REQUESTS: AtomicU64 = AtomicU64::new(0);
|
static ACTIVE_HTTP_REQUESTS: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
@@ -110,6 +118,78 @@ fn status_class_label(status: http::StatusCode) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn duration_ms(duration: Duration) -> u64 {
|
||||||
|
duration.as_millis().try_into().unwrap_or(u64::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_tls_handshake_failure(peer_addr: &str, kind: TlsHandshakeFailureKind, err: &dyn std::fmt::Display) {
|
||||||
|
match kind {
|
||||||
|
TlsHandshakeFailureKind::UnexpectedEof => {
|
||||||
|
debug!(
|
||||||
|
event = EVENT_TLS_HANDSHAKE_FAILED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TLS,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
failure_type = kind.as_str(),
|
||||||
|
error = %err,
|
||||||
|
result = "client_disconnect",
|
||||||
|
"TLS handshake failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
TlsHandshakeFailureKind::ProtocolVersion | TlsHandshakeFailureKind::Certificate | TlsHandshakeFailureKind::Alert => {
|
||||||
|
warn!(
|
||||||
|
event = EVENT_TLS_HANDSHAKE_FAILED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TLS,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
failure_type = kind.as_str(),
|
||||||
|
error = %err,
|
||||||
|
result = "client_error",
|
||||||
|
"TLS handshake failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
TlsHandshakeFailureKind::Unknown => {
|
||||||
|
error!(
|
||||||
|
event = EVENT_TLS_HANDSHAKE_FAILED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TLS,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
failure_type = kind.as_str(),
|
||||||
|
error = %err,
|
||||||
|
result = "transport_error",
|
||||||
|
"TLS handshake failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_transport_closed(peer_addr: &str, error_kind: &str, error_message: &str, result: &str) {
|
||||||
|
debug!(
|
||||||
|
event = EVENT_HTTP_TRANSPORT_CLOSED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TRANSPORT,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
error_kind,
|
||||||
|
error = %error_message,
|
||||||
|
result,
|
||||||
|
"HTTP transport closed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_transport_failed(peer_addr: &str, error_kind: &str, error_message: &str) {
|
||||||
|
warn!(
|
||||||
|
event = EVENT_HTTP_TRANSPORT_FAILED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TRANSPORT,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
error_kind,
|
||||||
|
error = %error_message,
|
||||||
|
result = "transport_error",
|
||||||
|
"HTTP transport failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn record_active_http_requests(delta: i64) {
|
fn record_active_http_requests(delta: i64) {
|
||||||
let next = if delta >= 0 {
|
let next = if delta >= 0 {
|
||||||
@@ -423,7 +503,7 @@ pub async fn start_http_server(config: &config::Config, readiness: Arc<GlobalRea
|
|||||||
debug!("graceful initiated");
|
debug!("graceful initiated");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
debug!("Waiting for new connection...");
|
trace!("Waiting for new connection");
|
||||||
let (socket, _) = {
|
let (socket, _) = {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -665,17 +745,18 @@ fn process_connection(
|
|||||||
// 7. CatchPanicLayer — panic → 500
|
// 7. CatchPanicLayer — panic → 500
|
||||||
// 8. ReadinessGateLayer — blocks until ready
|
// 8. ReadinessGateLayer — blocks until ready
|
||||||
// 9. KeystoneAuthLayer — X-Auth-Token validation
|
// 9. KeystoneAuthLayer — X-Auth-Token validation
|
||||||
// 10. TraceLayer — request/response tracing + metrics
|
// 10. TraceLayer — request span creation + metrics
|
||||||
// 11. PropagateRequestIdLayer — X-Request-ID → response
|
// 11. RequestLoggingLayer — single completion event per request
|
||||||
// 12. CompressionLayer — response compression (whitelist, path-aware)
|
// 12. PropagateRequestIdLayer — X-Request-ID → response
|
||||||
// 13. PathCategoryInjectionLayer — injects path category for compression predicate
|
// 13. CompressionLayer — response compression (whitelist, path-aware)
|
||||||
// 14. S3ErrorMessageCompatLayer — missing S3 error message compatibility
|
// 14. PathCategoryInjectionLayer — injects path category for compression predicate
|
||||||
// 15. ObjectAttributesEtagFixLayer — ETag fix for GetObjectAttributes
|
// 15. S3ErrorMessageCompatLayer — missing S3 error message compatibility
|
||||||
// 16. ConditionalCorsLayer — S3 API CORS
|
// 16. ObjectAttributesEtagFixLayer — ETag fix for GetObjectAttributes
|
||||||
// 17. RedirectLayer — console redirect (conditional)
|
// 17. ConditionalCorsLayer — S3 API CORS
|
||||||
// 18. BodylessStatusFixLayer — clears body for 1xx/204/205/304 responses
|
// 18. RedirectLayer — console redirect (conditional)
|
||||||
// 19. HeadRequestBodyFixLayer — strips actual body bytes from HEAD responses
|
// 19. BodylessStatusFixLayer — clears body for 1xx/204/205/304 responses
|
||||||
// 20. PublicHealthEndpointLayer — handles public health before s3s host parsing
|
// 20. HeadRequestBodyFixLayer — strips actual body bytes from HEAD responses
|
||||||
|
// 21. PublicHealthEndpointLayer — handles public health before s3s host parsing
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
let hybrid_service = ServiceBuilder::new()
|
let hybrid_service = ServiceBuilder::new()
|
||||||
// NOTE: Both extension types are intentionally inserted to maintain compatibility:
|
// NOTE: Both extension types are intentionally inserted to maintain compatibility:
|
||||||
@@ -705,10 +786,15 @@ fn process_connection(
|
|||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http()
|
TraceLayer::new_for_http()
|
||||||
.make_span_with(|request: &HttpRequest<_>| {
|
.make_span_with(|request: &HttpRequest<_>| {
|
||||||
let request_id = request
|
let request_context = request.extensions().get::<crate::storage::request_context::RequestContext>();
|
||||||
.headers()
|
let request_id = request_context
|
||||||
.get(http::header::HeaderName::from_static("x-request-id"))
|
.map(|ctx| ctx.request_id.as_str())
|
||||||
.and_then(|v| v.to_str().ok())
|
.unwrap_or("unknown");
|
||||||
|
let trace_id = request_context
|
||||||
|
.and_then(|ctx| ctx.trace_id.as_deref())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let span_id = request_context
|
||||||
|
.and_then(|ctx| ctx.span_id.as_deref())
|
||||||
.unwrap_or("unknown");
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
let parent_context = global::get_text_map_propagator(|propagator| {
|
let parent_context = global::get_text_map_propagator(|propagator| {
|
||||||
@@ -718,38 +804,49 @@ fn process_connection(
|
|||||||
// Log trace context extraction for debugging distributed tracing
|
// Log trace context extraction for debugging distributed tracing
|
||||||
if parent_context.has_active_span() {
|
if parent_context.has_active_span() {
|
||||||
let span_ref = parent_context.span();
|
let span_ref = parent_context.span();
|
||||||
debug!(
|
trace!(
|
||||||
otel_trace_id = %span_ref.span_context().trace_id(),
|
otel_trace_id = %span_ref.span_context().trace_id(),
|
||||||
otel_parent_span_id = %span_ref.span_context().span_id(),
|
otel_parent_span_id = %span_ref.span_context().span_id(),
|
||||||
sampled = span_ref.span_context().is_sampled(),
|
sampled = span_ref.span_context().is_sampled(),
|
||||||
"Extracted trace context from incoming request headers"
|
"Extracted trace context from incoming request headers"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debug!("No trace context found in request headers, will create root span");
|
trace!("No trace context found in request headers, will create root span");
|
||||||
}
|
}
|
||||||
// Extract real client IP from trusted proxy middleware if available
|
// Extract real client IP from trusted proxy middleware if available
|
||||||
let client_info = request.extensions().get::<ClientInfo>();
|
let client_info = request.extensions().get::<ClientInfo>();
|
||||||
let real_ip = client_info
|
let peer_addr = client_info
|
||||||
.map(|info| info.real_ip.to_string())
|
.map(|info| info.real_ip.to_string())
|
||||||
|
.or_else(|| request.extensions().get::<RemoteAddr>().map(|addr| addr.0.to_string()))
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
let span = tracing::info_span!("http-request",
|
let span = tracing::info_span!("http-request",
|
||||||
request_id = %request_id,
|
request_id = %request_id,
|
||||||
|
trace_id = %trace_id,
|
||||||
|
span_id = %span_id,
|
||||||
status_code = tracing::field::Empty,
|
status_code = tracing::field::Empty,
|
||||||
method = %request.method(),
|
method = %request.method(),
|
||||||
real_ip = %real_ip,
|
peer_addr = %peer_addr,
|
||||||
uri = %request.uri(),
|
uri = %request.uri(),
|
||||||
version = ?request.version(),
|
version = ?request.version(),
|
||||||
|
user_agent = tracing::field::Empty,
|
||||||
|
content_type = tracing::field::Empty,
|
||||||
|
content_length = tracing::field::Empty,
|
||||||
);
|
);
|
||||||
if span.is_disabled() {
|
if span.is_disabled() {
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
if let Err(e) = span.set_parent(parent_context) {
|
if let Err(e) = span.set_parent(parent_context) {
|
||||||
warn!("Failed to propagate tracing context: `{:?}`", e);
|
debug!(component = LOG_COMPONENT_SERVER, subsystem = LOG_SUBSYSTEM_HTTP, error = ?e, "Failed to propagate tracing context");
|
||||||
}
|
}
|
||||||
for (header_name, header_value) in request.headers() {
|
for (header_name, header_value) in request.headers() {
|
||||||
if header_name == "user-agent" || header_name == "content-type" || header_name == "content-length" {
|
let value = header_value.to_str().unwrap_or("invalid");
|
||||||
span.record(header_name.as_str(), header_value.to_str().unwrap_or("invalid"));
|
if header_name == "user-agent" {
|
||||||
|
span.record("user_agent", value);
|
||||||
|
} else if header_name == "content-type" {
|
||||||
|
span.record("content_type", value);
|
||||||
|
} else if header_name == "content-length" {
|
||||||
|
span.record("content_length", value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,7 +854,7 @@ fn process_connection(
|
|||||||
})
|
})
|
||||||
.on_request(|request: &HttpRequest<_>, span: &Span| {
|
.on_request(|request: &HttpRequest<_>, span: &Span| {
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
debug!("http started method: {}, url path: {}", request.method(), request.uri().path());
|
trace!("HTTP request started");
|
||||||
let method = request_method_label(request.method());
|
let method = request_method_label(request.method());
|
||||||
record_active_http_requests(1);
|
record_active_http_requests(1);
|
||||||
counter!(
|
counter!(
|
||||||
@@ -803,14 +900,13 @@ fn process_connection(
|
|||||||
)
|
)
|
||||||
.record(len as f64);
|
.record(len as f64);
|
||||||
}
|
}
|
||||||
debug!("http response generated in {:?}", latency)
|
|
||||||
})
|
})
|
||||||
.on_body_chunk(|chunk: &Bytes, latency: Duration, span: &Span| {
|
.on_body_chunk(|chunk: &Bytes, latency: Duration, span: &Span| {
|
||||||
counter!(METRIC_HTTP_SERVER_RESPONSE_BODY_BYTES_TOTAL).increment(chunk.len() as u64);
|
counter!(METRIC_HTTP_SERVER_RESPONSE_BODY_BYTES_TOTAL).increment(chunk.len() as u64);
|
||||||
#[cfg(feature = "tracing-chunk-debug")]
|
#[cfg(feature = "tracing-chunk-debug")]
|
||||||
{
|
{
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
debug!("http body sending {} bytes in {:?}", chunk.len(), latency);
|
debug!(chunk_bytes = chunk.len(), duration_ms = duration_ms(latency), "HTTP response body chunk sent");
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "tracing-chunk-debug"))]
|
#[cfg(not(feature = "tracing-chunk-debug"))]
|
||||||
{
|
{
|
||||||
@@ -821,7 +917,7 @@ fn process_connection(
|
|||||||
#[cfg(feature = "tracing-chunk-debug")]
|
#[cfg(feature = "tracing-chunk-debug")]
|
||||||
{
|
{
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
debug!("http stream closed after {:?}", stream_duration);
|
debug!(duration_ms = duration_ms(stream_duration), "HTTP response stream closed");
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "tracing-chunk-debug"))]
|
#[cfg(not(feature = "tracing-chunk-debug"))]
|
||||||
{
|
{
|
||||||
@@ -836,9 +932,10 @@ fn process_connection(
|
|||||||
LABEL_HTTP_STATUS_CLASS => "transport"
|
LABEL_HTTP_STATUS_CLASS => "transport"
|
||||||
)
|
)
|
||||||
.increment(1);
|
.increment(1);
|
||||||
debug!("http request failure error: {:?} in {:?}", _error, latency)
|
trace!(error = ?_error, duration_ms = duration_ms(latency), "HTTP request failure captured by trace layer");
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.layer(RequestLoggingLayer)
|
||||||
.layer(PropagateRequestIdLayer::x_request_id())
|
.layer(PropagateRequestIdLayer::x_request_id())
|
||||||
// Compress responses based on whitelist configuration
|
// Compress responses based on whitelist configuration
|
||||||
// Only compresses when enabled and matches configured extensions/MIME types
|
// Only compresses when enabled and matches configured extensions/MIME types
|
||||||
@@ -872,7 +969,7 @@ fn process_connection(
|
|||||||
|
|
||||||
// Decide whether to handle HTTPS or HTTP connections based on the existence of TLS Acceptor
|
// Decide whether to handle HTTPS or HTTP connections based on the existence of TLS Acceptor
|
||||||
if let Some(holder) = tls_acceptor {
|
if let Some(holder) = tls_acceptor {
|
||||||
debug!("TLS handshake start");
|
trace!("TLS handshake start");
|
||||||
let peer_addr = socket
|
let peer_addr = socket
|
||||||
.peer_addr()
|
.peer_addr()
|
||||||
.ok()
|
.ok()
|
||||||
@@ -880,7 +977,7 @@ fn process_connection(
|
|||||||
let acceptor = holder.get();
|
let acceptor = holder.get();
|
||||||
match acceptor.accept(socket).await {
|
match acceptor.accept(socket).await {
|
||||||
Ok(tls_socket) => {
|
Ok(tls_socket) => {
|
||||||
debug!("TLS handshake successful");
|
trace!("TLS handshake successful");
|
||||||
let stream = TokioIo::new(tls_socket);
|
let stream = TokioIo::new(tls_socket);
|
||||||
let conn = http_server.serve_connection(stream, hybrid_service);
|
let conn = http_server.serve_connection(stream, hybrid_service);
|
||||||
if let Err(err) = graceful.watch(conn).await {
|
if let Err(err) = graceful.watch(conn).await {
|
||||||
@@ -890,25 +987,9 @@ fn process_connection(
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
let err_str = err.to_string();
|
let err_str = err.to_string();
|
||||||
let kind = TlsHandshakeFailureKind::classify(&err_str);
|
let kind = TlsHandshakeFailureKind::classify(&err_str);
|
||||||
match kind {
|
log_tls_handshake_failure(&peer_addr, kind, &err);
|
||||||
TlsHandshakeFailureKind::UnexpectedEof => {
|
|
||||||
warn!(peer_addr = %peer_addr, "TLS handshake failed (unexpected EOF). If this client needs HTTP, it should connect to the HTTP port instead");
|
|
||||||
}
|
|
||||||
TlsHandshakeFailureKind::ProtocolVersion => {
|
|
||||||
error!(peer_addr = %peer_addr, "TLS handshake failed (protocol version mismatch): {}", err);
|
|
||||||
}
|
|
||||||
TlsHandshakeFailureKind::Certificate => {
|
|
||||||
error!(peer_addr = %peer_addr, "TLS handshake failed (certificate issue): {}", err);
|
|
||||||
}
|
|
||||||
TlsHandshakeFailureKind::Alert => {
|
|
||||||
error!(peer_addr = %peer_addr, "TLS handshake failed (alert): {}", err);
|
|
||||||
}
|
|
||||||
TlsHandshakeFailureKind::Unknown => {
|
|
||||||
error!(peer_addr = %peer_addr, "TLS handshake failed: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
counter!("rustfs_tls_handshake_failures", &[("failure_type", kind.as_str())]).increment(1);
|
counter!("rustfs_tls_handshake_failures", &[("failure_type", kind.as_str())]).increment(1);
|
||||||
debug!(
|
trace!(
|
||||||
peer_addr = %peer_addr,
|
peer_addr = %peer_addr,
|
||||||
error_type = %std::any::type_name_of_val(&err),
|
error_type = %std::any::type_name_of_val(&err),
|
||||||
error_details = %err,
|
error_details = %err,
|
||||||
@@ -918,61 +999,85 @@ fn process_connection(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug!("TLS handshake success");
|
trace!("TLS handshake success");
|
||||||
} else {
|
} else {
|
||||||
debug!("Http handshake start");
|
trace!("HTTP connection handling start");
|
||||||
let peer_addr = socket.peer_addr().ok().map(|addr| addr.to_string());
|
let peer_addr = socket.peer_addr().ok().map(|addr| addr.to_string());
|
||||||
let stream = TokioIo::new(socket);
|
let stream = TokioIo::new(socket);
|
||||||
let conn = http_server.serve_connection(stream, hybrid_service);
|
let conn = http_server.serve_connection(stream, hybrid_service);
|
||||||
if let Err(err) = graceful.watch(conn).await {
|
if let Err(err) = graceful.watch(conn).await {
|
||||||
handle_connection_error(peer_addr.as_deref(), &*err);
|
handle_connection_error(peer_addr.as_deref(), &*err);
|
||||||
}
|
}
|
||||||
debug!("Http handshake success");
|
trace!("HTTP connection handling finished");
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles connection errors by logging them with appropriate severity
|
/// Handles connection errors by logging them with appropriate severity
|
||||||
fn handle_connection_error(peer_addr: Option<&str>, err: &(dyn std::error::Error + 'static)) {
|
fn handle_connection_error(peer_addr: Option<&str>, err: &(dyn std::error::Error + 'static)) {
|
||||||
|
let peer_addr = peer_addr.unwrap_or("unknown");
|
||||||
let s = err.to_string();
|
let s = err.to_string();
|
||||||
if s.contains("connection reset") || s.contains("broken pipe") {
|
if s.contains("connection reset") || s.contains("broken pipe") {
|
||||||
warn!(
|
log_transport_closed(peer_addr, "connection_reset", &s, "client_disconnect");
|
||||||
peer_addr = %peer_addr.unwrap_or("unknown"),
|
|
||||||
"The connection was reset by the peer or broken pipe: {}", s
|
|
||||||
);
|
|
||||||
// Ignore common non-fatal errors
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
|
if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
|
||||||
if hyper_err.is_incomplete_message() {
|
if hyper_err.is_incomplete_message() {
|
||||||
warn!(
|
log_transport_closed(peer_addr, "incomplete_message", &hyper_err.to_string(), "client_disconnect");
|
||||||
peer_addr = %peer_addr.unwrap_or("unknown"),
|
|
||||||
"The HTTP connection is closed prematurely and the message is not completed:{}", hyper_err
|
|
||||||
);
|
|
||||||
} else if hyper_err.is_closed() {
|
} else if hyper_err.is_closed() {
|
||||||
warn!(peer_addr = %peer_addr.unwrap_or("unknown"), "The HTTP connection is closed:{}", hyper_err);
|
log_transport_closed(peer_addr, "connection_closed", &hyper_err.to_string(), "client_disconnect");
|
||||||
} else if hyper_err.is_parse() {
|
} else if hyper_err.is_parse() {
|
||||||
error!(peer_addr = %peer_addr.unwrap_or("unknown"), "HTTP message parsing failed:{}", hyper_err);
|
log_transport_failed(peer_addr, "parse_failure", &hyper_err.to_string());
|
||||||
} else if hyper_err.is_user() {
|
} else if hyper_err.is_user() {
|
||||||
error!(peer_addr = %peer_addr.unwrap_or("unknown"), "HTTP user-custom error:{}", hyper_err);
|
error!(
|
||||||
|
event = EVENT_HTTP_TRANSPORT_FAILED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TRANSPORT,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
error_kind = "service_error",
|
||||||
|
error = %hyper_err,
|
||||||
|
result = "transport_error",
|
||||||
|
"HTTP transport failed"
|
||||||
|
);
|
||||||
} else if hyper_err.is_canceled() {
|
} else if hyper_err.is_canceled() {
|
||||||
warn!(
|
log_transport_closed(peer_addr, "canceled", &hyper_err.to_string(), "client_disconnect");
|
||||||
peer_addr = %peer_addr.unwrap_or("unknown"),
|
|
||||||
"The HTTP connection is canceled:{}", hyper_err
|
|
||||||
);
|
|
||||||
} else if format!("{:?}", hyper_err).contains("HeaderTimeout") {
|
} else if format!("{:?}", hyper_err).contains("HeaderTimeout") {
|
||||||
info!(
|
log_transport_closed(peer_addr, "header_timeout", &hyper_err.to_string(), "client_timeout");
|
||||||
peer_addr = %peer_addr.unwrap_or("unknown"),
|
|
||||||
"The HTTP connection timed out while reading request headers (HeaderTimeout): {}", hyper_err
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
error!(peer_addr = %peer_addr.unwrap_or("unknown"), "Unknown hyper error:{:?}", hyper_err);
|
error!(
|
||||||
|
event = EVENT_HTTP_TRANSPORT_FAILED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TRANSPORT,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
error_kind = "hyper_error",
|
||||||
|
error = ?hyper_err,
|
||||||
|
result = "transport_error",
|
||||||
|
"HTTP transport failed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if let Some(io_err) = err.downcast_ref::<Error>() {
|
} else if let Some(io_err) = err.downcast_ref::<Error>() {
|
||||||
error!(peer_addr = %peer_addr.unwrap_or("unknown"), "Unknown connection IO error:{}", io_err);
|
error!(
|
||||||
|
event = EVENT_HTTP_TRANSPORT_FAILED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TRANSPORT,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
error_kind = "io_error",
|
||||||
|
error = %io_err,
|
||||||
|
result = "transport_error",
|
||||||
|
"HTTP transport failed"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
error!(peer_addr = %peer_addr.unwrap_or("unknown"), "Unknown connection error type:{:?}", err);
|
error!(
|
||||||
|
event = EVENT_HTTP_TRANSPORT_FAILED,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_TRANSPORT,
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
error_kind = "unknown_error",
|
||||||
|
error = ?err,
|
||||||
|
result = "transport_error",
|
||||||
|
"HTTP transport failed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,8 +1202,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Number of middleware layers in the canonical stack order (see http.rs).
|
/// Number of middleware layers in the canonical stack order (see http.rs).
|
||||||
/// Layers 1-2 are per-connection (AddExtension), 3-15 are stateless.
|
/// Layers 1-2 are per-connection (AddExtension), 3-21 are stateless.
|
||||||
pub const MIDDLEWARE_LAYER_COUNT: usize = 15;
|
pub const MIDDLEWARE_LAYER_COUNT: usize = 21;
|
||||||
|
|
||||||
/// Current HTTP/2 defaults (from rustfs_config).
|
/// Current HTTP/2 defaults (from rustfs_config).
|
||||||
pub const H2_INITIAL_STREAM_WINDOW_SIZE: u32 = DEFAULT_H2_INITIAL_STREAM_WINDOW_SIZE;
|
pub const H2_INITIAL_STREAM_WINDOW_SIZE: u32 = DEFAULT_H2_INITIAL_STREAM_WINDOW_SIZE;
|
||||||
@@ -1139,7 +1244,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_baseline_middleware_count() {
|
fn test_baseline_middleware_count() {
|
||||||
assert_eq!(baseline::MIDDLEWARE_LAYER_COUNT, 15);
|
assert_eq!(baseline::MIDDLEWARE_LAYER_COUNT, 21);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+374
-2
@@ -15,6 +15,7 @@
|
|||||||
use crate::admin::console::is_console_path;
|
use crate::admin::console::is_console_path;
|
||||||
use crate::admin::handlers::health::{HealthProbe, build_health_response_parts};
|
use crate::admin::handlers::health::{HealthProbe, build_health_response_parts};
|
||||||
use crate::error::ApiError;
|
use crate::error::ApiError;
|
||||||
|
use crate::server::RemoteAddr;
|
||||||
use crate::server::cors;
|
use crate::server::cors;
|
||||||
use crate::server::hybrid::HybridBody;
|
use crate::server::hybrid::HybridBody;
|
||||||
use crate::server::{
|
use crate::server::{
|
||||||
@@ -31,6 +32,7 @@ use http_body_util::BodyExt;
|
|||||||
use hyper::body::Incoming;
|
use hyper::body::Incoming;
|
||||||
use opentelemetry::global;
|
use opentelemetry::global;
|
||||||
use opentelemetry::trace::TraceContextExt;
|
use opentelemetry::trace::TraceContextExt;
|
||||||
|
use rustfs_trusted_proxies::ClientInfo;
|
||||||
use rustfs_utils::get_env_opt_str;
|
use rustfs_utils::get_env_opt_str;
|
||||||
use rustfs_utils::http::headers::AMZ_REQUEST_ID;
|
use rustfs_utils::http::headers::AMZ_REQUEST_ID;
|
||||||
use s3s::S3ErrorCode;
|
use s3s::S3ErrorCode;
|
||||||
@@ -40,7 +42,12 @@ use std::sync::Arc;
|
|||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tower::{Layer, Service};
|
use tower::{Layer, Service};
|
||||||
use tracing::debug;
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
const HTTP_REQUEST_COMPLETED_EVENT: &str = "http_request_completed";
|
||||||
|
const HTTP_REQUEST_FAILED_EVENT: &str = "http_request_failed";
|
||||||
|
const LOG_COMPONENT_SERVER: &str = "server";
|
||||||
|
const LOG_SUBSYSTEM_HTTP: &str = "http";
|
||||||
|
|
||||||
/// A carrier that adapts [`HeaderMap`] for OpenTelemetry trace context propagation.
|
/// A carrier that adapts [`HeaderMap`] for OpenTelemetry trace context propagation.
|
||||||
struct HeaderMapCarrier<'a>(&'a HeaderMap);
|
struct HeaderMapCarrier<'a>(&'a HeaderMap);
|
||||||
@@ -152,6 +159,174 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct RequestLoggingLayer;
|
||||||
|
|
||||||
|
impl<S> Layer<S> for RequestLoggingLayer {
|
||||||
|
type Service = RequestLoggingService<S>;
|
||||||
|
|
||||||
|
fn layer(&self, inner: S) -> Self::Service {
|
||||||
|
RequestLoggingService { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RequestLoggingService<S> {
|
||||||
|
inner: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RequestLogContext {
|
||||||
|
request_id: String,
|
||||||
|
trace_id: Option<String>,
|
||||||
|
span_id: Option<String>,
|
||||||
|
peer_addr: String,
|
||||||
|
method: String,
|
||||||
|
uri: String,
|
||||||
|
request_started_at: Option<RequestContext>,
|
||||||
|
fallback_start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestLogContext {
|
||||||
|
fn from_request<B>(req: &HttpRequest<B>) -> Self {
|
||||||
|
let request_context = req.extensions().get::<RequestContext>().cloned();
|
||||||
|
let request_id = request_context
|
||||||
|
.as_ref()
|
||||||
|
.map(|ctx| ctx.request_id.clone())
|
||||||
|
.unwrap_or_else(|| extract_request_id_from_headers(req.headers()));
|
||||||
|
let peer_addr = req
|
||||||
|
.extensions()
|
||||||
|
.get::<ClientInfo>()
|
||||||
|
.map(|info| info.real_ip.to_string())
|
||||||
|
.or_else(|| req.extensions().get::<RemoteAddr>().map(|addr| addr.0.to_string()))
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
request_id,
|
||||||
|
trace_id: request_context.as_ref().and_then(|ctx| ctx.trace_id.clone()),
|
||||||
|
span_id: request_context.as_ref().and_then(|ctx| ctx.span_id.clone()),
|
||||||
|
peer_addr,
|
||||||
|
method: req.method().to_string(),
|
||||||
|
uri: req.uri().to_string(),
|
||||||
|
request_started_at: request_context,
|
||||||
|
fallback_start: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duration_ms(&self) -> u64 {
|
||||||
|
self.request_started_at
|
||||||
|
.as_ref()
|
||||||
|
.map(RequestContext::duration_ms)
|
||||||
|
.unwrap_or_else(|| self.fallback_start.elapsed().as_millis().try_into().unwrap_or(u64::MAX))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn result_label(status: StatusCode) -> &'static str {
|
||||||
|
if status.is_server_error() {
|
||||||
|
"server_error"
|
||||||
|
} else if status.is_client_error() {
|
||||||
|
"client_error"
|
||||||
|
} else if status.is_redirection() {
|
||||||
|
"redirect"
|
||||||
|
} else {
|
||||||
|
"success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_response<ResBody>(&self, response: &Response<ResBody>) {
|
||||||
|
let duration_ms = self.duration_ms();
|
||||||
|
let status = response.status();
|
||||||
|
let status_code = status.as_u16();
|
||||||
|
let result = Self::result_label(status);
|
||||||
|
let trace_id = self.trace_id.as_deref().unwrap_or("unknown");
|
||||||
|
let span_id = self.span_id.as_deref().unwrap_or("unknown");
|
||||||
|
|
||||||
|
if status.is_server_error() {
|
||||||
|
error!(
|
||||||
|
event = HTTP_REQUEST_COMPLETED_EVENT,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_HTTP,
|
||||||
|
request_id = %self.request_id,
|
||||||
|
trace_id = %trace_id,
|
||||||
|
span_id = %span_id,
|
||||||
|
peer_addr = %self.peer_addr,
|
||||||
|
method = %self.method,
|
||||||
|
uri = %self.uri,
|
||||||
|
status_code,
|
||||||
|
duration_ms,
|
||||||
|
result,
|
||||||
|
"HTTP request completed"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
event = HTTP_REQUEST_COMPLETED_EVENT,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_HTTP,
|
||||||
|
request_id = %self.request_id,
|
||||||
|
trace_id = %trace_id,
|
||||||
|
span_id = %span_id,
|
||||||
|
peer_addr = %self.peer_addr,
|
||||||
|
method = %self.method,
|
||||||
|
uri = %self.uri,
|
||||||
|
status_code,
|
||||||
|
duration_ms,
|
||||||
|
result,
|
||||||
|
"HTTP request completed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_failure<E>(&self, error: &E)
|
||||||
|
where
|
||||||
|
E: std::fmt::Display,
|
||||||
|
{
|
||||||
|
error!(
|
||||||
|
event = HTTP_REQUEST_FAILED_EVENT,
|
||||||
|
component = LOG_COMPONENT_SERVER,
|
||||||
|
subsystem = LOG_SUBSYSTEM_HTTP,
|
||||||
|
request_id = %self.request_id,
|
||||||
|
trace_id = %self.trace_id.as_deref().unwrap_or("unknown"),
|
||||||
|
span_id = %self.span_id.as_deref().unwrap_or("unknown"),
|
||||||
|
peer_addr = %self.peer_addr,
|
||||||
|
method = %self.method,
|
||||||
|
uri = %self.uri,
|
||||||
|
duration_ms = self.duration_ms(),
|
||||||
|
result = "service_error",
|
||||||
|
error = %error,
|
||||||
|
"HTTP request failed before a response was produced"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B, ResBody> Service<HttpRequest<B>> for RequestLoggingService<S>
|
||||||
|
where
|
||||||
|
S: Service<HttpRequest<B>, Response = Response<ResBody>> + Clone + Send + 'static,
|
||||||
|
S::Future: Send + 'static,
|
||||||
|
S::Error: std::fmt::Display + Send + 'static,
|
||||||
|
B: Send + 'static,
|
||||||
|
{
|
||||||
|
type Response = Response<ResBody>;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.inner.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, req: HttpRequest<B>) -> Self::Future {
|
||||||
|
let context = RequestLogContext::from_request(&req);
|
||||||
|
let mut inner = self.inner.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let result = inner.call(req).await;
|
||||||
|
match &result {
|
||||||
|
Ok(response) => context.log_response(response),
|
||||||
|
Err(error) => context.log_failure(error),
|
||||||
|
}
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Redirect layer that redirects browser requests to the console
|
/// Redirect layer that redirects browser requests to the console
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RedirectLayer;
|
pub struct RedirectLayer;
|
||||||
@@ -1145,16 +1320,18 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::server::{FAVICON_PATH, LICENSE, VERSION};
|
use crate::server::{FAVICON_PATH, LICENSE, RemoteAddr, VERSION};
|
||||||
use futures::future::{Ready, ready};
|
use futures::future::{Ready, ready};
|
||||||
use http::Request;
|
use http::Request;
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use http_body_util::Full;
|
use http_body_util::Full;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
use std::io::{self, Write};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use temp_env::{async_with_vars, with_var};
|
use temp_env::{async_with_vars, with_var};
|
||||||
|
use tracing_subscriber::{Registry, fmt::MakeWriter, layer::SubscriberExt};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct CaptureService;
|
struct CaptureService;
|
||||||
@@ -2379,4 +2556,199 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(response_headers.get(cors::response::ACCESS_CONTROL_ALLOW_METHODS).unwrap(), "GET");
|
assert_eq!(response_headers.get(cors::response::ACCESS_CONTROL_ALLOW_METHODS).unwrap(), "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct StatusService {
|
||||||
|
status: StatusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusService {
|
||||||
|
fn new(status: StatusCode) -> Self {
|
||||||
|
Self { status }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B> Service<Request<B>> for StatusService {
|
||||||
|
type Response = Response<Full<Bytes>>;
|
||||||
|
type Error = Infallible;
|
||||||
|
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, _req: Request<B>) -> Self::Future {
|
||||||
|
ready(Ok(Response::builder()
|
||||||
|
.status(self.status)
|
||||||
|
.body(Full::from(Bytes::new()))
|
||||||
|
.expect("response")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct SharedWriter {
|
||||||
|
buffer: Arc<Mutex<Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SharedWriterGuard {
|
||||||
|
buffer: Arc<Mutex<Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for SharedWriterGuard {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.buffer.lock().expect("log buffer").extend_from_slice(buf);
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'writer> MakeWriter<'writer> for SharedWriter {
|
||||||
|
type Writer = SharedWriterGuard;
|
||||||
|
|
||||||
|
fn make_writer(&'writer self) -> Self::Writer {
|
||||||
|
SharedWriterGuard {
|
||||||
|
buffer: self.buffer.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_log_context_classifies_statuses() {
|
||||||
|
assert_eq!(RequestLogContext::result_label(StatusCode::OK), "success");
|
||||||
|
assert_eq!(RequestLogContext::result_label(StatusCode::TEMPORARY_REDIRECT), "redirect");
|
||||||
|
assert_eq!(RequestLogContext::result_label(StatusCode::BAD_REQUEST), "client_error");
|
||||||
|
assert_eq!(RequestLogContext::result_label(StatusCode::INTERNAL_SERVER_ERROR), "server_error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_log_context_prefers_request_context_and_remote_addr_extensions() {
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method(Method::PUT)
|
||||||
|
.uri("/bucket/object.txt")
|
||||||
|
.body(())
|
||||||
|
.expect("request");
|
||||||
|
request.extensions_mut().insert(RequestContext {
|
||||||
|
request_id: "req-ctx".to_string(),
|
||||||
|
x_amz_request_id: "amz-ctx".to_string(),
|
||||||
|
trace_id: Some("trace-123".to_string()),
|
||||||
|
span_id: Some("span-456".to_string()),
|
||||||
|
start_time: Instant::now(),
|
||||||
|
});
|
||||||
|
request
|
||||||
|
.extensions_mut()
|
||||||
|
.insert(RemoteAddr("127.0.0.1:9000".parse().expect("socket addr")));
|
||||||
|
|
||||||
|
let context = RequestLogContext::from_request(&request);
|
||||||
|
|
||||||
|
assert_eq!(context.request_id, "req-ctx");
|
||||||
|
assert_eq!(context.trace_id.as_deref(), Some("trace-123"));
|
||||||
|
assert_eq!(context.span_id.as_deref(), Some("span-456"));
|
||||||
|
assert_eq!(context.peer_addr, "127.0.0.1:9000");
|
||||||
|
assert_eq!(context.method, "PUT");
|
||||||
|
assert_eq!(context.uri, "/bucket/object.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_logging_layer_emits_single_completion_event_with_standard_fields() {
|
||||||
|
let writer = SharedWriter::default();
|
||||||
|
let captured = writer.buffer.clone();
|
||||||
|
let subscriber = Registry::default().with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.without_time()
|
||||||
|
.with_target(false)
|
||||||
|
.with_level(false)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_writer(writer),
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
|
let mut service = tower::ServiceBuilder::new()
|
||||||
|
.layer(RequestContextLayer)
|
||||||
|
.layer(RequestLoggingLayer)
|
||||||
|
.service(StatusService::new(StatusCode::OK));
|
||||||
|
|
||||||
|
let mut request: Request<Full<Bytes>> = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri("/bucket/object.txt")
|
||||||
|
.header("x-request-id", "req-123")
|
||||||
|
.body(Full::from(Bytes::new()))
|
||||||
|
.expect("request");
|
||||||
|
request
|
||||||
|
.extensions_mut()
|
||||||
|
.insert(RemoteAddr("127.0.0.1:9000".parse().expect("socket addr")));
|
||||||
|
|
||||||
|
let response = futures::executor::block_on(service.call(request)).expect("response");
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = String::from_utf8(captured.lock().expect("captured logs").clone()).expect("utf8 logs");
|
||||||
|
assert_eq!(output.matches("HTTP request completed").count(), 1, "{output}");
|
||||||
|
assert!(output.contains("event"), "{output}");
|
||||||
|
assert!(output.contains("http_request_completed"), "{output}");
|
||||||
|
assert!(output.contains("component"), "{output}");
|
||||||
|
assert!(output.contains("server"), "{output}");
|
||||||
|
assert!(output.contains("subsystem"), "{output}");
|
||||||
|
assert!(output.contains("http"), "{output}");
|
||||||
|
assert!(output.contains("request_id"), "{output}");
|
||||||
|
assert!(output.contains("req-123"), "{output}");
|
||||||
|
assert!(output.contains("peer_addr"), "{output}");
|
||||||
|
assert!(output.contains("127.0.0.1:9000"), "{output}");
|
||||||
|
assert!(output.contains("method"), "{output}");
|
||||||
|
assert!(output.contains("GET"), "{output}");
|
||||||
|
assert!(output.contains("uri"), "{output}");
|
||||||
|
assert!(output.contains("/bucket/object.txt"), "{output}");
|
||||||
|
assert!(output.contains("status_code"), "{output}");
|
||||||
|
assert!(output.contains("200"), "{output}");
|
||||||
|
assert!(output.contains("result"), "{output}");
|
||||||
|
assert!(output.contains("success"), "{output}");
|
||||||
|
assert!(output.contains("duration_ms"), "{output}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_logging_layer_uses_request_context_trace_fields() {
|
||||||
|
let writer = SharedWriter::default();
|
||||||
|
let captured = writer.buffer.clone();
|
||||||
|
let subscriber = Registry::default().with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.without_time()
|
||||||
|
.with_target(false)
|
||||||
|
.with_level(false)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_writer(writer),
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
|
let mut service = RequestLoggingLayer.layer(StatusService::new(StatusCode::INTERNAL_SERVER_ERROR));
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri("/bucket/object.txt")
|
||||||
|
.body(())
|
||||||
|
.expect("request");
|
||||||
|
request.extensions_mut().insert(RequestContext {
|
||||||
|
request_id: "req-ctx".to_string(),
|
||||||
|
x_amz_request_id: "amz-ctx".to_string(),
|
||||||
|
trace_id: Some("trace-ctx".to_string()),
|
||||||
|
span_id: Some("span-ctx".to_string()),
|
||||||
|
start_time: Instant::now(),
|
||||||
|
});
|
||||||
|
request
|
||||||
|
.extensions_mut()
|
||||||
|
.insert(RemoteAddr("127.0.0.1:9000".parse().expect("socket addr")));
|
||||||
|
|
||||||
|
let response = futures::executor::block_on(service.call(request)).expect("response");
|
||||||
|
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = String::from_utf8(captured.lock().expect("captured logs").clone()).expect("utf8 logs");
|
||||||
|
assert!(output.contains("http_request_completed"), "{output}");
|
||||||
|
assert!(output.contains("req-ctx"), "{output}");
|
||||||
|
assert!(output.contains("trace-ctx"), "{output}");
|
||||||
|
assert!(output.contains("span-ctx"), "{output}");
|
||||||
|
assert!(output.contains("500"), "{output}");
|
||||||
|
assert!(output.contains("server_error"), "{output}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ impl RequestContext {
|
|||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the elapsed request lifetime in whole milliseconds.
|
||||||
|
pub fn duration_ms(&self) -> u64 {
|
||||||
|
self.start_time.elapsed().as_millis().try_into().unwrap_or(u64::MAX)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_trace_context_ids() -> Option<(String, String)> {
|
fn current_trace_context_ids() -> Option<(String, String)> {
|
||||||
@@ -217,6 +222,12 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_request_context_duration_ms_is_non_negative() {
|
||||||
|
let ctx = RequestContext::fallback();
|
||||||
|
assert!(ctx.duration_ms() <= 10);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extract_request_id_from_x_request_id() {
|
fn test_extract_request_id_from_x_request_id() {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user