chore(clickhouse): 30d TTL on the trace-list rollup tables#137
Conversation
…otel_traces The 005 rollup tables were created without a TTL, so they would grow unbounded — and otel_traces_recent is one row per span (the full span history). Add a 30-day TTL matching otel_traces (toDateTime(<ts>) + toIntervalDay(30)) via migration 006 (ALTER MODIFY TTL) and inline in the bootstrap schema, so the derived data ages out in lockstep with the spans it summarizes.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dc51437. Configure here.
There was a problem hiding this comment.
3 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="infra/clickhouse/schema/ha-replicated-otel.sql">
<violation number="1" location="infra/clickhouse/schema/ha-replicated-otel.sql:476">
P2: Trace-list rollups are now hard-capped at 30 days, but the repository’s `otel_traces` source table definition still has no TTL. That means older traces can still exist in `otel_traces` while `otel_traces_recent`/`otel_traces_summary` have already dropped them, causing the rollup-backed list path to under-report historical traces relative to source data. Consider adding/updating the source table TTL in the same schema/migration set (or documenting/enforcing it in code-managed SQL) so retention actually stays in lockstep.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| ENGINE = ReplicatedMergeTree('/clickhouse/{cluster}/tables/{shard}/{database}/{table}', '{replica}') | ||
| PARTITION BY toDate(ts) | ||
| ORDER BY (project_id, ts) | ||
| TTL toDateTime(ts) + toIntervalDay(30) |
There was a problem hiding this comment.
P2: Trace-list rollups are now hard-capped at 30 days, but the repository’s otel_traces source table definition still has no TTL. That means older traces can still exist in otel_traces while otel_traces_recent/otel_traces_summary have already dropped them, causing the rollup-backed list path to under-report historical traces relative to source data. Consider adding/updating the source table TTL in the same schema/migration set (or documenting/enforcing it in code-managed SQL) so retention actually stays in lockstep.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At infra/clickhouse/schema/ha-replicated-otel.sql, line 476:
<comment>Trace-list rollups are now hard-capped at 30 days, but the repository’s `otel_traces` source table definition still has no TTL. That means older traces can still exist in `otel_traces` while `otel_traces_recent`/`otel_traces_summary` have already dropped them, causing the rollup-backed list path to under-report historical traces relative to source data. Consider adding/updating the source table TTL in the same schema/migration set (or documenting/enforcing it in code-managed SQL) so retention actually stays in lockstep.</comment>
<file context>
@@ -473,6 +473,7 @@ CREATE TABLE IF NOT EXISTS superlog.otel_traces_recent ON CLUSTER superlog_ha
ENGINE = ReplicatedMergeTree('/clickhouse/{cluster}/tables/{shard}/{database}/{table}', '{replica}')
PARTITION BY toDate(ts)
ORDER BY (project_id, ts)
+TTL toDateTime(ts) + toIntervalDay(30)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1
;
</file context>
There was a problem hiding this comment.
Intentional, and now spelled out in the migration comment. We are deliberately not adding a TTL to the open-core otel_traces (005’s rationale stands). Capping only the derived tables does not under-report: the read path gates on the recent index covering the window start (traceRollupCoversWindow) and falls back to the raw otel_traces scan for anything older than the rollup retains. So beyond the derived retention the list is served the slow-but-complete way from source, rather than silently truncated — retention stays correct without forcing a source-table TTL.
There was a problem hiding this comment.
The parent comment was wrong here: the derived-table TTL is intentional, and traceRollupCoversWindow already falls back to the raw otel_traces scan, so older windows aren’t silently truncated. The source table TTL doesn’t need to be added for correctness in this PR.
…ntion Review feedback: - Summary TTL was anchored on `start` (earliest span). A trace whose spans span several days would then lose its summary row 30d after its FIRST span, while otel_traces_recent rows (and source spans) for its LATER spans still exist — the fast path could find the trace_id but return no stats. Anchor the summary TTL on `end_unix_nano` (the trace's last span) so the summary outlives / expires with the last recent row. - The comment claimed otel_traces carries a 30d TTL; the open-core source table has none. Reframe: this is a standalone 30d bound on the derived tables (the recent index is one row per span, so it would otherwise grow unbounded), made safe by the read path's coverage gate, which falls back to the raw scan for any window the rollup no longer covers — so a shorter derived retention never under-reports.

Follow-up to #136. The rollup tables (
otel_traces_recent,otel_traces_summary) were created without a TTL, so they'd grow unbounded — andotel_traces_recentholds one row per span (the full span history). Give both the same 30-day TTL asotel_traces(toDateTime(<ts>) + toIntervalDay(30)) so the derived data ages out in lockstep with the spans it summarizes.migrations/006_otel_traces_rollup_ttl.sql—ALTER … MODIFY TTLfor existing clusters (idempotent;ttl_only_drop_parts = 1keeps enforcement to whole-part drops).No app code change.
Note
Low Risk
Infrastructure-only ClickHouse retention on derived tables; read path already falls back to raw traces when rollups don’t cover the window.
Overview
Adds 30-day retention on the trace-list rollup tables
otel_traces_recentandotel_traces_summaryso they no longer grow without bound (especiallyotel_traces_recent, which stores one row per span).otel_traces_recentexpires rows 30 days after each span’sts.otel_traces_summaryusesend_unix_nano(last span end), not trace start, so long-running traces don’t lose summary stats while recent rows for the sametrace_idstill exist.Existing HA clusters get idempotent
ALTER … MODIFY TTLin006_otel_traces_rollup_ttl.sql; fresh installs pick up the same rules inha-replicated-otel.sql, still withttl_only_drop_parts = 1. No application changes—older windows that rollups no longer cover keep using the existing rawotel_tracesfallback via the coverage gate.Reviewed by Cursor Bugbot for commit 2c3fe52. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Add a 30-day TTL to the ClickHouse rollup tables
otel_traces_recentandotel_traces_summaryto cap storage. The summary TTL anchors on the trace end so fast-path stats stay consistent; older windows fall back to the raw scan.infra/clickhouse/migrations/006_otel_traces_rollup_ttl.sqltoALTER ... MODIFY TTLonsuperlog.otel_traces_recentandsuperlog.otel_traces_summaryacross thesuperlog_hacluster.toDateTime(ts) + toIntervalDay(30)onotel_traces_recent;toDateTime(fromUnixTimestamp64Nano(end_unix_nano)) + toIntervalDay(30)onotel_traces_summary.ttl_only_drop_parts = 1for whole-part drops.Written for commit 2c3fe52. Summary will update on new commits.