[model-gateway][tracing]: implement request tracing using OpenTelemetry with trace context propagation (HTTP) (#13897)
This commit is contained in:
@@ -438,6 +438,14 @@ python -m sglang_router.launch_router \
|
|||||||
--request-id-headers x-request-id x-trace-id
|
--request-id-headers x-request-id x-trace-id
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Enable opentelmetry tracing:
|
||||||
|
```bash
|
||||||
|
python -m sglang_router.launch_router \
|
||||||
|
--worker-urls http://worker1:8000 \
|
||||||
|
--enable-trace \
|
||||||
|
--otlp-traces-endpoint 0.0.0.0:4317
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ tracing = "0.1"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "chrono"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "chrono"] }
|
||||||
tracing-log = "0.2"
|
tracing-log = "0.2"
|
||||||
tracing-appender = "0.2.3"
|
tracing-appender = "0.2.3"
|
||||||
|
opentelemetry = "0.27"
|
||||||
|
opentelemetry_sdk = { version = "0.27", features = ["trace", "rt-tokio"] }
|
||||||
|
opentelemetry-otlp = { version = "0.27", features = ["trace", "grpc-tonic"] }
|
||||||
|
tracing-opentelemetry = "0.28"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
kube = { version = "1.1.0", features = ["runtime", "derive"] }
|
kube = { version = "1.1.0", features = ["runtime", "derive"] }
|
||||||
k8s-openapi = { version = "0.25.0", features = ["v1_33"] }
|
k8s-openapi = { version = "0.25.0", features = ["v1_33"] }
|
||||||
@@ -131,6 +135,9 @@ tempfile = "3.8"
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
wasm-encoder = "0.242"
|
wasm-encoder = "0.242"
|
||||||
npyz = { version = "0.8", features = ["npz"] } # For reading numpy .npz files in golden tests
|
npyz = { version = "0.8", features = ["npz"] } # For reading numpy .npz files in golden tests
|
||||||
|
opentelemetry-proto = { version = "0.27", features = ["gen-tonic"] }
|
||||||
|
tonic-v12 = { version = "0.12.3", package = "tonic" }
|
||||||
|
serial_test = "3.0"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "request_processing"
|
name = "request_processing"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ High-performance model routing control and data plane for large-scale LLM deploy
|
|||||||
- Multi-model inference gateway mode (`--enable-igw`) that runs several routers at once and applies per-model policies.
|
- Multi-model inference gateway mode (`--enable-igw`) that runs several routers at once and applies per-model policies.
|
||||||
- Conversation, response, and chat-history connectors that centralize state at the router, enabling compliant sharing across models/MCP loops with in-memory, no-op, or Oracle ATP storage options.
|
- Conversation, response, and chat-history connectors that centralize state at the router, enabling compliant sharing across models/MCP loops with in-memory, no-op, or Oracle ATP storage options.
|
||||||
- Built-in reliability primitives: retries with exponential backoff, circuit breakers, token-bucket rate limiting, and queuing.
|
- Built-in reliability primitives: retries with exponential backoff, circuit breakers, token-bucket rate limiting, and queuing.
|
||||||
- First-class observability with structured logging and Prometheus metrics.
|
- First-class observability with structured logging, OpenTelemetry trace and Prometheus metrics.
|
||||||
|
|
||||||
### Architecture at a Glance
|
### Architecture at a Glance
|
||||||
**Control Plane**
|
**Control Plane**
|
||||||
@@ -562,6 +562,7 @@ Only one of `--oracle-dsn` or `--oracle-tns-alias` should be supplied.
|
|||||||
- **Prometheus Metrics**: Enable with `--prometheus-host`/`--prometheus-port` (defaults to `0.0.0.0:29000`). Metrics cover request latency, retry behavior, circuit breaker states, worker health/load, queue depth, PD pipeline stats, tokenizer timings, and MCP activity.
|
- **Prometheus Metrics**: Enable with `--prometheus-host`/`--prometheus-port` (defaults to `0.0.0.0:29000`). Metrics cover request latency, retry behavior, circuit breaker states, worker health/load, queue depth, PD pipeline stats, tokenizer timings, and MCP activity.
|
||||||
- **Request IDs**: Configurable headers via `--request-id-headers`; responses include `x-request-id`.
|
- **Request IDs**: Configurable headers via `--request-id-headers`; responses include `x-request-id`.
|
||||||
- **CORS**: Set `--cors-allowed-origins` for browser access.
|
- **CORS**: Set `--cors-allowed-origins` for browser access.
|
||||||
|
- **Request Tracing via OpenTelemetry**: Enable with `--enable-trace` and set opentelemetry collector endpoint with `--otlp-traces-endpoint <ip>:<port>`.
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
|||||||
@@ -41,10 +41,6 @@ def launch_router(args: argparse.Namespace) -> Optional[Router]:
|
|||||||
mini_lb = MiniLoadBalancer(router_args)
|
mini_lb = MiniLoadBalancer(router_args)
|
||||||
mini_lb.start()
|
mini_lb.start()
|
||||||
else:
|
else:
|
||||||
# TODO: support tracing for router(Rust).
|
|
||||||
del router_args.enable_trace
|
|
||||||
del router_args.otlp_traces_endpoint
|
|
||||||
|
|
||||||
if Router is None:
|
if Router is None:
|
||||||
raise RuntimeError("Rust Router is not installed")
|
raise RuntimeError("Rust Router is not installed")
|
||||||
router_args._validate_router_args()
|
router_args._validate_router_args()
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ struct Router {
|
|||||||
client_cert_path: Option<String>,
|
client_cert_path: Option<String>,
|
||||||
client_key_path: Option<String>,
|
client_key_path: Option<String>,
|
||||||
ca_cert_paths: Vec<String>,
|
ca_cert_paths: Vec<String>,
|
||||||
|
enable_trace: bool,
|
||||||
|
otlp_traces_endpoint: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Router {
|
impl Router {
|
||||||
@@ -309,6 +311,11 @@ impl Router {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let trace_config = Some(config::TraceConfig {
|
||||||
|
enable_trace: self.enable_trace,
|
||||||
|
otlp_traces_endpoint: self.otlp_traces_endpoint.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
let history_backend = match self.history_backend {
|
let history_backend = match self.history_backend {
|
||||||
HistoryBackendType::Memory => config::HistoryBackend::Memory,
|
HistoryBackendType::Memory => config::HistoryBackend::Memory,
|
||||||
HistoryBackendType::None => config::HistoryBackend::None,
|
HistoryBackendType::None => config::HistoryBackend::None,
|
||||||
@@ -376,6 +383,7 @@ impl Router {
|
|||||||
.maybe_api_key(self.api_key.as_ref())
|
.maybe_api_key(self.api_key.as_ref())
|
||||||
.maybe_discovery(discovery)
|
.maybe_discovery(discovery)
|
||||||
.maybe_metrics(metrics)
|
.maybe_metrics(metrics)
|
||||||
|
.maybe_trace(trace_config)
|
||||||
.maybe_log_dir(self.log_dir.as_ref())
|
.maybe_log_dir(self.log_dir.as_ref())
|
||||||
.maybe_log_level(self.log_level.as_ref())
|
.maybe_log_level(self.log_level.as_ref())
|
||||||
.maybe_request_id_headers(self.request_id_headers.clone())
|
.maybe_request_id_headers(self.request_id_headers.clone())
|
||||||
@@ -477,6 +485,8 @@ impl Router {
|
|||||||
client_cert_path = None,
|
client_cert_path = None,
|
||||||
client_key_path = None,
|
client_key_path = None,
|
||||||
ca_cert_paths = vec![],
|
ca_cert_paths = vec![],
|
||||||
|
enable_trace = false,
|
||||||
|
otlp_traces_endpoint = String::from("localhost:4317"),
|
||||||
))]
|
))]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn new(
|
fn new(
|
||||||
@@ -552,6 +562,8 @@ impl Router {
|
|||||||
client_cert_path: Option<String>,
|
client_cert_path: Option<String>,
|
||||||
client_key_path: Option<String>,
|
client_key_path: Option<String>,
|
||||||
ca_cert_paths: Vec<String>,
|
ca_cert_paths: Vec<String>,
|
||||||
|
enable_trace: bool,
|
||||||
|
otlp_traces_endpoint: String,
|
||||||
) -> PyResult<Self> {
|
) -> PyResult<Self> {
|
||||||
let mut all_urls = worker_urls.clone();
|
let mut all_urls = worker_urls.clone();
|
||||||
|
|
||||||
@@ -641,6 +653,8 @@ impl Router {
|
|||||||
client_cert_path,
|
client_cert_path,
|
||||||
client_key_path,
|
client_key_path,
|
||||||
ca_cert_paths,
|
ca_cert_paths,
|
||||||
|
enable_trace,
|
||||||
|
otlp_traces_endpoint,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::{
|
use super::{
|
||||||
CircuitBreakerConfig, ConfigError, ConfigResult, DiscoveryConfig, HealthCheckConfig,
|
CircuitBreakerConfig, ConfigError, ConfigResult, DiscoveryConfig, HealthCheckConfig,
|
||||||
HistoryBackend, MetricsConfig, OracleConfig, PolicyConfig, PostgresConfig, RetryConfig,
|
HistoryBackend, MetricsConfig, OracleConfig, PolicyConfig, PostgresConfig, RetryConfig,
|
||||||
RouterConfig, RoutingMode, TokenizerCacheConfig,
|
RouterConfig, RoutingMode, TokenizerCacheConfig, TraceConfig,
|
||||||
};
|
};
|
||||||
use crate::{core::ConnectionMode, mcp::McpConfig};
|
use crate::{core::ConnectionMode, mcp::McpConfig};
|
||||||
|
|
||||||
@@ -297,6 +297,24 @@ impl RouterConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== Otel Trace ====================
|
||||||
|
|
||||||
|
pub fn enable_trace<S: Into<String>>(mut self, endpoint: S) -> Self {
|
||||||
|
self.config.trace_config = Some(TraceConfig {
|
||||||
|
enable_trace: true,
|
||||||
|
otlp_traces_endpoint: endpoint.into(),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable_trace(mut self) -> Self {
|
||||||
|
self.config.trace_config = Some(TraceConfig {
|
||||||
|
enable_trace: false,
|
||||||
|
otlp_traces_endpoint: "".to_string(),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Logging ====================
|
// ==================== Logging ====================
|
||||||
|
|
||||||
pub fn log_dir<S: Into<String>>(mut self, dir: S) -> Self {
|
pub fn log_dir<S: Into<String>>(mut self, dir: S) -> Self {
|
||||||
@@ -461,6 +479,11 @@ impl RouterConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn maybe_trace(mut self, trace_config: Option<TraceConfig>) -> Self {
|
||||||
|
self.config.trace_config = trace_config;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn maybe_log_dir(mut self, dir: Option<impl Into<String>>) -> Self {
|
pub fn maybe_log_dir(mut self, dir: Option<impl Into<String>>) -> Self {
|
||||||
self.config.log_dir = dir.map(|d| d.into());
|
self.config.log_dir = dir.map(|d| d.into());
|
||||||
self
|
self
|
||||||
@@ -703,11 +726,13 @@ mod tests {
|
|||||||
.to_builder()
|
.to_builder()
|
||||||
.port(4000)
|
.port(4000)
|
||||||
.enable_metrics("0.0.0.0", 29000)
|
.enable_metrics("0.0.0.0", 29000)
|
||||||
|
.enable_trace("localhost:4317")
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(modified.port, 4000);
|
assert_eq!(modified.port, 4000);
|
||||||
assert!(modified.metrics.is_some());
|
assert!(modified.metrics.is_some());
|
||||||
|
assert!(modified.trace_config.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test complex routing mode helper method
|
/// Test complex routing mode helper method
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub struct RouterConfig {
|
|||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
pub discovery: Option<DiscoveryConfig>,
|
pub discovery: Option<DiscoveryConfig>,
|
||||||
pub metrics: Option<MetricsConfig>,
|
pub metrics: Option<MetricsConfig>,
|
||||||
|
pub trace_config: Option<TraceConfig>,
|
||||||
pub log_dir: Option<String>,
|
pub log_dir: Option<String>,
|
||||||
pub log_level: Option<String>,
|
pub log_level: Option<String>,
|
||||||
pub request_id_headers: Option<Vec<String>>,
|
pub request_id_headers: Option<Vec<String>>,
|
||||||
@@ -461,6 +462,21 @@ impl Default for MetricsConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TraceConfig {
|
||||||
|
pub enable_trace: bool,
|
||||||
|
pub otlp_traces_endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TraceConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enable_trace: false,
|
||||||
|
otlp_traces_endpoint: "localhost:4317".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for RouterConfig {
|
impl Default for RouterConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -478,6 +494,7 @@ impl Default for RouterConfig {
|
|||||||
api_key: None,
|
api_key: None,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
metrics: None,
|
metrics: None,
|
||||||
|
trace_config: None,
|
||||||
log_dir: None,
|
log_dir: None,
|
||||||
log_level: None,
|
log_level: None,
|
||||||
request_id_headers: None,
|
request_id_headers: None,
|
||||||
@@ -544,6 +561,14 @@ impl RouterConfig {
|
|||||||
self.metrics.is_some()
|
self.metrics.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if tracing is enabled
|
||||||
|
pub fn has_tracing(&self) -> bool {
|
||||||
|
match &self.trace_config {
|
||||||
|
Some(trace_config) => trace_config.enable_trace,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Compute the effective retry config considering disable flag
|
/// Compute the effective retry config considering disable flag
|
||||||
pub fn effective_retry_config(&self) -> RetryConfig {
|
pub fn effective_retry_config(&self) -> RetryConfig {
|
||||||
let mut cfg = self.retry.clone();
|
let mut cfg = self.retry.clone();
|
||||||
@@ -588,6 +613,7 @@ mod tests {
|
|||||||
assert_eq!(config.worker_startup_check_interval_secs, 30);
|
assert_eq!(config.worker_startup_check_interval_secs, 30);
|
||||||
assert!(config.discovery.is_none());
|
assert!(config.discovery.is_none());
|
||||||
assert!(config.metrics.is_none());
|
assert!(config.metrics.is_none());
|
||||||
|
assert!(config.trace_config.is_none());
|
||||||
assert!(config.log_dir.is_none());
|
assert!(config.log_dir.is_none());
|
||||||
assert!(config.log_level.is_none());
|
assert!(config.log_level.is_none());
|
||||||
}
|
}
|
||||||
@@ -636,6 +662,7 @@ mod tests {
|
|||||||
assert_eq!(config.log_level, deserialized.log_level);
|
assert_eq!(config.log_level, deserialized.log_level);
|
||||||
assert!(deserialized.discovery.is_none());
|
assert!(deserialized.discovery.is_none());
|
||||||
assert!(deserialized.metrics.is_none());
|
assert!(deserialized.metrics.is_none());
|
||||||
|
assert!(deserialized.trace_config.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -891,6 +918,25 @@ mod tests {
|
|||||||
assert_eq!(config.host, "0.0.0.0");
|
assert_eq!(config.host, "0.0.0.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trace_config_default() {
|
||||||
|
let config = TraceConfig::default();
|
||||||
|
|
||||||
|
assert!(!config.enable_trace);
|
||||||
|
assert_eq!(config.otlp_traces_endpoint, "localhost:4317");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trace_config_custom() {
|
||||||
|
let config = TraceConfig {
|
||||||
|
enable_trace: true,
|
||||||
|
otlp_traces_endpoint: "otel-collector:4317".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(config.enable_trace);
|
||||||
|
assert_eq!(config.otlp_traces_endpoint, "otel-collector:4317");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mode_type() {
|
fn test_mode_type() {
|
||||||
let config = RouterConfig::builder()
|
let config = RouterConfig::builder()
|
||||||
@@ -932,6 +978,17 @@ mod tests {
|
|||||||
assert!(config.has_metrics());
|
assert!(config.has_metrics());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_has_tracing() {
|
||||||
|
let config = RouterConfig::default();
|
||||||
|
assert!(!config.has_tracing());
|
||||||
|
|
||||||
|
let config = RouterConfig::builder()
|
||||||
|
.enable_trace("localhost:4317")
|
||||||
|
.build_unchecked();
|
||||||
|
assert!(config.has_tracing());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_large_worker_lists() {
|
fn test_large_worker_lists() {
|
||||||
let large_urls: Vec<String> = (0..1000).map(|i| format!("http://worker{}", i)).collect();
|
let large_urls: Vec<String> = (0..1000).map(|i| format!("http://worker{}", i)).collect();
|
||||||
@@ -1016,6 +1073,7 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.enable_metrics("0.0.0.0", 9090)
|
.enable_metrics("0.0.0.0", 9090)
|
||||||
|
.enable_trace("localhost:4317")
|
||||||
.log_dir("/var/log/sglang")
|
.log_dir("/var/log/sglang")
|
||||||
.log_level("info")
|
.log_level("info")
|
||||||
.max_concurrent_requests(64)
|
.max_concurrent_requests(64)
|
||||||
@@ -1026,6 +1084,7 @@ mod tests {
|
|||||||
assert_eq!(config.policy.name(), "power_of_two");
|
assert_eq!(config.policy.name(), "power_of_two");
|
||||||
assert!(config.has_service_discovery());
|
assert!(config.has_service_discovery());
|
||||||
assert!(config.has_metrics());
|
assert!(config.has_metrics());
|
||||||
|
assert!(config.has_tracing());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1055,6 +1114,7 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.metrics_config(MetricsConfig::default())
|
.metrics_config(MetricsConfig::default())
|
||||||
|
.enable_trace("localhost:4317")
|
||||||
.log_level("debug")
|
.log_level("debug")
|
||||||
.max_concurrent_requests(64)
|
.max_concurrent_requests(64)
|
||||||
.build_unchecked();
|
.build_unchecked();
|
||||||
@@ -1064,6 +1124,7 @@ mod tests {
|
|||||||
assert_eq!(config.policy.name(), "cache_aware");
|
assert_eq!(config.policy.name(), "cache_aware");
|
||||||
assert!(config.has_service_discovery());
|
assert!(config.has_service_discovery());
|
||||||
assert!(config.has_metrics());
|
assert!(config.has_metrics());
|
||||||
|
assert!(config.has_tracing());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1092,6 +1153,7 @@ mod tests {
|
|||||||
bootstrap_port_annotation: "mycompany.io/bootstrap".to_string(),
|
bootstrap_port_annotation: "mycompany.io/bootstrap".to_string(),
|
||||||
})
|
})
|
||||||
.enable_metrics("::", 9999) // IPv6 any
|
.enable_metrics("::", 9999) // IPv6 any
|
||||||
|
.enable_trace("localhost:4317")
|
||||||
.log_dir("/opt/logs/sglang")
|
.log_dir("/opt/logs/sglang")
|
||||||
.log_level("trace")
|
.log_level("trace")
|
||||||
.max_concurrent_requests(64)
|
.max_concurrent_requests(64)
|
||||||
@@ -1099,6 +1161,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(config.has_service_discovery());
|
assert!(config.has_service_discovery());
|
||||||
assert!(config.has_metrics());
|
assert!(config.has_metrics());
|
||||||
|
assert!(config.has_tracing());
|
||||||
assert_eq!(config.mode_type(), "regular");
|
assert_eq!(config.mode_type(), "regular");
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&config).unwrap();
|
let json = serde_json::to_string_pretty(&config).unwrap();
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ impl ConfigValidator {
|
|||||||
Self::validate_metrics(metrics)?;
|
Self::validate_metrics(metrics)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(trace_config) = &config.trace_config {
|
||||||
|
Self::validate_trace(trace_config)?;
|
||||||
|
}
|
||||||
|
|
||||||
Self::validate_compatibility(config)?;
|
Self::validate_compatibility(config)?;
|
||||||
|
|
||||||
let retry_cfg = config.effective_retry_config();
|
let retry_cfg = config.effective_retry_config();
|
||||||
@@ -357,6 +361,46 @@ impl ConfigValidator {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_trace(trace_config: &TraceConfig) -> ConfigResult<()> {
|
||||||
|
if !trace_config.enable_trace {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint = &trace_config.otlp_traces_endpoint;
|
||||||
|
|
||||||
|
let Some((host, port_str)) = endpoint.rsplit_once(':') else {
|
||||||
|
return Err(ConfigError::InvalidValue {
|
||||||
|
field: "trace_config.otlp_traces_endpoint".to_string(),
|
||||||
|
value: endpoint.clone(),
|
||||||
|
reason:
|
||||||
|
"expected format <host>:<port>, e.g., otel-collector:4317 or 127.0.0.1:4317"
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if host.is_empty() {
|
||||||
|
return Err(ConfigError::InvalidValue {
|
||||||
|
field: "trace_config.otlp_traces_endpoint".to_string(),
|
||||||
|
value: endpoint.clone(),
|
||||||
|
reason: "host part cannot be empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// check port: must be 1~65535
|
||||||
|
match port_str.parse::<u16>() {
|
||||||
|
Ok(p) if p > 0 => (), // valid port
|
||||||
|
_ => {
|
||||||
|
return Err(ConfigError::InvalidValue {
|
||||||
|
field: "trace_config.otlp_traces_endpoint".to_string(),
|
||||||
|
value: endpoint.clone(),
|
||||||
|
reason: "port must be a number between 1 and 65535".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_retry(retry: &RetryConfig) -> ConfigResult<()> {
|
fn validate_retry(retry: &RetryConfig) -> ConfigResult<()> {
|
||||||
if retry.max_retries < 1 {
|
if retry.max_retries < 1 {
|
||||||
return Err(ConfigError::InvalidValue {
|
return Err(ConfigError::InvalidValue {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub mod mcp;
|
|||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
pub mod multimodal;
|
pub mod multimodal;
|
||||||
|
pub mod otel_trace;
|
||||||
pub mod policies;
|
pub mod policies;
|
||||||
pub mod protocols;
|
pub mod protocols;
|
||||||
pub mod reasoning_parser;
|
pub mod reasoning_parser;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use tracing_subscriber::{
|
|||||||
fmt::time::ChronoUtc, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
|
fmt::time::ChronoUtc, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{config::TraceConfig, otel_trace::get_otel_layer};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LoggingConfig {
|
pub struct LoggingConfig {
|
||||||
pub level: Level,
|
pub level: Level,
|
||||||
@@ -38,7 +40,7 @@ pub struct LogGuard {
|
|||||||
_file_guard: Option<WorkerGuard>,
|
_file_guard: Option<WorkerGuard>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_logging(config: LoggingConfig) -> LogGuard {
|
pub fn init_logging(config: LoggingConfig, otel_layer_config: Option<TraceConfig>) -> LogGuard {
|
||||||
let _ = LogTracer::init();
|
let _ = LogTracer::init();
|
||||||
|
|
||||||
let level_filter = match config.level {
|
let level_filter = match config.level {
|
||||||
@@ -121,6 +123,19 @@ pub fn init_logging(config: LoggingConfig) -> LogGuard {
|
|||||||
layers.push(file_layer);
|
layers.push(file_layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(otel_layer_config) = &otel_layer_config {
|
||||||
|
if otel_layer_config.enable_trace {
|
||||||
|
match get_otel_layer() {
|
||||||
|
Ok(otel_layer) => {
|
||||||
|
layers.push(otel_layer);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to initialize OpenTelemetry: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let _ = tracing_subscriber::registry()
|
let _ = tracing_subscriber::registry()
|
||||||
.with(env_filter)
|
.with(env_filter)
|
||||||
.with(layers)
|
.with(layers)
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ use sgl_model_gateway::{
|
|||||||
config::{
|
config::{
|
||||||
CircuitBreakerConfig, ConfigError, ConfigResult, DiscoveryConfig, HealthCheckConfig,
|
CircuitBreakerConfig, ConfigError, ConfigResult, DiscoveryConfig, HealthCheckConfig,
|
||||||
HistoryBackend, MetricsConfig, OracleConfig, PolicyConfig, PostgresConfig, RetryConfig,
|
HistoryBackend, MetricsConfig, OracleConfig, PolicyConfig, PostgresConfig, RetryConfig,
|
||||||
RouterConfig, RoutingMode, TokenizerCacheConfig,
|
RouterConfig, RoutingMode, TokenizerCacheConfig, TraceConfig,
|
||||||
},
|
},
|
||||||
core::ConnectionMode,
|
core::ConnectionMode,
|
||||||
metrics::PrometheusConfig,
|
metrics::PrometheusConfig,
|
||||||
|
otel_trace::{is_otel_enabled, shutdown_otel},
|
||||||
server::{self, ServerConfig},
|
server::{self, ServerConfig},
|
||||||
service_discovery::ServiceDiscoveryConfig,
|
service_discovery::ServiceDiscoveryConfig,
|
||||||
version,
|
version,
|
||||||
@@ -348,6 +349,12 @@ struct CliArgs {
|
|||||||
|
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
enable_wasm: bool,
|
enable_wasm: bool,
|
||||||
|
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
enable_trace: bool,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "localhost:4317")]
|
||||||
|
otlp_traces_endpoint: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OracleConnectSource {
|
enum OracleConnectSource {
|
||||||
@@ -539,6 +546,11 @@ impl CliArgs {
|
|||||||
host: self.prometheus_host.clone(),
|
host: self.prometheus_host.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let trace_config = Some(TraceConfig {
|
||||||
|
enable_trace: self.enable_trace,
|
||||||
|
otlp_traces_endpoint: self.otlp_traces_endpoint.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
let mut all_urls = Vec::new();
|
let mut all_urls = Vec::new();
|
||||||
match &mode {
|
match &mode {
|
||||||
RoutingMode::Regular { worker_urls } => {
|
RoutingMode::Regular { worker_urls } => {
|
||||||
@@ -624,6 +636,7 @@ impl CliArgs {
|
|||||||
.maybe_api_key(self.api_key.as_ref())
|
.maybe_api_key(self.api_key.as_ref())
|
||||||
.maybe_discovery(discovery)
|
.maybe_discovery(discovery)
|
||||||
.maybe_metrics(metrics)
|
.maybe_metrics(metrics)
|
||||||
|
.maybe_trace(trace_config)
|
||||||
.maybe_log_dir(self.log_dir.as_ref())
|
.maybe_log_dir(self.log_dir.as_ref())
|
||||||
.maybe_request_id_headers(
|
.maybe_request_id_headers(
|
||||||
(!self.request_id_headers.is_empty()).then(|| self.request_id_headers.clone()),
|
(!self.request_id_headers.is_empty()).then(|| self.request_id_headers.clone()),
|
||||||
@@ -768,6 +781,8 @@ Provide --worker-urls or PD flags as usual.",
|
|||||||
let server_config = cli_args.to_server_config(router_config);
|
let server_config = cli_args.to_server_config(router_config);
|
||||||
let runtime = tokio::runtime::Runtime::new()?;
|
let runtime = tokio::runtime::Runtime::new()?;
|
||||||
runtime.block_on(async move { server::startup(server_config).await })?;
|
runtime.block_on(async move { server::startup(server_config).await })?;
|
||||||
|
if is_otel_enabled() {
|
||||||
|
shutdown_otel();
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ impl<B> MakeSpan<B> for RequestSpan {
|
|||||||
// Don't try to extract request ID here - it won't be available yet
|
// Don't try to extract request ID here - it won't be available yet
|
||||||
// The RequestIdLayer runs after TraceLayer creates the span
|
// The RequestIdLayer runs after TraceLayer creates the span
|
||||||
info_span!(
|
info_span!(
|
||||||
|
target: "sgl_model_gateway::otel-trace",
|
||||||
"http_request",
|
"http_request",
|
||||||
method = %request.method(),
|
method = %request.method(),
|
||||||
uri = %request.uri(),
|
uri = %request.uri(),
|
||||||
@@ -215,6 +216,7 @@ impl<B> MakeSpan<B> for RequestSpan {
|
|||||||
status_code = Empty,
|
status_code = Empty,
|
||||||
latency = Empty,
|
latency = Empty,
|
||||||
error = Empty,
|
error = Empty,
|
||||||
|
module = "sglang::router_rs"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
OnceLock,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::http::{HeaderMap, HeaderName, HeaderValue};
|
||||||
|
use opentelemetry::{global, trace::TracerProvider as _, KeyValue};
|
||||||
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
|
use opentelemetry_sdk::{
|
||||||
|
propagation::TraceContextPropagator,
|
||||||
|
runtime,
|
||||||
|
trace::{BatchConfigBuilder, BatchSpanProcessor, Tracer as SdkTracer, TracerProvider},
|
||||||
|
Resource,
|
||||||
|
};
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
|
use tracing::{Metadata, Subscriber};
|
||||||
|
use tracing_opentelemetry::{self, OpenTelemetrySpanExt};
|
||||||
|
use tracing_subscriber::{
|
||||||
|
layer::{Context, Filter},
|
||||||
|
Layer,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::routers::http::events::get_module_path as http_router_get_module_path;
|
||||||
|
|
||||||
|
static ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
// global tracer
|
||||||
|
static TRACER: OnceLock<SdkTracer> = OnceLock::new();
|
||||||
|
static PROVIDER: OnceLock<TracerProvider> = OnceLock::new();
|
||||||
|
|
||||||
|
pub struct CustomOtelFilter {
|
||||||
|
allowed_targets: HashSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomOtelFilter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut allowed_targets = HashSet::new();
|
||||||
|
allowed_targets.insert("sgl_model_gateway::otel-trace".to_string());
|
||||||
|
allowed_targets.insert(http_router_get_module_path().to_string());
|
||||||
|
|
||||||
|
Self { allowed_targets }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Filter<S> for CustomOtelFilter
|
||||||
|
where
|
||||||
|
S: Subscriber,
|
||||||
|
{
|
||||||
|
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
|
||||||
|
self.allowed_targets.contains(meta.target())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> tracing::subscriber::Interest {
|
||||||
|
if self.allowed_targets.contains(meta.target()) {
|
||||||
|
tracing::subscriber::Interest::always()
|
||||||
|
} else {
|
||||||
|
tracing::subscriber::Interest::never()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CustomOtelFilter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// init OpenTelemetry connection
|
||||||
|
pub fn otel_tracing_init(enable: bool, otlp_endpoint: Option<&str>) -> Result<()> {
|
||||||
|
if !enable {
|
||||||
|
ENABLED.store(false, Ordering::Relaxed);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint = otlp_endpoint.unwrap_or("localhost:4317");
|
||||||
|
let endpoint = if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
|
||||||
|
format!("http://{}", endpoint)
|
||||||
|
} else {
|
||||||
|
endpoint.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = std::panic::catch_unwind(|| -> Result<()> {
|
||||||
|
global::set_text_map_propagator(TraceContextPropagator::new());
|
||||||
|
|
||||||
|
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||||
|
.with_tonic()
|
||||||
|
.with_endpoint(endpoint)
|
||||||
|
.with_protocol(opentelemetry_otlp::Protocol::Grpc)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let batch_config = BatchConfigBuilder::default()
|
||||||
|
.with_scheduled_delay(Duration::from_millis(500))
|
||||||
|
.with_max_export_batch_size(64)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let span_processor = BatchSpanProcessor::builder(exporter, runtime::Tokio)
|
||||||
|
.with_batch_config(batch_config)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let resource = Resource::default().merge(&Resource::new(vec![KeyValue::new(
|
||||||
|
"service.name",
|
||||||
|
"sgl-router",
|
||||||
|
)]));
|
||||||
|
|
||||||
|
let provider = TracerProvider::builder()
|
||||||
|
.with_span_processor(span_processor)
|
||||||
|
.with_resource(resource)
|
||||||
|
.build();
|
||||||
|
PROVIDER
|
||||||
|
.set(provider.clone())
|
||||||
|
.map_err(|_| anyhow::anyhow!("Provider already initialized"))?;
|
||||||
|
|
||||||
|
let tracer = provider.tracer("sgl-router");
|
||||||
|
|
||||||
|
TRACER
|
||||||
|
.set(tracer)
|
||||||
|
.map_err(|_| anyhow::anyhow!("Tracer already initialized"))?;
|
||||||
|
|
||||||
|
let _ = global::set_tracer_provider(provider);
|
||||||
|
|
||||||
|
ENABLED.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => {
|
||||||
|
eprintln!(
|
||||||
|
"[tracing] OpenTelemetry initialized successfully, enabled: {}",
|
||||||
|
ENABLED.load(Ordering::Relaxed)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
eprintln!("[tracing] Failed to initialize OTLP tracer: {}", e);
|
||||||
|
ENABLED.store(false, Ordering::Relaxed);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("[tracing] Panic during OpenTelemetry initialization");
|
||||||
|
ENABLED.store(false, Ordering::Relaxed);
|
||||||
|
Err(anyhow::anyhow!("Panic during initialization"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_otel_layer<S>() -> Result<Box<dyn Layer<S> + Send + Sync + 'static>, &'static str>
|
||||||
|
where
|
||||||
|
S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a> + Send + Sync,
|
||||||
|
{
|
||||||
|
if !is_otel_enabled() {
|
||||||
|
return Err("OpenTelemetry is not enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
let tracer = TRACER
|
||||||
|
.get()
|
||||||
|
.ok_or("Tracer not initialized. Call otel_tracing_init first.")?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let custom_filter = CustomOtelFilter::new();
|
||||||
|
|
||||||
|
let layer = tracing_opentelemetry::layer()
|
||||||
|
.with_tracer(tracer)
|
||||||
|
.with_filter(custom_filter);
|
||||||
|
|
||||||
|
Ok(Box::new(layer))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_otel_enabled() -> bool {
|
||||||
|
ENABLED.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn flush_spans_async() -> Result<()> {
|
||||||
|
if !is_otel_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(provider) = PROVIDER.get() {
|
||||||
|
let provider = provider.clone();
|
||||||
|
|
||||||
|
spawn_blocking(move || provider.force_flush())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
anyhow::anyhow!("Failed to join blocking task for flushing spans: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Provider not initialized"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown_otel() {
|
||||||
|
if ENABLED.load(Ordering::Relaxed) {
|
||||||
|
global::shutdown_tracer_provider();
|
||||||
|
ENABLED.store(false, Ordering::Relaxed);
|
||||||
|
eprintln!("[tracing] OpenTelemetry shut down");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inject_trace_context_http(headers: &mut HeaderMap) -> Result<()> {
|
||||||
|
if !is_otel_enabled() {
|
||||||
|
return Err(anyhow::anyhow!("OTEL not enabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = tracing::Span::current().context();
|
||||||
|
|
||||||
|
struct HeaderInjector<'a>(&'a mut HeaderMap);
|
||||||
|
|
||||||
|
impl<'a> opentelemetry::propagation::Injector for HeaderInjector<'a> {
|
||||||
|
fn set(&mut self, key: &str, value: String) {
|
||||||
|
if let Ok(header_name) = HeaderName::from_bytes(key.as_bytes()) {
|
||||||
|
if let Ok(header_value) = HeaderValue::from_str(&value) {
|
||||||
|
self.0.insert(header_name, header_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global::get_text_map_propagator(|propagator| {
|
||||||
|
propagator.inject_context(&context, &mut HeaderInjector(headers));
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//! request events for observability and monitoring
|
||||||
|
|
||||||
|
use tracing::{debug, event, Level};
|
||||||
|
|
||||||
|
use crate::otel_trace::is_otel_enabled;
|
||||||
|
|
||||||
|
pub fn get_module_path() -> &'static str {
|
||||||
|
module_path!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Event {
|
||||||
|
fn emit(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RequestPDSentEvent {
|
||||||
|
pub prefill_url: String,
|
||||||
|
pub decode_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event for RequestPDSentEvent {
|
||||||
|
fn emit(&self) {
|
||||||
|
if !is_otel_enabled() {
|
||||||
|
debug!(
|
||||||
|
"Sending concurrent requests to prefill={} decode={}",
|
||||||
|
self.prefill_url, self.decode_url
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
prefill_url = %self.prefill_url,
|
||||||
|
decode_url = %self.decode_url,
|
||||||
|
"Sending concurrent requests"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RequestSentEvent {
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event for RequestSentEvent {
|
||||||
|
fn emit(&self) {
|
||||||
|
if !is_otel_enabled() {
|
||||||
|
debug!("Sending request to {}", self.url);
|
||||||
|
} else {
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
url = %self.url,
|
||||||
|
"Sending requests"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RequestReceivedEvent {}
|
||||||
|
|
||||||
|
impl Event for RequestReceivedEvent {
|
||||||
|
fn emit(&self) {
|
||||||
|
if !is_otel_enabled() {
|
||||||
|
debug!("Received concurrent requests");
|
||||||
|
} else {
|
||||||
|
event!(Level::INFO, "Received concurrent requests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! HTTP router implementations
|
//! HTTP router implementations
|
||||||
|
|
||||||
|
pub mod events;
|
||||||
pub mod pd_router;
|
pub mod pd_router;
|
||||||
pub mod pd_types;
|
pub mod pd_types;
|
||||||
pub mod router;
|
pub mod router;
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ use serde_json::{json, Value};
|
|||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
use super::pd_types::api_path;
|
use super::{
|
||||||
|
events::{self, Event},
|
||||||
|
pd_types::api_path,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::types::RetryConfig,
|
config::types::RetryConfig,
|
||||||
core::{
|
core::{
|
||||||
is_retryable_status, RetryExecutor, Worker, WorkerLoadGuard, WorkerRegistry, WorkerType,
|
is_retryable_status, RetryExecutor, Worker, WorkerLoadGuard, WorkerRegistry, WorkerType,
|
||||||
},
|
},
|
||||||
metrics::RouterMetrics,
|
metrics::RouterMetrics,
|
||||||
|
otel_trace::inject_trace_context_http,
|
||||||
policies::{LoadBalancingPolicy, PolicyRegistry},
|
policies::{LoadBalancingPolicy, PolicyRegistry},
|
||||||
protocols::{
|
protocols::{
|
||||||
chat::{ChatCompletionRequest, ChatMessage, MessageContent},
|
chat::{ChatCompletionRequest, ChatMessage, MessageContent},
|
||||||
@@ -391,6 +395,12 @@ impl PDRouter {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut headers_with_trace = headers.cloned().unwrap_or_default();
|
||||||
|
let headers = match inject_trace_context_http(&mut headers_with_trace) {
|
||||||
|
Ok(()) => Some(&headers_with_trace),
|
||||||
|
Err(_) => headers,
|
||||||
|
};
|
||||||
|
|
||||||
// Build both requests
|
// Build both requests
|
||||||
let prefill_request = self.build_post_with_headers(
|
let prefill_request = self.build_post_with_headers(
|
||||||
&self.client,
|
&self.client,
|
||||||
@@ -410,15 +420,16 @@ impl PDRouter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Send both requests concurrently and wait for both
|
// Send both requests concurrently and wait for both
|
||||||
debug!(
|
events::RequestPDSentEvent {
|
||||||
"Sending concurrent requests to prefill={} decode={}",
|
prefill_url: prefill.url().to_string(),
|
||||||
prefill.url(),
|
decode_url: decode.url().to_string(),
|
||||||
decode.url()
|
}
|
||||||
);
|
.emit();
|
||||||
|
|
||||||
let (prefill_result, decode_result) =
|
let (prefill_result, decode_result) =
|
||||||
tokio::join!(prefill_request.send(), decode_request.send());
|
tokio::join!(prefill_request.send(), decode_request.send());
|
||||||
debug!("Received responses from both servers");
|
|
||||||
|
events::RequestReceivedEvent {}.emit();
|
||||||
|
|
||||||
let duration = start_time.elapsed();
|
let duration = start_time.elapsed();
|
||||||
RouterMetrics::record_pd_request_duration(context.route, duration);
|
RouterMetrics::record_pd_request_duration(context.route, duration);
|
||||||
@@ -873,7 +884,11 @@ impl PDRouter {
|
|||||||
// Whitelist important end-to-end headers, skip hop-by-hop
|
// Whitelist important end-to-end headers, skip hop-by-hop
|
||||||
let forward = matches!(
|
let forward = matches!(
|
||||||
name_lc.as_str(),
|
name_lc.as_str(),
|
||||||
"authorization" | "x-request-id" | "x-correlation-id"
|
"authorization"
|
||||||
|
| "x-request-id"
|
||||||
|
| "x-correlation-id"
|
||||||
|
| "traceparent" // W3C Trace Context
|
||||||
|
| "tracestate" // W3C Trace Context
|
||||||
) || name_lc.starts_with("x-request-id-");
|
) || name_lc.starts_with("x-request-id-");
|
||||||
if forward {
|
if forward {
|
||||||
if let Ok(val) = value.to_str() {
|
if let Ok(val) = value.to_str() {
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ use reqwest::Client;
|
|||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
use super::events::{self, Event};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::types::RetryConfig,
|
config::types::RetryConfig,
|
||||||
core::{
|
core::{
|
||||||
is_retryable_status, ConnectionMode, RetryExecutor, Worker, WorkerRegistry, WorkerType,
|
is_retryable_status, ConnectionMode, RetryExecutor, Worker, WorkerRegistry, WorkerType,
|
||||||
},
|
},
|
||||||
metrics::RouterMetrics,
|
metrics::RouterMetrics,
|
||||||
|
otel_trace::inject_trace_context_http,
|
||||||
policies::PolicyRegistry,
|
policies::PolicyRegistry,
|
||||||
protocols::{
|
protocols::{
|
||||||
chat::ChatCompletionRequest,
|
chat::ChatCompletionRequest,
|
||||||
@@ -212,6 +214,16 @@ impl Router {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
events::RequestSentEvent {
|
||||||
|
url: worker.url().to_string(),
|
||||||
|
}
|
||||||
|
.emit();
|
||||||
|
let mut headers_with_trace = headers.cloned().unwrap_or_default();
|
||||||
|
let headers = match inject_trace_context_http(&mut headers_with_trace) {
|
||||||
|
Ok(()) => Some(&headers_with_trace),
|
||||||
|
Err(_) => headers,
|
||||||
|
};
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.send_typed_request(
|
.send_typed_request(
|
||||||
headers,
|
headers,
|
||||||
@@ -223,6 +235,8 @@ impl Router {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
events::RequestReceivedEvent {}.emit();
|
||||||
|
|
||||||
worker.record_outcome(response.status().is_success());
|
worker.record_outcome(response.status().is_success());
|
||||||
|
|
||||||
// For retryable failures, we need to decrement load since send_typed_request
|
// For retryable failures, we need to decrement load since send_typed_request
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use crate::{
|
|||||||
logging::{self, LoggingConfig},
|
logging::{self, LoggingConfig},
|
||||||
metrics::{self, PrometheusConfig},
|
metrics::{self, PrometheusConfig},
|
||||||
middleware::{self, AuthConfig, QueuedRequest},
|
middleware::{self, AuthConfig, QueuedRequest},
|
||||||
|
otel_trace,
|
||||||
protocols::{
|
protocols::{
|
||||||
chat::ChatCompletionRequest,
|
chat::ChatCompletionRequest,
|
||||||
classify::ClassifyRequest,
|
classify::ClassifyRequest,
|
||||||
@@ -703,25 +704,35 @@ pub fn build_app(
|
|||||||
pub async fn startup(config: ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn startup(config: ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
if let Some(trace_config) = &config.router_config.trace_config {
|
||||||
|
otel_trace::otel_tracing_init(
|
||||||
|
trace_config.enable_trace,
|
||||||
|
Some(&trace_config.otlp_traces_endpoint),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
let _log_guard = if !LOGGING_INITIALIZED.swap(true, Ordering::SeqCst) {
|
let _log_guard = if !LOGGING_INITIALIZED.swap(true, Ordering::SeqCst) {
|
||||||
Some(logging::init_logging(LoggingConfig {
|
Some(logging::init_logging(
|
||||||
level: config
|
LoggingConfig {
|
||||||
.log_level
|
level: config
|
||||||
.as_deref()
|
.log_level
|
||||||
.and_then(|s| match s.to_uppercase().parse::<Level>() {
|
.as_deref()
|
||||||
Ok(l) => Some(l),
|
.and_then(|s| match s.to_uppercase().parse::<Level>() {
|
||||||
Err(_) => {
|
Ok(l) => Some(l),
|
||||||
warn!("Invalid log level string: '{s}'. Defaulting to INFO.");
|
Err(_) => {
|
||||||
None
|
warn!("Invalid log level string: '{s}'. Defaulting to INFO.");
|
||||||
}
|
None
|
||||||
})
|
}
|
||||||
.unwrap_or(Level::INFO),
|
})
|
||||||
json_format: false,
|
.unwrap_or(Level::INFO),
|
||||||
log_dir: config.log_dir.clone(),
|
json_format: false,
|
||||||
colorize: true,
|
log_dir: config.log_dir.clone(),
|
||||||
log_file_name: "sgl-model-gateway".to_string(),
|
colorize: true,
|
||||||
log_targets: None,
|
log_file_name: "sgl-model-gateway".to_string(),
|
||||||
}))
|
log_targets: None,
|
||||||
|
},
|
||||||
|
config.router_config.trace_config.clone(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use axum::{body::Body, extract::Request, http::StatusCode};
|
||||||
|
use common::mock_worker::{HealthStatus, MockWorker, MockWorkerConfig, WorkerType};
|
||||||
|
use opentelemetry_proto::tonic::collector::trace::v1::{
|
||||||
|
trace_service_server::{TraceService, TraceServiceServer},
|
||||||
|
ExportTraceServiceRequest, ExportTraceServiceResponse,
|
||||||
|
};
|
||||||
|
use portpicker::pick_unused_port;
|
||||||
|
use serde_json::json;
|
||||||
|
use serial_test::serial;
|
||||||
|
use sgl_model_gateway::{
|
||||||
|
config::{RouterConfig, TraceConfig},
|
||||||
|
core::Job,
|
||||||
|
logging, otel_trace,
|
||||||
|
routers::RouterFactory,
|
||||||
|
};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use tonic_v12::{transport::Server, Request as TonicRequest, Response, Status};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
use tracing::info_span;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TestOtelCollector {
|
||||||
|
span_count: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestOtelCollector {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
span_count: Arc::new(AtomicUsize::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_span_count(&self) -> usize {
|
||||||
|
self.span_count.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic_v12::async_trait]
|
||||||
|
impl TraceService for TestOtelCollector {
|
||||||
|
async fn export(
|
||||||
|
&self,
|
||||||
|
request: TonicRequest<ExportTraceServiceRequest>,
|
||||||
|
) -> Result<Response<ExportTraceServiceResponse>, Status> {
|
||||||
|
let req = request.into_inner();
|
||||||
|
|
||||||
|
let mut total_spans = 0;
|
||||||
|
|
||||||
|
for resource_span in &req.resource_spans {
|
||||||
|
for scope_span in &resource_span.scope_spans {
|
||||||
|
total_spans += scope_span.spans.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.span_count.fetch_add(total_spans, Ordering::SeqCst);
|
||||||
|
|
||||||
|
Ok(Response::new(ExportTraceServiceResponse {
|
||||||
|
partial_success: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_collector(
|
||||||
|
port: u16,
|
||||||
|
shutdown_rx: oneshot::Receiver<()>,
|
||||||
|
) -> Result<TestOtelCollector, Box<dyn std::error::Error>> {
|
||||||
|
let addr = format!("0.0.0.0:{}", port).parse()?;
|
||||||
|
let collector = TestOtelCollector::new();
|
||||||
|
let collector_clone = collector.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = Server::builder()
|
||||||
|
.add_service(TraceServiceServer::new(collector_clone))
|
||||||
|
.serve_with_shutdown(addr, async {
|
||||||
|
shutdown_rx.await.ok();
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
|
||||||
|
Ok(collector)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_router_with_tracing() {
|
||||||
|
// 1. Start the OTLP collector
|
||||||
|
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);
|
||||||
|
println!("OTLP Collector started on: {}", collector_endpoint);
|
||||||
|
|
||||||
|
// 2. create the mock worker
|
||||||
|
let mut mock_worker = MockWorker::new(MockWorkerConfig {
|
||||||
|
port: 0,
|
||||||
|
worker_type: WorkerType::Regular,
|
||||||
|
health_status: HealthStatus::Healthy,
|
||||||
|
response_delay_ms: 0,
|
||||||
|
fail_rate: 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let worker_url = mock_worker.start().await.unwrap();
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
println!("Mock worker started on: {}", worker_url);
|
||||||
|
|
||||||
|
// 3. create router config and enable tracing
|
||||||
|
let router_config = RouterConfig::builder()
|
||||||
|
.regular_mode(vec![worker_url.clone()])
|
||||||
|
.random_policy()
|
||||||
|
.host("0.0.0.0")
|
||||||
|
.port(0)
|
||||||
|
.max_payload_size(256 * 1024 * 1024)
|
||||||
|
.request_timeout_secs(60)
|
||||||
|
.worker_startup_timeout_secs(1)
|
||||||
|
.worker_startup_check_interval_secs(1)
|
||||||
|
.max_concurrent_requests(64)
|
||||||
|
.queue_timeout_secs(60)
|
||||||
|
.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");
|
||||||
|
|
||||||
|
let trace_config = TraceConfig {
|
||||||
|
enable_trace: true,
|
||||||
|
otlp_traces_endpoint: collector_endpoint.clone(),
|
||||||
|
};
|
||||||
|
let _log_guard = logging::init_logging(
|
||||||
|
logging::LoggingConfig {
|
||||||
|
level: tracing::Level::INFO,
|
||||||
|
json_format: false,
|
||||||
|
log_dir: None,
|
||||||
|
colorize: false,
|
||||||
|
log_file_name: "test-otel".to_string(),
|
||||||
|
log_targets: Some(vec!["sgl_model_gateway".to_string()]),
|
||||||
|
},
|
||||||
|
Some(trace_config),
|
||||||
|
);
|
||||||
|
println!("Logging initialized with OTEL layer");
|
||||||
|
|
||||||
|
// 5. Create a span and sleep for a while
|
||||||
|
let _span = info_span!(target: "sgl_model_gateway::otel-trace", "test_router_with_tracing");
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
drop(_span);
|
||||||
|
|
||||||
|
// 6. create app context and router
|
||||||
|
let app_context = common::create_test_context(router_config.clone()).await;
|
||||||
|
|
||||||
|
// 7. initialize worker
|
||||||
|
let job_queue = app_context
|
||||||
|
.worker_job_queue
|
||||||
|
.get()
|
||||||
|
.expect("JobQueue should be initialized");
|
||||||
|
|
||||||
|
let job = Job::InitializeWorkersFromConfig {
|
||||||
|
router_config: Box::new(router_config.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
job_queue
|
||||||
|
.submit(job)
|
||||||
|
.await
|
||||||
|
.expect("Failed to submit worker init job");
|
||||||
|
|
||||||
|
// 8. wait for worker initialization
|
||||||
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||||
|
println!("Workers initialized");
|
||||||
|
|
||||||
|
// 9. create router
|
||||||
|
let router = RouterFactory::create_router(&app_context)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create router");
|
||||||
|
|
||||||
|
println!("Router created");
|
||||||
|
|
||||||
|
// 10. create app (middleware::create_logging_layer() will use the already initialized OTEL layer)
|
||||||
|
let app =
|
||||||
|
common::test_app::create_test_app_with_context(Arc::from(router), app_context.clone());
|
||||||
|
|
||||||
|
println!("App created with logging middleware");
|
||||||
|
|
||||||
|
// 10. send request
|
||||||
|
let request_body = json!({
|
||||||
|
"model": "test-model",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "Hello, test OpenTelemetry tracing!"}
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 50
|
||||||
|
});
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method("POST")
|
||||||
|
.uri("/v1/chat/completions")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(request_body.to_string()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Sending request to router...");
|
||||||
|
let response = app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK, "Request should succeed");
|
||||||
|
|
||||||
|
println!("Request completed successfully");
|
||||||
|
drop(response);
|
||||||
|
|
||||||
|
// 11. Wait for spans to be exported
|
||||||
|
match otel_trace::flush_spans_async().await {
|
||||||
|
Ok(_) => println!("Spans flushed successfully"),
|
||||||
|
Err(e) => println!("Failed to flush spans: {:?}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Verify that the spans were exported to the collector
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 13. cleanup
|
||||||
|
let _ = shutdown_tx.send(());
|
||||||
|
mock_worker.stop().await;
|
||||||
|
|
||||||
|
println!("Cleanup completed");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user