A headless, open-source observability backend: unified logs, metrics, and traces access, built-in alerting, and a native Model Context Protocol (MCP) interface, built on Elixir/OTP with a Rust hot path for columnar data.
Pulso ships no UI. It exists to be talked to by humans through their own dashboards and, first-class, by AI agents through MCP.
Warning
Pulso is in early scaffolding. The design is settled in docs/architecture.md; the codebase is still catching up to it. Expect the shape to change without warning until we tag a first release.
The Tuist deployment plan describes the implementation milestones, cost measurements, and rollout gates for collecting logs, metrics, and traces and replacing Grafana Cloud services.
The architecture is documented in docs/architecture.md. Read that first if you want to understand what Pulso is trying to be. The short version:
- ☁️ Object storage is the source of truth. S3 (or R2, GCS, Azure Blob, MinIO) holds every acknowledged record. Local disk is a warm cache and nothing more.
- 🧩 Shared-nothing nodes. No shared database, no leader election, no consensus service. Nodes coordinate only through S3 conditional writes and rendezvous hashing.
- ⚙️ BEAM for orchestration, Rust for bytes. Elixir/OTP owns concurrency, supervision, backpressure, and the MCP surface. Rust owns Parquet, DataFusion, and the S3 client, called via Rustler NIFs.
- 🤖 MCP first-class. The primary read surface is MCP tools. HTTP wire protocols (OTLP, Prometheus
remote_write, Loki push) exist to accept telemetry from existing agents unchanged.
The Erlang, Elixir, and Rust toolchains are needed to build Pulso. Erlang and Elixir are pinned in mise.toml; a stable Rust toolchain (from rustup or your package manager) covers the NIF. With mise installed:
mise install
mix setup
mix testThe first build compiles the Rust NIFs under native/pulso_object_store and native/pulso_codec and copies the shared objects into priv/native/. Subsequent builds are incremental.
To boot the app locally:
mix phx.server-
OTLP/HTTP JSON logs land at
POST /v1/logs. Tenant is picked up fromX-Scope-OrgID(Loki/Cortex convention), defaulting todefault. -
Loki push lands at
POST /loki/api/v1/push(same tenant convention), both as JSON (optionally gzip-encoded) and as Snappy-compressed protobuf, the default Grafana Alloy and Promtail send. The protobuf path is decoded in Rust. -
The MCP endpoint is exposed at
POST /mcp. It implements the stateless MCP2026-07-28Streamable HTTP transport: no handshake or session, each request carries its version and capabilities inparams._metaand mirrors them inMCP-Protocol-Version,Mcp-Method, and (fortools/call)Mcp-Nameheaders. Methods:server/discover,tools/list,tools/call,subscriptions/listen. Browser origins must be allowlisted withPULSO_MCP_ALLOWED_ORIGINS. -
Self-monitoring is exposed at
GET /metricsin Prometheus text format. Scrape each node into an independent monitoring system, not Pulso itself. Keep the listener private; any public ingest/MCP proxy must deny or separately authenticate the metrics path, since port-level network policy cannot separate it from ingest. See self-monitoring for metrics, counting boundaries, and example queries.
All ingest receivers enforce per-request record and attribute budgets and reject oversized batches in full with HTTP 413. See ingest limits for defaults, runtime configuration, counting rules, and authentication boundaries.
The Rust NIF talks to any S3-compatible endpoint. docker-compose.yml brings up MinIO and preseeds a bucket:
docker compose up -d- API on
http://localhost:9000, console onhttp://localhost:9001(minioadmin/minioadmin). - Preseeded bucket:
pulso.
To run the integration test suite against MinIO:
PULSO_INTEGRATION=1 mix test --only integrationEvery PULSO_MINIO_* variable defaults to the values docker-compose sets up, so no other environment is needed when running against the local stack.
Before opening a pull request:
mix precommitThis runs mix compile --warnings-as-errors, mix deps.unlock --unused, mix format, and mix test — the same checks CI runs.
Pulso is released under the MIT License.
Prometheus remote_write samples ingested at POST /api/v1/write can be queried
through the read-only query_promql Model Context Protocol tool.
Its tenant and query arguments are required. end_ts_ns selects the instant
evaluation time (defaults to now); adding start_ts_ns and a positive step_ms
selects a range query.
The initial Prometheus Query Language
subset supports selectors, rate, increase, irate, delta, five over-time
functions (sum, avg, min, max, count), and nested vector aggregations
with by/without grouping. For example:
sum by (job) (rate(http_requests_total{job="api"}[5m]))
avg without (instance) (process_resident_memory_bytes)
Compatibility endpoints GET|POST /api/v1/query and /api/v1/query_range accept
Prometheus parameters (query, time, or start/end/step) and the same
X-Scope-OrgID tenant and authorization headers as ingestion. Timestamps accept
Unix seconds or date-time strings with a timezone; steps accept seconds or
unit durations such as 15s. Client timeout values are accepted and capped at ten seconds. Results use Prometheus vector or matrix envelopes.
Queries have sample, work, result, time, and heap budgets. Conflicting samples at the same timestamp resolve deterministically with a warning. Unsupported parameter overrides and expressions return an error. Binary operations, scalar expressions,
subqueries, histograms, negative offsets, @, and stale-marker semantics are
not included yet. See the architecture for query limits
and the remaining compatibility gaps.