Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2cdb466
refactor: separate OTLP HTTP decoding from trace codec
yujonglee-berri Oct 1, 2026
3361089
feat: complete trace ingestion and read paths
yujonglee-berri Oct 1, 2026
d658271
fix: encode OTLP protobuf errors in Rust
yujonglee-berri Oct 1, 2026
4a4307b
fix: raise OTLP body limit to 16 MiB
yujonglee-berri Oct 1, 2026
6a9b197
test: cover OTLP auth body parsing boundary
yujonglee-berri Oct 1, 2026
1aad40b
refactor: parse OTLP media type into enum
yujonglee-berri Oct 1, 2026
cb67ba4
fix: enforce OTLP body size at HTTP boundary
yujonglee-berri Oct 1, 2026
89cb993
perf: preserve shared OTLP metadata across ingestion
yujonglee-berri Oct 1, 2026
9ecb10c
bench: compare owned and shared trace resource fanout
yujonglee-berri Oct 1, 2026
e59aa75
refactor: extract shared storage and Python conversion caches
yujonglee-berri Oct 1, 2026
5b376da
refactor: keep shared storage owned by traces
yujonglee-berri Oct 1, 2026
02442b3
test: keep trace loopback coverage in Rust
yujonglee-berri Oct 1, 2026
3b7c9e7
test(proxy): adapt trace coverage to injected access context
yujonglee-berri Oct 1, 2026
d63e323
fix(tracing): satisfy stacked branch lint checks
yujonglee-berri Oct 1, 2026
211abd7
refactor(tracing): use immutable ingestion payloads
yujonglee-berri Oct 1, 2026
f3041e6
fix(tracing): declare native error encoder export
yujonglee-berri Oct 1, 2026
7dada1e
test(proxy): resolve trace access through dependency
yujonglee-berri Oct 1, 2026
b5110d3
merge: sync trace boundary with tracing dependencies
yujonglee-berri Oct 1, 2026
22d9125
fix(tracing): align merged normalizer types and bridge tests
yujonglee-berri Oct 1, 2026
09a0352
merge: include migration test fix from tracing base
yujonglee-berri Oct 1, 2026
eb85680
fix(tracing): address ingestion and diagnostic review findings
yujonglee-berri Oct 1, 2026
f6e145e
Merge remote-tracking branch 'origin/litellm_tracing_dependencies' in…
yujonglee-berri Oct 1, 2026
484ce13
fix(proxy): preserve body parsing for partial request scopes
yujonglee-berri Oct 1, 2026
d9ed39a
test(proxy): use valid HTTP scopes in request fixtures
yujonglee-berri Oct 1, 2026
8019534
test(proxy): complete auth request flow scopes
yujonglee-berri Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions litellm-rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions litellm-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ litellm-core = { path = "crates/core" }
litellm-gateway-mcp = { path = "crates/gateway-mcp" }
litellm-gateway = { path = "crates/gateway" }
litellm-gateway-inference = { path = "crates/gateway-inference" }
litellm-gateway-traces = { path = "crates/gateway-traces" }
litellm-gateway-auth = { path = "crates/gateway-auth" }
litellm-gateway-management = { path = "crates/gateway-management" }
litellm-gateway-ui = { path = "crates/gateway-ui" }
Expand Down Expand Up @@ -81,6 +82,7 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "mul
qdrant-client = { version = "1.19.0", default-features = false }
uuid = { version = "1", features = ["v4"] }
rstest = "0.26.1"
wiremock = "0.6.5"
rstest_reuse = "0.7.0"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustify = "=0.7.0"
Expand Down Expand Up @@ -115,6 +117,8 @@ time = { version = "0.3.53", features = ["parsing"] }
criterion = "0.8.2"
fancy-regex = "0.19.2"
veil = "0.3.0"
prost = "0.14.4"
opentelemetry-proto = "0.33"

[profile.release]
opt-level = 3
Expand Down
2 changes: 1 addition & 1 deletion litellm-rust/crates/cache-azure-blob/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ litellm-cache-testing.workspace = true
rstest.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
wiremock = "0.6.5"
wiremock.workspace = true
2 changes: 1 addition & 1 deletion litellm-rust/crates/cache-gcs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ litellm-cache-testing.workspace = true
rstest.workspace = true
serde_json.workspace = true
tokio.workspace = true
wiremock = "0.6.5"
wiremock.workspace = true
2 changes: 1 addition & 1 deletion litellm-rust/crates/cache-response/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ redis = "1.7.0"
redis-test = "1.0.4"
rstest.workspace = true
tokio.workspace = true
wiremock = "0.6.5"
wiremock.workspace = true
2 changes: 1 addition & 1 deletion litellm-rust/crates/cache-s3/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ tokio.workspace = true
litellm-http = { workspace = true, features = ["test-support"] }
litellm-cache-testing.workspace = true
rstest.workspace = true
wiremock = "0.6.5"
wiremock.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
2 changes: 1 addition & 1 deletion litellm-rust/crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ litellm-host-native.workspace = true
litellm-llms = { workspace = true, features = ["test-support"] }
rstest.workspace = true
rstest_reuse.workspace = true
wiremock = "0.6.5"
wiremock.workspace = true
2 changes: 1 addition & 1 deletion litellm-rust/crates/gateway-inference/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ futures-util.workspace = true
tokio = { workspace = true, features = ["io-util"] }
rstest.workspace = true
tower = { version = "0.5.3", features = ["util"] }
wiremock = "0.6.5"
wiremock.workspace = true
18 changes: 18 additions & 0 deletions litellm-rust/crates/gateway-traces/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "litellm-gateway-traces"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true

[dependencies]
axum.workspace = true
flate2.workspace = true
litellm-traces.workspace = true

[dev-dependencies]
prost.workspace = true
opentelemetry-proto = { workspace = true, features = ["gen-tonic-messages", "trace", "with-serde"] }
rstest.workspace = true
tokio.workspace = true
tower = { version = "0.5", features = ["util"] }
125 changes: 125 additions & 0 deletions litellm-rust/crates/gateway-traces/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use std::{future::Future, io::Read, sync::Arc};

use axum::{
Router,
body::to_bytes,
extract::{Request, State},
http::{StatusCode, header},
response::{IntoResponse, Response},
routing::post,
};
use flate2::read::MultiGzDecoder;
use litellm_traces::{DecodeError, DecodedSpan, decode_otlp};

pub const MAX_BODY_BYTES: usize = 16 * 1024 * 1024;

fn otlp_max_body_bytes() -> usize {
std::env::var("OTLP_MAX_BODY_BYTES")
.ok()
.and_then(|value| value.trim().parse().ok())
.unwrap_or(MAX_BODY_BYTES)
}

pub trait SpanSink: Send + Sync + 'static {
fn write(&self, spans: Vec<DecodedSpan>) -> impl Future<Output = Result<(), ()>> + Send;
}

pub fn router<S: SpanSink>(sink: S) -> Router {
Router::new()
.route("/v1/traces", post(ingest::<S>))
.with_state(Arc::new(sink))
Comment thread
yujonglee-berri marked this conversation as resolved.
Resolved
}

async fn ingest<S: SpanSink>(State(sink): State<Arc<S>>, request: Request) -> Response {
let max_body_bytes = otlp_max_body_bytes();
let content_type = request
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let encoding = request
.headers()
.get(header::CONTENT_ENCODING)
.map(|value| value.to_str().map(str::to_owned));
let response_type = if content_type.as_deref().is_some_and(|value| {
value
.split(';')
.next()
.is_some_and(|value| value.trim().eq_ignore_ascii_case("application/json"))
}) {
"application/json"
} else {
"application/x-protobuf"
};
let result = async {
let encoding = encoding.transpose().map_err(|_| StatusCode::BAD_REQUEST)?;
let body = to_bytes(request.into_body(), max_body_bytes.saturating_add(1))
.await
.map_err(|_| StatusCode::PAYLOAD_TOO_LARGE)?;
let payload = decode_content_encoding(&body, encoding.as_deref(), max_body_bytes)?;
let spans =
decode_otlp(&payload, content_type.as_deref()).map_err(|error| match error {
DecodeError::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
DecodeError::InvalidPayload => StatusCode::BAD_REQUEST,
})?;
sink.write(spans)
.await
.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)
}
.await;
match result {
Ok(()) if response_type == "application/json" => {
([(header::CONTENT_TYPE, response_type)], "{}").into_response()
}
Ok(()) => ([(header::CONTENT_TYPE, response_type)], "").into_response(),
Err(status) => status.into_response(),
}
}

fn decode_content_encoding(
body: &[u8],
encoding: Option<&str>,
max_body_bytes: usize,
) -> Result<Vec<u8>, StatusCode> {
if body.len() > max_body_bytes {
return Err(StatusCode::PAYLOAD_TOO_LARGE);
}
match encoding {
None => Ok(body.to_vec()),
Some(value) if value.eq_ignore_ascii_case("identity") => Ok(body.to_vec()),
Some(value) if value.eq_ignore_ascii_case("gzip") => {
let mut payload = Vec::new();
MultiGzDecoder::new(body)
.take((max_body_bytes as u64).saturating_add(1))
.read_to_end(&mut payload)
.map_err(|_| StatusCode::BAD_REQUEST)?;
if payload.len() > max_body_bytes {
return Err(StatusCode::PAYLOAD_TOO_LARGE);
}
Ok(payload)
}
_ => Err(StatusCode::BAD_REQUEST),
}
}

#[cfg(test)]
mod tests {
use std::io::Write;

use flate2::{Compression, write::GzEncoder};
use rstest::rstest;

use super::*;

#[rstest]
fn decompressed_size_limit_rejects_expansion() {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&vec![b' '; MAX_BODY_BYTES + 1]).unwrap();
let compressed = encoder.finish().unwrap();
assert!(compressed.len() < MAX_BODY_BYTES);
assert!(matches!(
decode_content_encoding(&compressed, Some("gzip"), MAX_BODY_BYTES),
Err(StatusCode::PAYLOAD_TOO_LARGE)
));
}
}
Loading
Loading