feat(local): maple start — standalone local observability binary#65
Open
Makisuo wants to merge 27 commits into
Open
feat(local): maple start — standalone local observability binary#65Makisuo wants to merge 27 commits into
maple start — standalone local observability binary#65Makisuo wants to merge 27 commits into
Conversation
|
This run croaked 😵 The workflow encountered an error before any progress could be reported. Please check the link below for details. |
Adds `maple start`, a standalone single-binary local mode: OTLP/HTTP
ingest into an embedded in-process ClickHouse (chDB), reusing the
production OTLP→NDJSON encoders and the generated ClickHouse schema so
local rows are shaped identically to cloud. Single-tenant (OrgId="local").
- chdb module: one dedicated writer thread owns the chDB session; all
bootstrap/insert/query is funneled through it (chDB is single-owner).
- new `maple` bin gated behind the `local` cargo feature so the
production maple-ingest build never links libchdb; clap CLI, Axum
routes, rust-embed SPA fallback.
- telemetry::encode_local_{traces,logs,metrics} wrap the private encoders
for zero row-mapping divergence with the Tinybird path.
- schema codegen: emit local-schema.sql + local-inserts.json from the
Tinybird manifest, wired into the clickhouse:schema task.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pre-existing regression on `main` (introduced by ac723de "feat: fix some react stuff", which rewrote this provider to use createElement). main's CI is red on the same error; this branch inherits it via rebase onto main. AutocompleteValuesProvider's `children` prop is required, so React 19's createElement overload requires it in the props object — passing it as the variadic 3rd arg leaves the required prop unsatisfied (TS2769). Move children into the props object. Functionally identical; unblocks @maple/web typecheck. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ingest Rust Test + Benchmark ResultsCommit: Load Benchmark —
|
| Metric | main (median) | PR (median) | Delta |
|---|---|---|---|
| Requests/sec | 2412.92 | 2268.52 | -6.0% worse |
| Rows/sec | 24129.23 | 22685.17 | -6.0% worse |
| p50 latency | 25.83 ms | 27.66 ms | +7.1% worse |
| p95 latency | 28.34 ms | 30.43 ms | +7.4% worse |
| p99 latency | 30.40 ms | 32.74 ms | +7.7% worse |
| Export catch-up | 0.026 s | 0.026 s | -0.6% better |
| Max RSS | 97.58 MiB | 96.01 MiB | -1.6% better |
| 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": 24,
"duration_seconds": 0.897704576,
"export_catchup_seconds": 0.025471513,
"request_rps": 2227.904428104419,
"row_rps": 22279.044281044193,
"p50_ms": 27.729,
"p95_ms": 31.516,
"p99_ms": 34.113,
"max_rss_mb": 96.75390625,
"max_cpu_percent": 75.0,
"avg_cpu_percent": 54.15
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 25,
"duration_seconds": 0.881633053,
"export_catchup_seconds": 0.026095632,
"request_rps": 2268.5174894412676,
"row_rps": 22685.174894412674,
"p50_ms": 27.665,
"p95_ms": 30.425,
"p99_ms": 32.744,
"max_rss_mb": 95.90625,
"max_cpu_percent": 75.0,
"avg_cpu_percent": 45.8
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 24,
"duration_seconds": 0.816166996,
"export_catchup_seconds": 0.026171473,
"request_rps": 2450.478896845763,
"row_rps": 24504.788968457626,
"p50_ms": 25.621,
"p95_ms": 27.202,
"p99_ms": 28.461,
"max_rss_mb": 96.0078125,
"max_cpu_percent": 80.3,
"avg_cpu_percent": 56.8
}
]main load benchmark JSON (per-iteration)
[
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 25,
"duration_seconds": 0.895887909,
"export_catchup_seconds": 0.026263043,
"request_rps": 2232.4221366403103,
"row_rps": 22324.221366403104,
"p50_ms": 28.031,
"p95_ms": 31.086,
"p99_ms": 32.759,
"max_rss_mb": 97.1875,
"max_cpu_percent": 75.0,
"avg_cpu_percent": 45.8
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 25,
"duration_seconds": 0.828870368,
"export_catchup_seconds": 0.025845742,
"request_rps": 2412.9225476184474,
"row_rps": 24129.225476184474,
"p50_ms": 25.832,
"p95_ms": 28.341,
"p99_ms": 30.403,
"max_rss_mb": 99.15234375,
"max_cpu_percent": 80.3,
"avg_cpu_percent": 60.15
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 23,
"duration_seconds": 0.814727958,
"export_catchup_seconds": 0.026269159,
"request_rps": 2454.807129621051,
"row_rps": 24548.07129621051,
"p50_ms": 25.498,
"p95_ms": 27.858,
"p99_ms": 28.268,
"max_rss_mb": 97.578125,
"max_cpu_percent": 80.7,
"avg_cpu_percent": 57.0
}
]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 32.24s
Running benches/ingest_bench.rs (target/release/deps/ingest_bench-56e0fe315b7f3811)
Gnuplot not found, using plotters backend
test ingest_accept/logs_10_rows_wal_ack ... bench: 467413 ns/iter (+/- 7714)
test ingest_accept/traces_10_spans_wal_ack ... bench: 503311 ns/iter (+/- 23826)
cargo test
Updating crates.io index
Compiling maple-ingest v0.1.0 (/home/runner/work/maple/maple/apps/ingest)
Finished `test` profile [unoptimized + debuginfo] target(s) in 6.73s
Running unittests src/lib.rs (target/debug/deps/maple_ingest-8b9e9fc61a910385)
running 22 tests
test telemetry::tests::apply_attribute_mappings_rewrites_span_attributes ... ok
test otel::tests::build_resource_sets_runtime_and_sdk_type ... ok
test telemetry::tests::hex_empty_for_zero_ids ... ok
test telemetry::tests::log_encoder_matches_tinybird_row_shape ... ok
test telemetry::tests::logs_severity_text_falls_back_to_mapped_number ... ok
test telemetry::tests::logs_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::custom_datasource_names_propagate_to_frames ... ok
test telemetry::tests::logs_use_observed_time_when_time_unix_nano_is_zero ... ok
test telemetry::tests::metrics_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::metric_encoder_matches_all_tinybird_datasource_shapes ... ok
test telemetry::tests::metrics_summary_data_points_are_dropped ... ok
test telemetry::tests::sampling_keeps_errors_even_when_ratio_low ... 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_e2e_exports_gzip_ndjson_to_fake_tinybird ... ok
test telemetry::tests::pipeline_e2e_exports_metrics_to_fake_tinybird ... ok
test telemetry::tests::pipeline_e2e_exports_traces_to_fake_tinybird ... ok
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s
Running unittests src/bin/load_test.rs (target/debug/deps/load_test-3ae74910c06cd17d)
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-c2f428c94e6b99e8)
running 18 tests
test tests::cloudflare_validation_payload_is_detected ... 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::d1_response_parses_empty_results_as_no_match ... ok
test tests::d1_response_parses_failure_with_errors ... ok
test tests::d1_truthy_accepts_int_and_bool_self_managed ... ok
test tests::enrichment_overwrites_tenant_fields ... ok
test tests::d1_response_parses_success_with_rows ... ok
test tests::cloudflare_ndjson_payload_parses_multiple_records ... ok
test tests::non_self_managed_goes_to_shared_pool ... ok
test tests::hash_is_deterministic ... ok
test tests::extract_ingest_key_returns_sentinel_literal_unchanged ... ok
test tests::self_managed_degrades_to_shared_when_endpoint_unset ... ok
test tests::resolve_ingest_key_returns_self_managed_true_when_active_settings_row ... ok
test tests::resolve_ingest_key_returns_none_when_hash_missing ... ok
test tests::self_managed_goes_to_self_managed_pool_when_configured ... ok
test tests::sentinel_token_matches_only_exact_literal ... ok
test tests::resolve_ingest_key_returns_self_managed_false_when_no_settings_row ... ok
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
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
Adds the frontend and distribution half of `maple start` (the standalone
local Maple binary). The Rust ingest binary + embedded chDB (earlier on this
branch) now has a UI to drive it and a reproducible way to ship it.
apps/local-ui — minimal React + Vite SPA (no Clerk/router/Cloudflare):
- hash-routed trace list / trace detail / logs views
- data layer hits the binary's POST /local/query, builds ClickHouse SQL
via @maple/query-engine (CH.compile) pinned to the `local` org
- reuses @maple/ui components (trace waterfall, severity badges, tables)
- its dist/ is synced into apps/ingest/ui-dist/ and baked in via rust-embed
packages/ui/src/lib/span-tree.ts — shared buildTraceDetail/buildSpanTree
(branded TraceId/SpanId) consumed by the local trace-detail view.
scripts/build-local-binary.sh — builds the SPA, syncs ui-dist, compiles
`maple` with the `local` feature, then bundles libchdb.so beside the binary
and rewrites the dynamic-load path (install_name_tool @rpath/@loader_path on
macOS, patchelf $ORIGIN on Linux) so the 2-file bundle is relocatable.
.github/workflows/local-binary-release.yml — builds the bundle on native
runners per target triple (aarch64/x86_64 darwin + linux; native because
chdb-rust downloads a host-specific libchdb), ad-hoc codesigns macOS, tars
maple + libchdb.so with a sha256, and publishes to a GitHub release on v*.
ui-dist is now a build artifact: only .gitkeep is tracked (rust-embed needs
the folder to exist); the generated SPA output is gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Orphan scaffolding never imported anywhere; ToolbarSearch already has its own inline debounce (with external-sync for "Clear all", which this hook lacks). Knip's files rule is error-severity, so the unused file failed CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…actor TinybirdConfig consolidated the flat datasource_traces/logs/metrics_* fields into a single `datasources: DatasourceNames`, but benches/ingest_bench.rs still constructed the old shape — breaking `cargo bench --bench ingest_bench` (E0560) and failing the Cargo test CI job. Use DatasourceNames::defaults(), matching the telemetry tests and main.rs construction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds scripts/install.sh: detects OS/arch, downloads the matching bundle from the latest GitHub release, verifies the sha256, installs the 3-file bundle into ~/.maple/bin (kept co-located for the rpath + maple→maple-cli forwarding), clears the macOS quarantine, and symlinks `maple` onto PATH. Documents the one-liner in docs/local-mode.md and the release notes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
apps/landing copies scripts/install.sh → public/cli/install at build/dev time (single source via the `sync:install` script; the copy is gitignored). Astro's static build emits it to dist/cli/install, so the canonical installer URL is: curl -fsSL https://maple.dev/cli/install | sh Updates the script header, docs/local-mode.md, and release notes to the nice URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unners Replace the single aggregated `release` job (which waited for all 4 platforms) with a per-platform "Publish to release" step at the end of each build job. Each platform creates/uploads to the release as soon as it finishes. A queued macos-13 runner can no longer block arm64 mac and Linux bundles from publishing. `gh release create ... || true` handles the creation race between concurrent jobs. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…essage Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Use printf to write release notes to a file — heredocs inside YAML literal block scalars break when the terminator/content have less indentation than the block, causing instant workflow failures on every branch push. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
maple-cli (the Bun/TypeScript query CLI) is now baked into the maple binary at build time via rust-embed (#[folder = "cli-dist/"]), the same pattern used for the SPA. The build script compiles it before cargo so rust-embed picks it up. At runtime, the first query subcommand (services, traces, etc.) extracts the embedded CLI to ~/.maple/maple-cli-<hash> and execs into it; subsequent invocations skip re-extraction. Distribution shrinks from 3 files (maple + libchdb.so + maple-cli) to 2 (maple + libchdb.so). The install script, CI, and docs are updated to match. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- `maple start` writes ~/.maple/maple.pid and refuses to start a second instance with a clear "already running (PID N) — run maple stop" message. Stale PID files from crashes are cleaned up automatically. - `maple stop` sends SIGTERM and waits up to 5 s, printing progress dots, then reports "maple stopped." or suggests kill -9. - Graceful shutdown wires SIGTERM + SIGINT into axum's with_graceful_shutdown and removes the PID file on clean exit. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
executor.ts was refactored to export makeLocalWarehouseExecutor(baseUrl) and DEFAULT_LOCAL_URL instead of the resolved LocalWarehouseExecutorLive. Update bin.ts to match. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- auth.ts: Effect.catchAll removed in Effect v4; replace with Effect.catch - warehouse.ts: executor.query pipe param is typed as WarehouseQueryName union; the passthrough wrapper casts string→WarehouseQueryName Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Knip has no entry for apps/local-cli so it can't trace the import chain from src/bin.ts and marks auth.ts + warehouse.ts as unused files (error). Add the workspace with src/bin.ts as the entry point. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Overview
Adds
maple start: a single, distributable binary that runs a local OTLP ingest endpoint + embedded storage + a purpose-built telemetry UI — a Jaeger/Grafana-style local tool that reuses Maple's real ingest pipeline, real ClickHouse schema, real SQL query layer, and real visualization components, without shipping the heavy multi-tenant SaaS frontend (no Clerk, billing, agents, or Cloudflare).This PR is the full feature across the planned phases; the three commits build on each other.
What's in it
Backend —
apps/ingest(maplebin,localfeature)WriteMode::Local+chdbmodule owning a single embedded chDB session (in-process ClickHouse — no server, no Docker). All bootstrap/insert/query calls serialized through one writer.encode_traces/logs/metricsrow-building (zero mapping divergence from the Tinybird path), batched per datasource.local-schema.sql) on startup — base tables and materialized views, so chDB derives the rest on INSERT.POST /local/queryruns SQL through chDB;GET /*serves the embedded SPA via rust-embed. Single pinned tenant (OrgId = "local").Shared components —
packages/uiapps/webso both apps share them.span-tree.tsexposesbuildTraceDetail/buildSpanTreewith brandedTraceId/SpanId.Frontend —
apps/local-ui(new minimal React + Vite SPA)@maple/query-engine(CH.compile) pinned to thelocalorg and posts to/local/query.useInfiniteQuery+@tanstack/react-virtualfor logs.Build & distribution
scripts/build-local-binary.sh: builds the SPA → syncs intoapps/ingest/ui-dist/(rust-embed bakes it in) →cargo build --features local --bin maple→ bundleslibchdb.sobeside the binary and rewrites the dynamic-load path (install_name_tool@rpath/@loader_pathon macOS,patchelf $ORIGINon Linux) → relocatable 2-file bundle..github/workflows/local-binary-release.yml: builds on native runners per triple (aarch64/x86_64× darwin/linux), ad-hoc codesigns macOS, tarsmaple+libchdb.sowith a sha256, publishes to a GitHub release onv*tags.Why native runners (no cross-compilation)
chdb-rust'sbuild.rsdownloads a host-specific prebuiltlibchdb.soand links it. Cross-compiling would link the wrong architecture's engine, so each target is built on a matching native runner.Verification
DYLD_LIBRARY_PATH.Reviewer notes
APPLE_*secrets not configured here.libchdbdownload path in particular.apps/ingest/ui-dist/is now a build artifact: only.gitkeepis tracked (rust-embed needs the folder to exist); the generated SPA output is gitignored. The previously-tracked staleui-dist/index.htmlis removed.🤖 Generated with Claude Code