Skip to content

feat(traces): add Rust storage foundation - #43819

Merged
yujonglee-berri merged 12 commits into
mainfrom
litellm_trace_query_api
Sep 30, 2026
Merged

yujonglee-berri merged 12 commits into
mainfrom
litellm_trace_query_api

Conversation

@yujonglee-berri

@yujonglee-berri yujonglee-berri commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Agent tracing needs one ClickHouse schema and bounded storage path

How it solves it:

Pre-Submission checklist

  • I have added meaningful tests
  • Focused Rust and bridge checks passed locally
  • My PR passes all required CI/CD checks
  • My PR's scope is as isolated as possible
  • I have received a Greptile Confidence Score of at least 4/5

Type

New Feature

Caveats (if any)

Medium

  • Existing incompatible ClickHouse tables require a manual migration

Low

Final Attestation

  • The tests check the right things, including edge cases and real-world regressions

Devin Review

@codspeed

codspeed Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_trace_query_api (b0518f2) with main (264b09a)

Open in CodSpeed

@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/rust_bridge/traces.py 0.00% 20 Missing ⚠️

📢 Thoughts on this report? Let us know!

@yujonglee-berri yujonglee-berri changed the title feat(traces): add Rust-backed trace query scaffolding feat(traces): add Rust storage foundation Sep 30, 2026
@yujonglee-berri
yujonglee-berri added this pull request to stack #43860 September 30, 2026 14:13
@yujonglee-berri
yujonglee-berri removed this pull request from stack #43860 September 30, 2026 14:44
@yujonglee-berri
yujonglee-berri added this pull request to stack #43866 September 30, 2026 14:44
@yujonglee-berri
yujonglee-berri marked this pull request as ready for review September 30, 2026 18:35
@yujonglee-berri
yujonglee-berri requested a review from a team September 30, 2026 18:35

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 7 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +26 to +28
<query>GRANT SELECT ON default.otel_traces</query>
<query>GRANT SELECT ON default.agent_traces</query>
<query>GRANT SELECT ON default.spend_logs</query>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Reader cannot query custom trace databases

When ensure_schema creates a nondefault database, litellm_traces_reader retains grants only on default. Queries against the configured trace tables fail with access denied

Learn more

The schema creator accepts any valid database identifier in schema_statements. The reader profile grants SELECT on tables in default only. When trace_query selects another database through Connection::configured, SELECT reads tables on which that user has no grants.

Example: Creating tables in trace_test succeeds. Querying trace_test.otel_traces as litellm_traces_reader fails with access denied.

Recommended fix: Match reader grants to the configured schema database during deployment or provisioning, and test a nondefault database with the restricted reader.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +23 to +24
PARTITION BY toDate(StartTs)
ORDER BY (TeamId, TraceId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Trace rollups split across calendar days

When one trace spans midnight, StartTs puts its batches into different partitions. AggregatingMergeTree cannot merge the same (TeamId, TraceId) across partitions, leaving multiple incomplete rollup rows

Learn more

The materialized view agent_traces_mv emits one aggregate row per trace and insert batch, with StartTs equal to the minimum span timestamp in that batch. AggregatingMergeTree only merges rows with the same key within one partition. A trace with batches on different dates has two distinct aggregate rows even after merges. A read that expects one row per trace receives split counts and incompatible root fields.

Example: Team A inserts the root at 23:59 and an LLM span at 00:01 the next day with the same TraceId. The agent and LLM counts remain in separate rows instead of forming one trace.

Recommended fix: Choose a partition expression stable for all batches of a trace, or explicitly aggregate across partitions on every read and ensure retention matches the intended whole-trace lifetime.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +40 to +42
ENGINE = ReplacingMergeTree(end_time)
PARTITION BY toYYYYMM(start_time)
ORDER BY (team_id, toDateTime(start_time), request_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Same-second spend logs collapse into one

When distinct spend logs share a team, second, and request ID, ReplacingMergeTree treats them as one row. A later merge silently discards one spend entry

Learn more

ReplacingMergeTree identifies duplicates using the ORDER BY tuple, not the version column. Converting start_time to DateTime truncates the DateTime64(3) milliseconds. Distinct request logs that reuse an ID within a second therefore have the same replacing key, and ClickHouse retains only the highest end_time on merge.

Example: Two entries for request req-1 from the same team start at 12:00:00.100 and 12:00:00.800. They share the key (team, 12:00:00, req-1) and one spend record disappears.

Recommended fix: Preserve millisecond precision in the ordering key and determine whether request ID is unique across all spend-log events. Use a key that identifies actual retry duplicates rather than unrelated events.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +50 to +54
let response = client
.post(connection.url().clone())
.body(statement)
.send()
.await

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Schema setup can wait indefinitely

If ClickHouse accepts a schema request but never responds, ensure_schema has no request timeout. Python's ensure_schema future remains pending indefinitely, blocking startup paths awaiting it

Suggested change
let response = client
.post(connection.url().clone())
.body(statement)
.send()
.await
let response = client
.post(connection.url().clone())
.timeout(std::time::Duration::from_secs(15))
.body(statement)
.send()
.await

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +7 to +9
anyLastIf(SpanName, ParentSpanId = '') AS RootName,
anyLastIf(InputPreview, ParentSpanId = '') AS RootInput,
anyLastIf(StatusCode, ParentSpanId = '') AS RootStatus,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing-root batches erase trace root fields

When later batches contain no root span, anyLastIf produces empty root values for their rollup rows. anyLast merges those rows with the root batch and can replace the root name, input, and status with empties

Learn more

The view groups spans from each incoming insert into one row per trace. For a batch with no root span, anyLastIf returns the default empty string. The destination's RootName fields use SimpleAggregateFunction(anyLast, String), which can retain the empty value during part merges rather than preserve the original root.

Example: Insert a root span named agent in batch one, then an LLM child alone in batch two. The second rollup supplies an empty RootName, and the merged trace can lose agent.

Recommended fix: Store a nullable or explicitly conditional root aggregate that ignores absent root spans when combining batches, and test separate root and child inserts followed by merges.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +51 to +52
if sql.trim().is_empty() {
return Err(Error::EmptySql);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Untrusted SQL bypasses reader-only intent

With a privileged ClickHouse account, execute_read accepts arbitrary SQL and relies on a request setting for read-only behavior. The account retains its underlying privileges, so a query that bypasses the setting can modify data.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +20 to +21
if !matches!(url.scheme(), "http" | "https") || url.host().is_none() {
return Err(Error::InvalidUrl);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 ClickHouse credentials travel over plaintext HTTP

When Connection::configured or Connection::writer receives an http:// URL, requests send credentials without transport encryption. Anyone observing that network path can read them.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 1/5

[High risk] Adds database schema and storage layer for traces.

This PR is not ready to merge because trace root metadata can be lost and schema setup can silently retain the wrong policy or stall startup.

Findings

  1. P1 Root fields can disappear ▶
  2. P1 Retention changes are ignored ▶
  3. P1 Schema setup can stall startup ▶
  4. P1 Reader grants exclude default database ▶
  5. P2 Row encoding has no limit ▶

Summary

This PR adds the Rust ClickHouse trace schema, row encoder and bounded read transport, plus Python bridge exports and tests.

  • Partial rollups can lose root metadata.
  • Schema setup does not apply changed retention settings and can wait indefinitely on a stalled server.
  • The supplied reader grants do not cover the tracing caller's default database.
  • Row encoding remains unbounded.

Reviews (1) · Last reviewed commit: 535c3ad

Comment on lines +8 to +10
RootName SimpleAggregateFunction(anyLast, String),
RootInput SimpleAggregateFunction(anyLast, String),
RootStatus SimpleAggregateFunction(anyLast, String),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Root fields can disappear When a root span arrives in one batch and child spans arrive in a later batch, the later batch has empty root fields. Merging those batches with anyLast can overwrite the real values, leaving the trace list in PR #43864 with a blank name, input preview, or status.

Comment on lines +47 to +50
spend_log_retention_days: u32,
) -> Result<(), Error> {
for statement in schema_statements(database, trace_retention_days, spend_log_retention_days)? {
let response = client

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Retention changes are ignored If an operator changes either retention period after the tables exist, ensure_schema runs CREATE TABLE IF NOT EXISTS again but never updates their TTLs. Setup succeeds while trace and spend data continue to be retained or deleted under the old policy.

Comment on lines +49 to +53
for statement in schema_statements(database, trace_retention_days, spend_log_retention_days)? {
let response = client
.post(connection.url().clone())
.body(statement)
.send()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Schema setup can stall startup If ClickHouse accepts a connection but stalls while handling a schema statement, this POST has no total request timeout. The proxy startup path in PR #43864 awaits tracing setup, so startup can remain blocked indefinitely.

Comment on lines +26 to +28
<query>GRANT SELECT ON default.otel_traces</query>
<query>GRANT SELECT ON default.agent_traces</query>
<query>GRANT SELECT ON default.spend_logs</query>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reader grants exclude default database If this reader profile is installed with the tracing caller's default configuration from PR #43864, the tables are created in litellm but these grants cover only default. Valid trace reads then fail with access denied.

Comment on lines +8 to +19
pub fn encode_rows(rows: Vec<BTreeMap<String, Value>>) -> Result<String, Error> {
rows.into_iter()
.map(|row| {
let encoded = row
.into_iter()
.map(|(name, value)| insert_value(&name, value).map(|value| (name, value)))
.collect::<Result<BTreeMap<_, _>, _>>()?;
serde_json::to_string(&encoded).map_err(|_| Error::InvalidRow)
})
.collect::<Result<Vec<_>, _>>()
.map(|rows| rows.join("\n"))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Row encoding has no limit This exported function collects every serialized row and then joins them into another string without checking the total byte count. A large valid batch can consume substantial additional memory before its caller can enforce a write limit; the bounded insert path added in PR #43864 does not change this function.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@yujonglee-berri
yujonglee-berri merged commit 41df8cf into main Sep 30, 2026
98 of 100 checks passed
@yujonglee-berri
yujonglee-berri deleted the litellm_trace_query_api branch September 30, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants