[model-gateway] add OTEL integration to grpc router (#14671)
This commit is contained in:
@@ -24,9 +24,11 @@ use sgl_model_gateway::{
|
||||
routers::RouterFactory,
|
||||
};
|
||||
use tokio::sync::oneshot;
|
||||
use tonic::metadata::MetadataMap;
|
||||
use tonic_v12::{transport::Server, Request as TonicRequest, Response, Status};
|
||||
use tower::ServiceExt;
|
||||
use tracing::info_span;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestOtelCollector {
|
||||
@@ -131,14 +133,22 @@ async fn test_router_with_tracing() {
|
||||
.enable_trace(&collector_endpoint)
|
||||
.build_unchecked();
|
||||
|
||||
// 4. Initialize the OTLP client
|
||||
let init_result = otel_trace::otel_tracing_init(true, Some(&collector_endpoint));
|
||||
assert!(
|
||||
init_result.is_ok(),
|
||||
"Failed to initialize OTEL: {:?}",
|
||||
init_result.err()
|
||||
);
|
||||
println!("OpenTelemetry initialized successfully");
|
||||
// 4. Initialize the OTLP client (check if already initialized by another test)
|
||||
let otel_initialized_by_this_test = if !otel_trace::is_otel_enabled() {
|
||||
let init_result = otel_trace::otel_tracing_init(true, Some(&collector_endpoint));
|
||||
assert!(
|
||||
init_result.is_ok(),
|
||||
"Failed to initialize OTEL: {:?}",
|
||||
init_result.err()
|
||||
);
|
||||
println!("OpenTelemetry initialized successfully");
|
||||
true
|
||||
} else {
|
||||
println!(
|
||||
"OpenTelemetry already initialized by previous test (spans will go to that collector)"
|
||||
);
|
||||
false
|
||||
};
|
||||
|
||||
let trace_config = TraceConfig {
|
||||
enable_trace: true,
|
||||
@@ -232,14 +242,23 @@ async fn test_router_with_tracing() {
|
||||
let span_count = collector.get_span_count();
|
||||
println!("Total spans received by collector: {}", span_count);
|
||||
|
||||
assert!(
|
||||
span_count == 2,
|
||||
"Expected to receive at least 2 span, but got {}. \
|
||||
This indicates that tracing data is not being exported to the OTLP collector.",
|
||||
span_count
|
||||
);
|
||||
|
||||
println!("Test passed! Collector received {} spans", span_count);
|
||||
// Only assert span count if we initialized OTEL with our own collector
|
||||
// When OTEL was pre-initialized by another test, spans go to that collector instead
|
||||
if otel_initialized_by_this_test {
|
||||
assert!(
|
||||
span_count == 2,
|
||||
"Expected to receive at least 2 span, but got {}. \
|
||||
This indicates that tracing data is not being exported to the OTLP collector.",
|
||||
span_count
|
||||
);
|
||||
println!("Test passed! Collector received {} spans", span_count);
|
||||
} else {
|
||||
println!(
|
||||
"Skipping span count assertion - OTEL was pre-initialized by another test. \
|
||||
Spans went to that collector. Received {} spans on this test's collector.",
|
||||
span_count
|
||||
);
|
||||
}
|
||||
|
||||
// 13. cleanup
|
||||
let _ = shutdown_tx.send(());
|
||||
@@ -247,3 +266,115 @@ async fn test_router_with_tracing() {
|
||||
|
||||
println!("Cleanup completed");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// gRPC Trace Context Injection Tests
|
||||
// ============================================================================
|
||||
|
||||
/// Comprehensive test for gRPC trace context injection.
|
||||
///
|
||||
/// This test validates:
|
||||
/// 1. W3C trace context headers are properly injected into gRPC metadata
|
||||
/// 2. traceparent format is correct (version-traceid-spanid-flags)
|
||||
/// 3. All metadata keys are lowercase (gRPC requirement)
|
||||
///
|
||||
/// Note: This test handles the case where OTEL may already be initialized
|
||||
/// by a previous test (since tests run sequentially with #[serial]).
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_grpc_trace_context_injection() {
|
||||
// 1. Start the OTLP collector (needed even if OTEL is already initialized,
|
||||
// as a target for any spans that might be exported)
|
||||
let port = pick_unused_port().expect("Failed to pick unused port");
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let _collector = start_collector(port, shutdown_rx)
|
||||
.await
|
||||
.expect("Failed to start collector");
|
||||
let collector_endpoint = format!("0.0.0.0:{}", port);
|
||||
|
||||
// 2. Initialize OTEL if not already enabled
|
||||
// Note: otel_tracing_init will fail if already initialized (OnceLock),
|
||||
// but that's fine - we just need OTEL to be enabled
|
||||
let already_enabled = otel_trace::is_otel_enabled();
|
||||
if !already_enabled {
|
||||
let init_result = otel_trace::otel_tracing_init(true, Some(&collector_endpoint));
|
||||
assert!(
|
||||
init_result.is_ok(),
|
||||
"Failed to initialize OTEL: {:?}",
|
||||
init_result.err()
|
||||
);
|
||||
}
|
||||
|
||||
// Verify OTEL is enabled (either from this test or a previous one)
|
||||
assert!(otel_trace::is_otel_enabled(), "OTEL should be enabled");
|
||||
|
||||
// 3. Set up tracing subscriber with OTEL layer
|
||||
let otel_layer = otel_trace::get_otel_layer().expect("Failed to get OTEL layer");
|
||||
let subscriber = tracing_subscriber::registry().with(otel_layer);
|
||||
|
||||
// 4. Test within a span context
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
// Create a span that will be exported to OTEL
|
||||
let span = info_span!(target: "sgl_model_gateway::otel-trace", "test_grpc_span");
|
||||
let _guard = span.enter();
|
||||
|
||||
// Create empty gRPC metadata
|
||||
let mut metadata = MetadataMap::new();
|
||||
|
||||
// Inject trace context
|
||||
otel_trace::inject_trace_context_grpc(&mut metadata);
|
||||
|
||||
// === Test 1: Verify traceparent header was injected ===
|
||||
let traceparent = metadata.get("traceparent");
|
||||
assert!(
|
||||
traceparent.is_some(),
|
||||
"traceparent header should be present in gRPC metadata"
|
||||
);
|
||||
|
||||
// === Test 2: Verify traceparent format (version-traceid-spanid-flags) ===
|
||||
let traceparent_value = traceparent.unwrap().to_str().unwrap();
|
||||
let parts: Vec<&str> = traceparent_value.split('-').collect();
|
||||
assert_eq!(
|
||||
parts.len(),
|
||||
4,
|
||||
"traceparent should have 4 parts: version-traceid-spanid-flags"
|
||||
);
|
||||
assert_eq!(parts[0], "00", "traceparent version should be 00");
|
||||
assert_eq!(parts[1].len(), 32, "trace ID should be 32 hex characters");
|
||||
assert_eq!(parts[2].len(), 16, "span ID should be 16 hex characters");
|
||||
|
||||
println!("Successfully injected traceparent: {}", traceparent_value);
|
||||
|
||||
// === Test 3: Verify all keys are lowercase (gRPC metadata requirement) ===
|
||||
for key_and_value in metadata.iter() {
|
||||
match key_and_value {
|
||||
tonic::metadata::KeyAndValueRef::Ascii(key, _) => {
|
||||
let key_str = key.as_str();
|
||||
assert_eq!(
|
||||
key_str,
|
||||
key_str.to_lowercase(),
|
||||
"gRPC metadata key '{}' should be lowercase",
|
||||
key_str
|
||||
);
|
||||
}
|
||||
tonic::metadata::KeyAndValueRef::Binary(key, _) => {
|
||||
let key_str = key.as_str();
|
||||
assert_eq!(
|
||||
key_str,
|
||||
key_str.to_lowercase(),
|
||||
"gRPC metadata key '{}' should be lowercase",
|
||||
key_str
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("All gRPC metadata keys are lowercase as required");
|
||||
});
|
||||
|
||||
// Cleanup - don't shutdown OTEL since tests share global state (OnceLock)
|
||||
// and other tests may need to use the already-initialized OTEL
|
||||
let _ = shutdown_tx.send(());
|
||||
|
||||
println!("test_grpc_trace_context_injection: All assertions passed!");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user