feat(http): improve error logging with client IP (#7503)

* feat(http): improve error logging with client IP

- Add logging to ErrorResponse::from_error_message()
- Add middleware to log HTTP errors with client IP

Closes #7328

Signed-off-by: maximk777 <maximkirienkov777@gmail.com>

* fix(http): address review comments for error logging

Restore rich Debug logging in from_error(), add URI/method/matched path
to client IP middleware, and only log when client address is available.

Signed-off-by: evenyag <realevenyag@gmail.com>

---------

Signed-off-by: maximk777 <maximkirienkov777@gmail.com>
Signed-off-by: evenyag <realevenyag@gmail.com>
Co-authored-by: evenyag <realevenyag@gmail.com>
This commit is contained in:
maximk777
2026-03-16 12:10:33 +05:00
committed by GitHub
parent c6f1ef8aec
commit b007f85986
3 changed files with 125 additions and 5 deletions

View File

@@ -112,8 +112,8 @@ pub mod utils;
use result::HttpOutputWriter;
pub(crate) use timeout::DynamicTimeoutLayer;
mod client_ip;
use crate::prom_remote_write::validation::PromValidationMode;
mod hints;
mod read_preference;
#[cfg(any(test, feature = "testing"))]
@@ -883,6 +883,7 @@ impl HttpServer {
authorize::check_http_auth,
))
.layer(middleware::from_fn(hints::extract_hints))
.layer(middleware::from_fn(client_ip::log_error_with_client_ip))
.layer(middleware::from_fn(
read_preference::extract_read_preference,
)),
@@ -1247,7 +1248,10 @@ impl Server for HttpServer {
error!(e; "Failed to set TCP_NODELAY on incoming connection");
}
});
let serve = axum::serve(listener, app.into_make_service());
let serve = axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
);
// FIXME(yingwen): Support keepalive.
// See:

View File

@@ -0,0 +1,109 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::net::SocketAddr;
use axum::body::Body;
use axum::extract::{ConnectInfo, MatchedPath};
use axum::http::Request;
use axum::middleware::Next;
use axum::response::Response;
use common_telemetry::warn;
/// Middleware that logs HTTP error responses (4xx/5xx) with client IP address.
///
/// Extracts client address from [`ConnectInfo`] if available.
pub async fn log_error_with_client_ip(req: Request<Body>, next: Next) -> Response {
let request_info = req
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|c| c.0)
.map(|addr| {
let method = req.method().clone();
let uri = req.uri().clone();
let matched_path = req.extensions().get::<MatchedPath>().cloned();
(addr, method, uri, matched_path)
});
let response = next.run(req).await;
if (response.status().is_client_error() || response.status().is_server_error())
&& let Some((addr, method, uri, matched_path)) = request_info
{
warn!(
"HTTP error response {} for {} {} (matched: {}) from client {}",
response.status(),
method,
uri,
matched_path
.as_ref()
.map(|p| p.as_str())
.unwrap_or("<unknown>"),
addr
);
}
response
}
#[cfg(test)]
mod tests {
use axum::Router;
use axum::routing::get;
use http::StatusCode;
use tower::ServiceExt;
use super::*;
#[tokio::test]
async fn test_middleware_passes_error_response() {
async fn not_found_handler() -> StatusCode {
StatusCode::NOT_FOUND
}
let app = Router::new()
.route("/not-found", get(not_found_handler))
.layer(axum::middleware::from_fn(log_error_with_client_ip));
let response = app
.oneshot(
Request::builder()
.uri("/not-found")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_middleware_passes_success_response() {
async fn ok_handler() -> StatusCode {
StatusCode::OK
}
let app = Router::new()
.route("/ok", get(ok_handler))
.layer(axum::middleware::from_fn(log_error_with_client_ip));
let response = app
.oneshot(Request::builder().uri("/ok").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}

View File

@@ -32,17 +32,24 @@ pub struct ErrorResponse {
impl ErrorResponse {
pub fn from_error(error: impl ErrorExt) -> Self {
let code = error.status_code();
if code.should_log_error() {
error!(error; "Failed to handle HTTP request");
} else {
debug!("Failed to handle HTTP request, err: {:?}", error);
}
Self::from_error_message(code, error.output_msg())
ErrorResponse {
code: code as u32,
error: error.output_msg(),
execution_time_ms: 0,
}
}
pub fn from_error_message(code: StatusCode, msg: String) -> Self {
if code.should_log_error() {
error!("Failed to handle HTTP request: {}", msg);
} else {
debug!("Failed to handle HTTP request: {}", msg);
}
ErrorResponse {
code: code as u32,
error: msg,