perf(query-engine): ClickStack-style skip indexes + token-aware log search#170
perf(query-engine): ClickStack-style skip indexes + token-aware log search#170Makisuo wants to merge 8 commits into
Conversation
…earch Apply optimizations from ClickHouse's "Making ClickStack 5x faster" to Maple's telemetry read path: - logs.Body gains a tokenbf_v1(32768,3,0) index on lower(Body); body search AND-s an interior-token `hasToken(lower(Body), t)` pre-filter ahead of the substring ILIKE. Only separator-bounded, ASCII, interior tokens qualify, so the result set is byte-for-byte unchanged — the index just prunes granules. - logs gains the four attribute-map bloom filters that traces already had. - traces gains ClickStack "Items" bloom indexes over arrayMap((k,v)->concat(k,'=',v), ...) so attribute-equality filters prune via has(items,'k=v') (AND-ed with the exact map equality; enabled only on the raw traces table, not trace_detail_spans or metrics). - Index expressions live in one shared module (index-exprs.ts) imported by the datasource, the migration, and the query predicate so they can't drift. New committed benchmark suite (bench suite): 13 cases compiled from the real query-engine DSL, plus `--table-map` for A/B against shadow tables and `compare --max-regression-pct` as a CI regression gate. BYO migration 0005 adds every index with ADD INDEX (no MATERIALIZE — new parts only; TTL churn backfills), bumping SCHEMA_VERSION to 5. Tests: builder 72, domain 174, query-engine 727, apps/api bench 11; repo typecheck 24/24. Pre-merge gates (need a live warehouse): - Tinybird `tb --cloud deploy --check` on a branch to confirm managed Tinybird accepts tokenbf_v1 + the arrayMap expression index as an in-place ADD INDEX. - Live `EXPLAIN indexes=1` granule-drop check on a dev ClickHouse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Your LLM provider API key was rejected. Rotate the key in your provider dashboard, then update the matching GitHub Actions secret. Update repo secret → · Model settings → · Setup docs → · Ask in Discord →
|
There was a problem hiding this comment.
🚩 Log timeseries raw path does not use bodySearchConditions, but this is pre-existing
The bodySearchConditions integration was applied to logsCountQuery (packages/query-engine/src/ch/queries/logs.ts:360) and logsListQuery (line 455), but the timeseries query's raw path was not updated. This is NOT a regression: the timeseries raw path never had a search filter — canUseLogsAggregateInterior (line 81) returns false when opts.search is set, routing to the raw-only scan, but the raw timeseries scan itself doesn't filter by search term. The timeseries query counts all logs in the window for the volume chart, while search filtering is only applied to the list and count queries. If search-filtered timeseries is ever needed, bodySearchConditions should be added there too.
Was this helpful? React with 👍 or 👎 to provide feedback.
Ingest Rust Test + Benchmark ResultsCommit: Load Benchmark —
|
| Metric | main (median) | PR (median) | Delta |
|---|---|---|---|
| Requests/sec | 2238.69 | 2079.38 | -7.1% worse |
| Rows/sec | 22386.95 | 20793.82 | -7.1% worse |
| p50 latency | 27.72 ms | 30.33 ms | +9.4% worse |
| p95 latency | 32.65 ms | 38.48 ms | +17.9% worse |
| p99 latency | 39.37 ms | 41.30 ms | +4.9% worse |
| Export catch-up | 0.026 s | 0.026 s | +0.4% worse |
| Max RSS | 101.18 MiB | 101.32 MiB | +0.1% worse |
| Failures | 0 | 0 | same |
Same code path on both sides (same LOAD_TEST_INGEST_MODE), so the delta column is meaningful. Numbers come from ubuntu-latest, which is noisy — treat single-digit-percent deltas as noise.
PR load benchmark JSON (per-iteration)
[
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 29,
"duration_seconds": 1.053399643,
"export_catchup_seconds": 0.025876718,
"request_rps": 1898.6146552168518,
"row_rps": 18986.146552168517,
"p50_ms": 31.411,
"p95_ms": 45.195,
"p99_ms": 48.864,
"max_rss_mb": 102.3828125,
"max_cpu_percent": 71.4,
"avg_cpu_percent": 58.03333333333334
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 24,
"duration_seconds": 0.961824271,
"export_catchup_seconds": 0.025820273,
"request_rps": 2079.3819206918306,
"row_rps": 20793.81920691831,
"p50_ms": 30.327,
"p95_ms": 33.965,
"p99_ms": 35.539,
"max_rss_mb": 101.3203125,
"max_cpu_percent": 76.7,
"avg_cpu_percent": 46.650000000000006
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 25,
"duration_seconds": 0.93503533,
"export_catchup_seconds": 0.026451309,
"request_rps": 2138.9566103347133,
"row_rps": 21389.56610334713,
"p50_ms": 28.583,
"p95_ms": 38.484,
"p99_ms": 41.3,
"max_rss_mb": 96.8828125,
"max_cpu_percent": 76.7,
"avg_cpu_percent": 55.0
}
]main load benchmark JSON (per-iteration)
[
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 28,
"duration_seconds": 1.021057731,
"export_catchup_seconds": 0.025281856,
"request_rps": 1958.7531040397166,
"row_rps": 19587.531040397167,
"p50_ms": 31.613,
"p95_ms": 38.266,
"p99_ms": 44.953,
"max_rss_mb": 104.703125,
"max_cpu_percent": 70.6,
"avg_cpu_percent": 58.0
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 25,
"duration_seconds": 0.893377714,
"export_catchup_seconds": 0.025861955,
"request_rps": 2238.6947521281018,
"row_rps": 22386.947521281018,
"p50_ms": 27.718,
"p95_ms": 32.652,
"p99_ms": 39.367,
"max_rss_mb": 99.7421875,
"max_cpu_percent": 78.5,
"avg_cpu_percent": 55.9
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 23,
"duration_seconds": 0.886745797,
"export_catchup_seconds": 0.025777986,
"request_rps": 2255.437811790384,
"row_rps": 22554.378117903838,
"p50_ms": 27.496,
"p95_ms": 29.855,
"p99_ms": 38.386,
"max_rss_mb": 101.18359375,
"max_cpu_percent": 82.1,
"avg_cpu_percent": 49.349999999999994
}
]WAL-acked microbench (cargo bench --bench ingest_bench)
Compiling maple-ingest v0.1.0 (/home/runner/work/maple/maple/apps/ingest)
Finished `bench` profile [optimized] target(s) in 41.01s
Running benches/ingest_bench.rs (target/release/deps/ingest_bench-581d2100de893627)
Gnuplot not found, using plotters backend
test ingest_accept/logs_10_rows_wal_ack ... bench: 505164 ns/iter (+/- 30271)
test ingest_accept/traces_10_spans_wal_ack ... bench: 502560 ns/iter (+/- 30715)
cargo test
test telemetry::tests::metrics_summary_data_points_are_dropped ... ok
test telemetry::tests::metrics_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::migrate_legacy_shard_relocates_frames_into_lanes ... ok
test telemetry::tests::pipeline_can_start_for_clickhouse_only_without_tinybird_credentials ... ok
test telemetry::tests::clickhouse_export_drops_passworded_non_https_endpoint_without_sending ... ok
test telemetry::tests::pipeline_e2e_exports_gzip_ndjson_to_fake_tinybird ... ok
test telemetry::tests::pipeline_e2e_exports_metrics_to_fake_tinybird ... ok
test telemetry::tests::sampling_keeps_errors_even_when_ratio_low ... ok
test telemetry::tests::scraper_contract::scraper_otlp_json_decodes_with_gateway_serde_and_encodes_to_rows ... ok
test telemetry::tests::signal_tag_round_trips_all_variants ... ok
test telemetry::tests::pipeline_e2e_exports_traces_to_fake_tinybird ... ok
test telemetry::tests::telemetry_signal_as_str_is_canonical_lowercase ... ok
test telemetry::tests::timestamp_has_nano_precision ... ok
test telemetry::tests::timestamps_match_clickhouse_datetime64_nine_format ... ok
test telemetry::tests::trace_encoder_matches_tinybird_row_shape ... ok
test telemetry::tests::traces_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::wal_partial_drain_advances_cursor_without_truncating ... ok
test telemetry::tests::wal_round_trips_frame ... ok
test telemetry::tests::wal_truncates_after_full_drain_allowing_further_appends ... ok
test telemetry::tests::pipeline_exports_ready_org_to_clickhouse_without_tinybird_calls ... ok
test telemetry::tests::slow_clickhouse_lane_does_not_block_cosharded_tinybird_org ... ok
test telemetry::tests::clickhouse_breaker_sheds_after_threshold_failures ... ok
test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.78s
Running unittests src/bin/load_test.rs (target/debug/deps/load_test-661a0aa1eb3f6d6d)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/maple_ingest-c33bf80c577edb95)
running 38 tests
test autumn::tests::allowed_only_no_balance_field ... ok
test autumn::tests::flat_hardcap_with_remaining_allows ... ok
test autumn::tests::flat_hardcap_depleted_blocks ... ok
test autumn::tests::flat_overage_allows ... ok
test autumn::tests::flat_unlimited_allows ... ok
test autumn::tests::flat_sub_one_gb_remaining_still_allows ... ok
test autumn::tests::nested_balance_object_depleted_blocks ... ok
test autumn::tests::nested_balance_object_with_remaining_allows ... ok
test autumn::tests::nested_overage_allows ... ok
test autumn::tests::null_balance_no_subscription_blocks ... ok
test autumn::tests::unrecognized_shape_returns_none ... ok
test tests::api_error_from_pipeline_maps_variants_to_status ... ok
test tests::api_error_kind_maps_status_to_stable_label ... ok
test tests::clickhouse_destination_is_terminal_in_dual_mode ... ok
test tests::clickhouse_destination_uses_native_pipeline_even_in_forward_mode ... ok
test tests::clickhouse_target_resolver_decrypts_current_schema_password ... ok
test tests::clickhouse_target_resolver_requires_current_schema ... ok
test tests::cloudflare_ndjson_payload_parses_multiple_records ... ok
test tests::clickhouse_target_resolver_rejects_password_over_http ... ok
test tests::cloudflare_log_record_maps_body_severity_and_attributes ... ok
test tests::cloudflare_timestamps_support_rfc3339_unix_and_unix_nano ... ok
test tests::cloudflare_validation_payload_is_detected ... ok
test tests::decrypt_aes256_gcm_matches_node_crypto_fixture ... ok
test tests::enrichment_overwrites_tenant_fields ... ok
test tests::extract_ingest_key_returns_sentinel_literal_unchanged ... ok
test tests::rejection_span_status_is_error_only_for_5xx ... ok
test tests::hash_is_deterministic ... ok
test tests::resolve_ingest_key_keeps_stale_schema_on_managed_native_path ... ok
test tests::resolve_connector_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_ingest_key_returns_none_when_hash_missing ... ok
test tests::resolve_ingest_key_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_ingest_key_returns_self_managed_false_when_no_settings_row ... ok
test tests::resolve_ingest_key_returns_self_managed_true_when_active_settings_row ... ok
test tests::sentinel_token_matches_only_exact_literal ... ok
test tests::tinybird_destination_keeps_forward_mode_on_forward_path ... ok
test autumn::tests::fails_open_on_transport_error ... ok
test tests::resolve_ingest_key_serves_last_known_routing_when_refresh_fails ... ok
test tests::forward_mode_switches_ready_org_to_clickhouse_without_forwarding_again ... ok
test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s
Doc-tests maple_ingest
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
…-inserts Two generated/barrel artifacts missed in the first commit: - tinybird/index.ts now re-exports ./index-exprs like its sibling modules. - local-inserts.json regenerated to the new schema revision (was stale, would have failed clickhouse:schema:check in CI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tection (#210) * Bootstrap shared production bindings and tighten PlanetScale probes * fix * :3
The shared-bindings bootstrap in #210 added Email.Routing + SendingSubdomain (zone-level enable + DKIM/SPF DNS writes) that the prod CLOUDFLARE_API_TOKEN lacks scopes for, so every push-to-main prd deploy 403'd in the plan phase (POST /zones/{id}/email/routing/enable → Forbidden). Revert the email plumbing to the pre-#210 wiring: both api and alerting keep their plain Cloudflare.Email.SendEmail("email", ...) binding (unchanged from before, needs no extra token scope). The Workers ObservabilityDestination bootstrap stays — that permission is granted and its plan call now succeeds. Removes the emailBinding param from createMapleApi/createAlertingWorker and the now-unused Input/Output imports. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…apes (#212) PlanetScale authenticates the metrics data plane (metrics.psdb.cloud) with a signed, expiring URL — NOT the service token or OAuth bearer. Its Prometheus http_sd response mints per-branch `__param_sig`/`__param_exp` labels, which Prometheus convention promotes to `?sig=&exp=` query params on the scrape URL. The Authorization header (token/bearer) only auths the discovery listing on api.planetscale.com. `subTargetsFromGroup` stripped every `__`-prefixed label except `__scheme__`/`__metrics_path__`, dropping sig/exp and building an unsigned URL, so every branch scrape returned `403 invalid signature` (a 22h+ metrics outage in production). Reconnecting and rotating service tokens couldn't help because the token was never the data-plane credential. Fix: promote `__param_*` labels onto a new `PlanetScaleSubTarget.signedUrl` (base `url` stays param-free so the scraper's per-branch fiber identity + `instance` label don't churn as the signature rotates each 10-min refresh). `ScrapeTargetsService.scrapeForCollector` and `PlanetScaleConnectionService.probeDataPlaneScrape` now fetch `signedUrl`. Verified: the URL built via URLSearchParams is byte-identical to one proven to return HTTP 200 with live branch metrics. apps/api-only deploy; existing targets recover on the next scrape with no reconnect. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Brings PR #170 (ClickStack-style skip indexes + token-aware log search) up to date with main and resolves all conflicts. Key resolutions: - Migration renumbered 0005 -> 0008: main landed migrations 0005 (alert_checks error columns), 0006 (db edge namespace), 0007 (db namespace hyperdrive) since the branch forked, colliding with the clickstack migration's 0005 slot. Renamed the file, bumped version 5 -> 8, and kept every migration in the ordered registry. clickHouseSchemaVersion now resolves to "8". - clickhouse-builder: keep both new exports (main's `arrayJoin`, feat's `has`). - Regenerated all generated artifacts from the merged source (tinybird manifest, clickhouse-schema.ts, local-schema.sql, local-inserts.json, clickhouse_insert_mappings.rs) — consistent PROJECT_REVISION 82756d56..., SCHEMA_VERSION "8". Verified: typecheck 29/29 (incl. Rust ingest); tests domain 177, query-engine 783, clickhouse-builder 73, apps/api bench 11. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

What & why
Applies the schema/query optimizations from ClickHouse's Making ClickStack 5x faster to Maple's telemetry read path. The blog's biggest wins map directly onto gaps Maple had:
logs.Bodyhad no index — search was a full-partitionILIKEscan behind amax_block_size=512OOM guardrailtokenbf_v1onlower(Body)+ a semantics-safehasTokenpre-filterlogshad zero attribute-map indexes (traces had 4)bloom_filterindexes onlogsidx_*_attr_itemsbloom overarrayMap((k,v)->concat(k,'=',v),…)ontracesbench suitecompiled from the real DSL + A/B + regression gateChanges
Indexes (
packages/domain/src/tinybird/datasources.ts)logs:idx_body_tokens(tokenbf_v1(32768,3,0)onlower(Body)) + blooms on mapKeys/mapValues ofLogAttributesandResourceAttributes.traces:idx_span_attr_items+idx_resource_attr_itemsover the concatenatedkey=value"Items" expression.index-exprs.tsand imported by the datasource, the migration, and the query predicate so they can't drift (a mismatch silently disables the index).Query engine
body-search.ts:bodySearchConditionsAND-shasToken(lower(Body), t)for interior, separator-bounded, ASCII tokens only, keeping theILIKEas the final predicate. A single word or edge token yields no pre-filter → today's behavior. Whole-token semantics ≠ substring, so this is the only sound derivation; result sets are unchanged.buildAttrFilterCondition(…, { itemsIndex }): equality filters emithas(items,'k=v')(OR-ed across HTTP semconv aliases) AND-ed with the exact map equality. Enabled only on the rawtracestable (nottrace_detail_spansor metrics, which lack the index).CH.hasToken/CH.has.Migration & rollout
0005_body_and_attr_indexes.tsadds every index withADD INDEX IF NOT EXISTSand noMATERIALIZE(a materialize over billion-row parts exceeds the Worker subrequest budget; indexes cover new parts and backfill via the 30-day TTL).SCHEMA_VERSIONbumps 4→5.Benchmark harness (
apps/api/scripts)bench suitecompiles 13 representative cases from the real DSL;--table-map from=torewritesFROM/JOINfor A/B against shadow tables;bench compare --max-regression-pct Nexits non-zero as a CI gate. SeeBENCH.md.Testing
tb build+ branchtb --cloud deploy --checkto confirm managed Tinybird acceptstokenbf_v1and thearrayMapexpression index as an in-placeADD INDEX(noFORWARD_QUERY/rebuild). Fallback if rejected: keep those indexes BYO-migration-only.EXPLAIN indexes=1granule-drop check on a dev ClickHouse (the localclickhousebinary is macOS-Gatekeeper-blocked in the dev box used here).SCHEMA_VERSIONbump un-readies BYO-ClickHouse ingest untilapplySchemastamps v5; deploy API + applySchema + Rust gateway together.Follow-up (separate PR)
The time-bucketed sort-key /
read_in_orderspike (Phase 3) — thebench suite --table-mapA/B harness in this PR is built to drive it against a dedicated ClickHouse.🤖 Generated with Claude Code