Skip to content

Repository files navigation

Pulse

Grafana-lite you can deploy in 30 seconds. A single-binary log/metrics ingester with a live web dashboard. No Prometheus. No Loki. No YAML.

CI Release License: MIT

Pulse dashboard — live log tail and metrics charts

Status:v0.1.0 released (download) — ingest → aggregation → Parquet → SQL all live, 107 tests green, CI-enforced (clippy -D warnings, fmt --check, test matrix, RustSec audit). Measured on a dev machine: 100k events/s ingested with 0 drops at ~32 MB RSS; kill -9 mid-load recovers without corruption. Full evidence in docs/BENCHMARKS.md; remaining work tracked in docs/ROADMAP.md.


Why

Solo devs and small teams running side projects on a $5 VPS do not want to operate a Prometheus + Grafana + Loki stack. That's three services, a reverse proxy, a pile of config, and gigabytes of RAM — to answer "is my thing up, how slow is it, and what did it just log?"

Pulse is one Rust binary that:

  • accepts logs and metrics over HTTP and UDP (line protocol + JSON),
  • stores them embedded (time-partitioned Parquet, queried with SQL via DataFusion),
  • and serves a beautiful live dashboard (WebSocket tail, live charts, filters) straight from the binary — the frontend is embedded via rust-embed.

One file. One command. ~tens of MB of RAM. The Rust ecosystem loves single-binary tools; this is very shareable.

Who this is for

  • Solo devs / small teams with side projects on a VPS who want observability, not an observability project.
  • Rust shops who want a 2-line tracing integration (pulse-client).
  • Anyone who has ever typed "grafana docker compose" and closed the tab.

Quickstart (the 30-second promise)

# install (prebuilt Linux + macOS binaries from GitHub Releases — Linux builds are
# glibc-linked, not fully static; or: cargo install --path .)
curl -fsSL https://raw.githubusercontent.com/sambai-dev/pulse/main/install.sh | sh

# run
pulse serve --port 8000 --token s3cr3t

# point anything at it
curl -s -X POST localhost:8000/ingest/logs \
  -H "authorization: Bearer s3cr3t" \
  -d '2026-08-22T12:00:00Z INFO api "POST /users 201" latency_ms=12 user_id=42'

# open the dashboard
#   browse to: http://localhost:8000   # live tail + charts + SQL query view
#   Linux desktop: xdg-open http://localhost:8000

Or with Docker:

docker run -p 8000:8000 ghcr.io/sambai-dev/pulse:v0.1.0 serve --port 8000 --bind 0.0.0.0

Note: the server binds to loopback (127.0.0.1) by default, so containers must override the bind (e.g. --bind 0.0.0.0 or PULSE_BIND=0.0.0.0) or published ports are unreachable.

Architecture

                        ┌─────────────────────────────┐
 apps ──HTTP/UDP──────▶ │ Ingest (axum + UDP listener)│
                        │  line protocol + JSON       │
                        └──────────────┬──────────────┘
                                       │ bounded mpsc (backpressure!)
                        ┌──────────────▼──────────────┐
                        │ Aggregator (tokio task)     │
                        │  10s windows · quantile     │
                        │  p50/p95/p99 · tag index    │
                        └───────┬─────────────┬───────┘
                          write │             │ broadcast
              ┌─────────────────▼──┐   ┌──────▼────────────────┐
              │ Storage            │   │ Live fan-out           │
              │ Parquet partitions │   │ tokio broadcast → WS   │
              │ + DataFusion SQL   │   └──────┬────────────────┘
              └────────────────────┘   ┌──────▼────────────────┐
                                       │ Dashboard (uPlot,     │
                                       │ log tail, filters)    │
                                       └───────────────────────┘

Full walkthrough, instrumentation strategy, and memory budget: docs/ARCHITECTURE.md.

Design decisions (and their tradeoffs)

A repo becomes evidence when it argues its choices. Each decision below gets a full write-up in the architecture doc; the one-paragraph versions:

1. Bounded channels with drop-newest, not unbounded queueing. Backpressure is a feature: under overload Pulse degrades to lossy instead of dead. An unbounded queue converts a traffic spike into OOM — a restart, a lost dashboard, a lost everything. Pulse drops newest events past the queue bound, counts every drop in pulse_ingest_dropped_total, and surfaces that counter on its own dashboard. Tradeoff: you lose events exactly when you're having your worst day. Mitigation: the drop counter is itself exported, and UDP senders get fire-and-forget semantics with the same accounting. For a $5-VPS side project, "stays up and tells you it dropped" beats "dies silently."

2. Time-partitioned Parquet + DataFusion, not redb / SQLite / Timescale. Parquet files per time partition give columnar compression, a trivially understandable on-disk story (data/2026-08-22/*.parquet), retention as rm -rf, and — the real prize — a "we query with SQL" demo for free via DataFusion. Tradeoff: more moving parts than a KV store like redb, and DataFusion is a heavy dependency. Chosen because query-ability is the demo that makes people care, and redb can't do GROUP BY service without us writing a query engine.

3. Single static binary with rust-embed, not a separate frontend deploy. The dashboard is compiled into the binary. No CDN, no npm install, no version skew between server and UI. Tradeoff: frontend iteration requires a rebuild — acceptable for a tool whose UI is one page and changes weekly at most.

4. Line protocol + simple JSON, not OpenTelemetry. OTLP is the "right" long-term answer and the wrong v0.1 answer: protobuf schemas, a big dependency tree, and a spec that outgrows a weekend project. A curl-able line protocol makes the 30-second promise testable with zero client code. Tradeoff: no ecosystem interop yet; an OTLP ingestion path is a stretch goal, not a promise.

5. uPlot for charts, not ECharts. uPlot renders tens of thousands of points at 60fps in ~40 KB. ECharts is prettier out of the box and 10x the bundle. Live, high-frequency, canvas-fast wins for a dashboard whose job is a moving line at 1 Hz refresh. Tradeoff: we hand-roll tooltips/legend polish.

Honest limitations

Read this before filing an issue — these are design choices, not bugs:

  • Single-node only. No HA, no replication, no clustering. If the VPS dies, the data dies.
  • Lossy under overload. Bounded queues drop newest (counted, visible). We choose availability over completeness.
  • No alerting (yet). Dashboards only. Alert rules are a v0.2+ conversation.
  • Retention is deletion, not compaction. Old partitions are dropped whole; no downsampling tiers like Prometheus.
  • SQL dialect is DataFusion's, not Postgres. Most of what you want works; not everything you know transfers.
  • UDP ingest is fire-and-forget. No delivery guarantees, ever. Use HTTP when you care.

Benchmarks

Numbers or it didn't happen. Full methodology, ladder results, and honest findings live in docs/BENCHMARKS.md. Measured so far (dev box: i9-14900KF, 64 GB, Windows — VPS run pending):

Metric Result Notes
Ingest throughput 100,160 events/s accepted, 0 dropped (10 s sustained) HTTP, batch 100, 32 client workers
p99 ingest latency @ that rate 1.49 ms per batch request client-observed on loopback
Steady-state RSS at 50–100k/s 26–33 MB goal was < 100 MB
SQL over 1.84M stored logs count(*) 527 ms · GROUP BY service 723 ms DataFusion over Parquet partitions
Crash safety kill -9 mid-load → clean restart, counts consistent atomic tmp→rename + .ok markers
Parser microbench 2.29 µs/log line ≈ 437k lines/s/core criterion

Known honest wrinkle: at sustained 100k/s the single Parquet writer lags and drops ~0.25% of raw log rows (pulse_storage_dropped_total makes it visible); at ≤ 50k/s: zero drops.

Implementation notes (deviations from the original spec)

The docs under docs/ were written before implementation; reality won a few arguments:

  • Quantile sketch is hand-rolled (src/quantile.rs): log-scale bucketed sketch with an exact small-N mode, instead of the tdigests crate. Same memory class (~2 KB/series), deterministic, property-tested against exact quantiles (≤3% error at p50/p95/p99 on uniform + skewed distributions). Avoided a dependency and made accuracy provable.
  • Self-metrics registry is hand-rolled instead of metrics + metrics-exporter-prometheus: ~90 lines of atomics rendering Prometheus text format.
  • No dashmap: the aggregator is single-owner by design, so plain HashMap suffices; concurrency lives at the channel boundary.
  • uPlot is vendored into the binary (51 KB) — no CDN, keeping the offline single-binary promise; canvas fallback charts ship too.
  • Rate limiting ships as a fixed-window per-token limiter (default 600 rpm) rather than the more elaborate per-event budget sketched in the roadmap.

Pipe any Rust app in with two lines

pulse-client (in this repo, crates/pulse-client) is a tracing::Layer that batches your app's events and ships them to a Pulse server — non-blocking, bounded queue, drop counters included:

// Cargo.toml: pulse-client = "0.1"   (crates.io publish pending; path/git dep for now)
let layer = pulse_client::Builder::new("http://vps:8000")
    .token("s3cr3t")
    .service("my-app")            // default: crate target path
    .build()
    .await;

tracing_subscriber::registry().with(layer).init();

tracing::info!(user_id = 42, latency_ms = 12, "handled request"); // → your dashboard

Docs

Doc What's in it
docs/ARCHITECTURE.md Component walkthrough, design decisions in depth, tracing strategy, memory budget
docs/ROADMAP.md 5-week plan with demoable acceptance criteria, launch checklist
docs/INGEST-API.md Line protocol + JSON schema, HTTP/UDP/WS/query endpoints
docs/BENCHMARKS.md Methodology, results tables, flamegraph capture
docs/CI-RELEASE.md CI from day one, release binaries, Docker, install script
docs/UI-NOTES.md UI patterns studied (Datadog, Axiom, Vercel, Railway, Hookdeck) and how Pulse applies them

License

MIT — same as opencode.

About

Grafana-lite you can deploy in 30 seconds: single-binary log/metrics ingester with a live web dashboard. No Prometheus, no Loki, no YAML.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages