164 lines
5.8 KiB
Rust
164 lines
5.8 KiB
Rust
//! Integration tests verifying that the API router's TraceLayer emits
|
|
//! per-request events at DEBUG (not INFO), so they are suppressed under the
|
|
//! default `tower_http=info` filter while still surfacing under
|
|
//! `RUST_LOG=tower_http=debug`.
|
|
|
|
#![cfg(feature = "api")]
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
};
|
|
use tower::ServiceExt;
|
|
use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _};
|
|
|
|
use kreuzberg::{ExtractionConfig, api::create_router};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Captured-event layer
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A tracing `Layer` that records the target + level of every emitted event.
|
|
#[derive(Clone, Default)]
|
|
struct EventCapture {
|
|
events: Arc<Mutex<Vec<CapturedEvent>>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct CapturedEvent {
|
|
target: String,
|
|
}
|
|
|
|
impl<S> tracing_subscriber::Layer<S> for EventCapture
|
|
where
|
|
S: tracing::Subscriber,
|
|
{
|
|
fn on_event(&self, event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
|
|
let meta = event.metadata();
|
|
self.events.lock().unwrap().push(CapturedEvent {
|
|
target: meta.target().to_owned(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn captured_events(capture: &EventCapture) -> Vec<CapturedEvent> {
|
|
capture.events.lock().unwrap().clone()
|
|
}
|
|
|
|
/// Drive a GET /health request through the router.
|
|
async fn get_health(router: axum::Router) -> StatusCode {
|
|
let response = router
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/health")
|
|
.body(Body::empty())
|
|
.expect("Failed to build /health request"),
|
|
)
|
|
.await
|
|
.expect("Failed to send /health request");
|
|
response.status()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// At default subscriber level (INFO), the TraceLayer must not emit any
|
|
/// per-request or per-response events from `tower_http::trace`.
|
|
///
|
|
/// The configured TraceLayer sets per-request events to DEBUG; the subscriber
|
|
/// here uses `tower_http=info` so those DEBUG events are filtered out.
|
|
///
|
|
/// Uses a plain `#[test]` (not `#[tokio::test]`) so we can wrap the runtime
|
|
/// construction inside `tracing::subscriber::with_default` without nesting
|
|
/// a second runtime on top of the tokio test runtime.
|
|
#[test]
|
|
fn tower_http_trace_events_suppressed_at_info() {
|
|
let capture = EventCapture::default();
|
|
let capture_clone = capture.clone();
|
|
|
|
// Build a subscriber with tower_http capped at info — mirrors the default
|
|
// kreuzberg-cli filter (tower_http=info).
|
|
let filter = EnvFilter::new("info,tower_http=info");
|
|
let subscriber = tracing_subscriber::registry().with(filter).with(capture_clone);
|
|
|
|
// with_default sets the subscriber for the closure's duration. We build and
|
|
// drive the runtime *inside* so all tracing spans are recorded by `subscriber`.
|
|
let status = tracing::subscriber::with_default(subscriber, || {
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let router = create_router(ExtractionConfig::default());
|
|
get_health(router).await
|
|
})
|
|
});
|
|
|
|
assert_eq!(status, StatusCode::OK, "/health must return 200");
|
|
|
|
// No events from tower_http::trace should have been captured.
|
|
let events = captured_events(&capture);
|
|
let tower_trace_events: Vec<_> = events
|
|
.iter()
|
|
.filter(|e| e.target.starts_with("tower_http::trace"))
|
|
.collect();
|
|
assert!(
|
|
tower_trace_events.is_empty(),
|
|
"expected zero tower_http::trace events at info filter; got: {tower_trace_events:?}"
|
|
);
|
|
}
|
|
|
|
/// At DEBUG level for tower_http, per-request events from `tower_http::trace`
|
|
/// DO appear — confirming we suppressed by filter config, not by removing the layer.
|
|
///
|
|
/// Uses a plain `#[test]` for the same reason as `tower_http_trace_events_suppressed_at_info`.
|
|
#[test]
|
|
fn tower_http_trace_events_visible_at_debug() {
|
|
let capture = EventCapture::default();
|
|
let capture_clone = capture.clone();
|
|
|
|
// Explicitly enable tower_http at DEBUG.
|
|
let filter = EnvFilter::new("info,tower_http=debug");
|
|
let subscriber = tracing_subscriber::registry().with(filter).with(capture_clone);
|
|
|
|
let status = tracing::subscriber::with_default(subscriber, || {
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let router = create_router(ExtractionConfig::default());
|
|
get_health(router).await
|
|
})
|
|
});
|
|
|
|
assert_eq!(status, StatusCode::OK, "/health must return 200");
|
|
|
|
// At least one event from tower_http::trace must be present.
|
|
let events = captured_events(&capture);
|
|
let tower_trace_events: Vec<_> = events.iter().filter(|e| e.target.starts_with("tower_http")).collect();
|
|
assert!(
|
|
!tower_trace_events.is_empty(),
|
|
"expected at least one tower_http event at debug filter; got none (total events: {:?})",
|
|
events
|
|
);
|
|
}
|
|
|
|
/// The /health route must respond correctly regardless of logging configuration.
|
|
///
|
|
/// Uses `#[tokio::test]` since it does not install a custom subscriber.
|
|
#[tokio::test]
|
|
async fn health_route_works_without_subscriber() {
|
|
let router = create_router(ExtractionConfig::default());
|
|
let status = get_health(router).await;
|
|
assert_eq!(status, StatusCode::OK);
|
|
}
|