faucet-stream

Crates.io Docs.rs Guide CI Coverage Downloads MSRV Dependencies License Changelog LinkedIn

# faucet-stream **ETL that governs your data while it moves.** faucet-stream is a data-movement platform in a single native binary. It extracts from APIs, databases, files and streams, transforms records in flight, and enforces masking, quality checks, data contracts and data-flow policies *before* anything reaches the destination. Then it loads into your warehouse, lake, database or queue, with resumable state, CDC and effectively-once delivery built in. You don't need to operate a platform, install a Python runtime or pay per row. Pipelines are YAML files that you version, review and run from cron, CI or Kubernetes, or compile into your own Rust service. ```bash brew install faucet-hq/faucet-stream/faucet-cli ``` [Guide](https://faucet-hq.github.io/faucet-stream/) · [Your first pipeline](https://faucet-hq.github.io/faucet-stream/getting-started/first-pipeline.html) · [Connectors](https://faucet-hq.github.io/faucet-stream/reference/connectors.html) · [Config reference](https://faucet-hq.github.io/faucet-stream/reference/config.html) · [Benchmarks](BENCHMARKS.md) · [API docs](https://docs.rs/faucet-stream) --- ## A governed ETL pipeline in one file This pipeline loads orders from a billing API into BigQuery every 15 minutes. Every row passes the governance the analytics team relies on before anything is written. - **Extract and load** - Reads only what changed since the last run, keyed on `updated_at`, with retries. - Merges into BigQuery by order id (`write_mode: upsert`), so an updated order replaces its old row. - **Transform** - Converts keys to snake_case and casts `amount` to a number. - **Govern in flight** - Hashes customer emails before any sink, dead-letter file or lineage event sees them. - Checks every row against quality rules and the contract the analytics team depends on. - Adds new source columns to the table instead of failing (`schema: evolve`). - Allows PII only into sinks marked `residency: eu`, which is checked before the run starts. - Sends rows that fail to a dead-letter file, and lets the rest of the run continue. - **Run and control** - Runs on a 15-minute cron with `faucet schedule`, and skips a run if the previous one is still going. - Caps each run's rows and duration with a budget. - **Observe and alert** - Serves Prometheus metrics on `127.0.0.1:9090/metrics` and sends traces to your OTLP collector. - Sends column-level lineage to OpenLineage, and records each dataset in a catalog for impact analysis. - Watches freshness with an SLA, and learns per-column profiles to spot drift. - Posts failures, SLA breaches, DLQ spikes and column drift to Slack. - **Secure** - Reads credentials from AWS Secrets Manager and GCP Secret Manager, never from the file. ```yaml # orders.yaml version: 1 name: orders_to_warehouse schedule: # `faucet schedule orders.yaml` runs it every 15 minutes cron: "*/15 * * * *" overlap_policy: skip policy: # decided before any data moves version: 1 classifications: - label: pii fields: [customer_email, phone] rules: - name: pii-stays-in-eu when: { label: pii } require: { residency: [eu] } catalog: # every dataset written, for lineage and impact analysis url: sqlite:./faucet-catalog.db lineage: # OpenLineage events with column-level lineage namespace: prod.warehouse include_column_lineage: true transport: { type: http, config: { url: "${env:OPENLINEAGE_URL}" } } sla: # freshness and volume, checked after every run max_staleness_secs: 3600 min_rows_per_run: 1 profiling: # learned per-column baselines; drift is reported, not guessed on_drift: notify budget: # hard ceilings for a single run max_records: 5000000 max_duration_secs: 1800 notifications: - name: data-alerts on: [run_failure, sla_breach, dlq_threshold, profile_drift] channel: type: slack config: { webhook_url: "${aws-sm:prod/faucet#slack_webhook}" } observability: prometheus: listen: "127.0.0.1:9090" # GET /metrics: rows, bytes, latency, errors, DLQ counts (unauthenticated, so bind locally) otel: endpoint: "${env:OTEL_EXPORTER_OTLP_ENDPOINT}" export: [traces, metrics] pipeline: source: # extract: only what changed since the last run type: rest config: base_url: https://api.example.com/v1 path: /orders auth: { type: bearer, config: { token: "${aws-sm:prod/billing-api#token}" } } records_path: $.data[*] pagination: { type: Cursor, next_token_path: $.meta.next_cursor, param_name: cursor } replication_method: { type: Incremental } replication_key: updated_at primary_keys: [id] max_retries: 5 transforms: # transform in flight - type: keys_case config: { mode: snake } - type: cast config: { fields: { amount: float }, on_error: "null" } masking: # PII is masked before any sink, DLQ or lineage event rules: - name: hash-emails match: { fields: [customer_email] } action: { type: hash } quality: # per-record and per-batch assertions record: - { type: not_null, field: id, on_failure: abort } - { type: compare, field: amount, op: gte, value: 0, on_failure: quarantine } batch: - { type: unique, fields: [id], on_failure: quarantine } contract: # the promise made to downstream consumers version: "1.0.0" owner: data-platform on_breach: quarantine allow_extra_fields: true fields: - { name: id, type: string } - { name: status, type: string, enum: [open, paid, refunded] } schema: # new source columns are added to the table on_drift: evolve dlq: # quarantined rows land here; the run keeps going sink: { type: file, config: { path: ./dlq/orders.jsonl } } max_failures_total: 1000 # ...unless too many fail sink: # load: MERGE by id, so updated orders replace old rows type: bigquery attributes: { residency: eu } config: project_id: my-gcp-project dataset_id: analytics table_id: orders write_mode: upsert key: [id] auth: type: service_account_key config: { json: "${gcp-sm:projects/my-gcp-project/secrets/faucet-loader/versions/latest}" } state: # bookmarks, so the next run resumes where this one stopped type: file config: { path: ./.faucet-state } ``` ```bash faucet validate --no-secrets orders.yaml # in CI: grammar, types, policy verdict; no credentials needed faucet validate orders.yaml # also resolves every secret from the secrets managers faucet doctor orders.yaml # probe auth, network and permissions on both ends faucet test tests/orders.yaml # fixture tests of the transforms and checks; touches no real system faucet plan --impact orders.yaml # what will run, and which downstream datasets a schema change breaks faucet run orders.yaml # run once: extract, transform, govern, load faucet schedule orders.yaml # or keep it running on the cron in the file faucet status orders.yaml # one screen: last success, resume point, lag, DLQ backlog; exit 0/1/2 curl -s localhost:9090/metrics # Prometheus metrics while it runs faucet dlq inspect ./dlq/orders.jsonl # why rows were quarantined faucet dlq replay --from ./dlq/orders.jsonl orders.yaml # re-run them once fixed faucet state export orders.yaml -o orders-state.json # back up or move the bookmark; also show / set / reset / import ``` The [`cli/examples/`](cli/examples) directory has runnable configs for common source-to-sink pairs and for each governance feature on its own. **No infrastructure handy?** `./scripts/try-local.sh` builds a light binary, runs a governed demo pipeline on generated data and opens the [web console](https://faucet-hq.github.io/faucet-stream/cookbook/web-console.html), where you can browse runs, datasets and lineage. It needs no Docker and no cloud account ([details](https://faucet-hq.github.io/faucet-stream/getting-started/try-it-locally.html)). ## The data path Every page of records takes the same route. Governance sits between the transform and the write, so nothing reaches a destination until it has passed: ```mermaid flowchart LR S[Source] --> T[Transforms] --> M[Masking] --> Q[Quality] --> C[Contract] --> D[Schema drift] --> K[Sink] Q -. quarantined .-> DLQ[(Dead-letter queue)] C -. breaches .-> DLQ K -. bookmark after durable write .-> ST[(State)] K -. events .-> L([Lineage, metrics, catalog]) ``` Policies are checked before the run starts, and again at run time against the actual values. ## Governance in the data path Most stacks add these as separate tools after the data has landed. In faucet each one is a block in the pipeline file and applies to every connector. | Guardrail | What it does | Guide | |---|---|---| | **PII masking** | Redact, hash, tokenize or partially mask fields, matched by name, pattern or value detector (emails, cards, …). Runs first, so raw PII never reaches a sink, the DLQ or a lineage event. | [masking](https://faucet-hq.github.io/faucet-stream/cookbook/masking.html) | | **Data-quality checks** | 13 per-record and per-batch assertions: not-null, regex, ranges, sets, uniqueness, row counts, JSON Schema. Each can quarantine, abort or warn. | [quality](https://faucet-hq.github.io/faucet-stream/cookbook/quality.html) | | **Data contracts** | A versioned, owned promise about output shape: types, nullability, enums, patterns and bounds, enforced per page. Export it as JSON Schema with `faucet contract`. | [contracts](https://faucet-hq.github.io/faucet-stream/cookbook/contracts.html) | | **Data-flow policies** | Classify columns and declare which sinks each label may reach, for example "PII only to EU sinks" or "salaries only hashed". `validate`, `plan` and `doctor` report violations, and `run` refuses them. | [policies](https://faucet-hq.github.io/faucet-stream/cookbook/policies.html) | | **Schema drift** | Detects added, removed and retyped columns per page, and then evolves the destination, quarantines or fails, according to the policy you set. | [schema drift](https://faucet-hq.github.io/faucet-stream/cookbook/schema-drift.html) | | **Column profiling** | Learns per-column profiles (null rate, type mix, distinct counts, distributions) and reports statistically significant drift, with no thresholds to write. | [profiling](https://faucet-hq.github.io/faucet-stream/cookbook/profiling.html) | | **SLA monitoring** | Declared freshness and volume floors, plus anomaly detection against learned baselines. | [SLA](https://faucet-hq.github.io/faucet-stream/cookbook/sla.html) | | **Lineage and catalog** | OpenLineage events with schema and column-level lineage, sent over HTTP, file or Kafka, and a catalog of every dataset faucet has written. | [lineage](https://faucet-hq.github.io/faucet-stream/cookbook/lineage.html) · [catalog](https://faucet-hq.github.io/faucet-stream/cookbook/catalog.html) | | **Change impact analysis** | With a `catalog:` block, as in the example above, `faucet plan --impact` walks the lineage graph and shows which datasets, contracts, owners and consumers a dropped or retyped column breaks, before the change ships. | [impact](https://faucet-hq.github.io/faucet-stream/cookbook/impact.html) | | **Plan, approve, run** | Runs and template changes can require approval from named approvers. faucet re-plans before it executes, and every step is audited. | [approvals](https://faucet-hq.github.io/faucet-stream/cookbook/approvals.html) | | **Cost and budgets** | Meters rows, bytes, round trips and connector cost signals per run, and enforces hard ceilings with a `budget:` block. | [usage](https://faucet-hq.github.io/faucet-stream/cookbook/usage.html) | | **Verify and roll back** | Compares source and destination content with targeted repair, and undoes a bad load with `faucet rollback `. | [verify](https://faucet-hq.github.io/faucet-stream/cookbook/verify.html) · [rollback](https://faucet-hq.github.io/faucet-stream/cookbook/rollback.html) | | **Secrets** | `${vault:…}`, `${aws-sm:…}`, `${gcp-sm:…}` and `${azure-kv:…}` are resolved at load time and redacted from logs. | [secrets](https://faucet-hq.github.io/faucet-stream/cookbook/secrets.html) | ## Transform in flight - **Record transforms**: `flatten`, `select`, `drop`, `rename_keys`, `keys_case`, `cast`, `set`, `coalesce`, `split`, `join`, `json_parse`, `redact`, `hash`, `cdc_unwrap`, and more ([transforms](https://faucet-hq.github.io/faucet-stream/cookbook/transforms.html)). - **SQL on each page** with embedded DuckDB, for aggregations, filters and joins against lookup tables ([SQL transform](https://faucet-hq.github.io/faucet-stream/cookbook/sql-transform.html)). - **Custom code** in any language that compiles to WebAssembly, sandboxed and run per record ([WASM transforms](https://faucet-hq.github.io/faucet-stream/cookbook/wasm-transforms.html)). faucet transforms data while it moves. Modelling data that has already landed in the warehouse is dbt's job, and the two work well together ([orchestration recipe](https://faucet-hq.github.io/faucet-stream/cookbook/orchestration.html)). ## Reliability you don't have to build | Guarantee | How it works | |---|---| | **Bounded memory** | Sources stream page by page and sinks write each page as it arrives, so memory stays at one batch whatever the volume. | | **Incremental and resumable** | Bookmarks are saved only after the page is durably written, to a file, Redis or Postgres. A crash replays at most the last page. State is versioned: after an upgrade, faucet migrates an older bookmark or refuses one it can't read, instead of silently re-syncing or skipping ([state](https://faucet-hq.github.io/faucet-stream/cookbook/state.html)). | | **Effectively-once** | On supported sinks, a per-page commit token is written atomically with the data, so a resumed run writes no duplicates. This is idempotent at-least-once, not distributed-consensus exactly-once. | | **Upsert and delete** | `write_mode: upsert \| delete` with a key and a delete marker ([upsert](https://faucet-hq.github.io/faucet-stream/cookbook/upsert.html)). | | **Failure handling** | Retries with backoff and `Retry-After`, a circuit breaker, and a dead-letter queue you can inspect and replay. Each sink declares whether a failed batch is all-or-nothing, and `on_batch_error: dlq_all` is refused on sinks where a failed write may have partly landed, so replaying the DLQ can't duplicate rows ([resilience](https://faucet-hq.github.io/faucet-stream/cookbook/resilience.html) · [DLQ](https://faucet-hq.github.io/faucet-stream/cookbook/dlq.html)). | | **Change data capture** | Row-level CDC from PostgreSQL, MySQL, SQL Server, MongoDB, Oracle (LogMiner) and DynamoDB Streams. `faucet mirror` runs a snapshot and hands it off to CDC without gaps, for one table or a whole table set ([replication](https://faucet-hq.github.io/faucet-stream/cookbook/replication.html)). | | **Status and state** | `faucet status` shows each pipeline on one screen: last success or failure, where the next run resumes, how far a CDC or streaming source is behind, the DLQ backlog, and SLA and profiling verdicts. Exit codes 0 / 1 / 2 make it a cron or Nagios check. `faucet state show / set / reset / export / import` moves, resets, backs up or migrates a bookmark safely, including between file, Redis and Postgres stores ([state and status](https://faucet-hq.github.io/faucet-stream/cookbook/state-and-status.html)). | | **Backfills** | Resumable, windowed historical replays with `faucet backfill` ([backfill](https://faucet-hq.github.io/faucet-stream/cookbook/backfill.html)). | | **Observability** | Prometheus metrics and `tracing` / OTLP spans for every source, sink, transform and state operation, including source lag (`faucet_source_lag_seconds`, `_bytes`, `_events`) and API throttling (`faucet_source_throttled_total`, `faucet_source_throttle_wait_seconds`) ([observability](https://faucet-hq.github.io/faucet-stream/operations/observability.html)). | | **Alerts** | Slack, PagerDuty or signed webhooks on run failures, SLA breaches, DLQ thresholds, open circuit breakers and column drift, with deduplication ([notifications](https://faucet-hq.github.io/faucet-stream/cookbook/notifications.html)). | | **Testing** | `faucet test` runs fixture-based tests of a pipeline's transforms and checks offline, and `validate --no-secrets` checks configs in CI without credentials ([testing](https://faucet-hq.github.io/faucet-stream/cookbook/testing.html)). | ## Connectors 43 sources and 35 sinks. Every connector depends only on `faucet-core`, so any source works with any sink. | Category | Sources | Sinks | |---|---|---| | **Databases** | PostgreSQL, MySQL, SQL Server, Oracle, MongoDB, SQLite, DuckDB, Redis, DynamoDB, Spanner | PostgreSQL, MySQL, SQL Server, Oracle, MongoDB, SQLite, DuckDB, Redis, DynamoDB, Spanner | | **CDC** | PostgreSQL, MySQL, SQL Server, MongoDB, Oracle, DynamoDB Streams | Applied through `write_mode: upsert` / `delete` on keyed sinks | | **Warehouses and lakehouses** | BigQuery, Snowflake, Redshift, ClickHouse, Databricks, Delta, Iceberg | BigQuery, Snowflake, Redshift, ClickHouse, Databricks, Delta, Iceberg | | **Object stores and files** | S3, GCS, Azure Blob, SFTP, local files (path, glob or URL); CSV and Parquet *(deprecated: use local files)* | S3, GCS, Azure Blob, SFTP, local files; CSV, Parquet and JSONL *(deprecated: use local files)* | | **Streams and queues** | Kafka, Kinesis, Pub/Sub, NATS, RabbitMQ, SQS | Kafka, Kinesis, Pub/Sub, NATS, RabbitMQ, SQS | | **APIs** | REST, GraphQL, gRPC, XML, Webhook, WebSocket | HTTP | | **Search** | Elasticsearch | Elasticsearch | | **Bridges** | Singer taps (experimental) | Singer targets (experimental), stdout | - **Capabilities per connector** (streaming, resumable state, write modes, delivery guarantee, auth) are in the [connector matrix](https://faucet-hq.github.io/faucet-stream/reference/connectors.html). For help choosing between overlapping connectors, see [choosing a connector](https://faucet-hq.github.io/faucet-stream/reference/choosing.html). - **File formats**: the file and object-store connectors read and write JSONL, JSON, CSV, Excel, XML, Parquet and Avro, and read ORC, chosen per file by extension or set explicitly ([file formats](https://faucet-hq.github.io/faucet-stream/cookbook/file-formats.html)). - **Tier 1** connectors pass the [conformance battery](https://faucet-hq.github.io/faucet-stream/reference/conformance.html) in CI against a real backend or an official emulator. - **SaaS sources** (CRM, payments, ticketing, advertising and analytics APIs) are maintained as declarative templates on the REST and GraphQL engines, not as separate crates. Browse the [Template Hub](https://faucet-hq.github.io/hub) and run one with `faucet run --source / --sink faucet-hq/bigquery` ([guide](https://faucet-hq.github.io/faucet-stream/cookbook/template-hub.html)). ## Performance These numbers are reproducible, and the [methodology](BENCHMARKS.md) includes the caveats. Each workload moves 1M rows on one machine, compared with Meltano 4.4.1 running the equivalent Singer pipeline (measured 2026-10-16): | Workload | Bottleneck | faucet | Meltano | Speed-up | |---|---|---:|---:|---:| | CSV → JSONL | parsing and serialization | 701k rows/s, 20 MiB | 8.7k rows/s, 819 MiB | ~80× | | Postgres → JSONL | row decoding | 191k rows/s | 7.9k rows/s | ~24× | | Postgres → Postgres | destination writes | 117k rows/s (`copy`) | 12.2k rows/s | ~10× | For a realistic database-to-database move, quote **~10×**. Treat 80× as the upper bound, because once the destination is the bottleneck the gap narrows. Run `make bench` to reproduce it on your own machine. ## Run it the way you already run things ```bash faucet run pipeline.yaml # one run, then exit: cron, CI, a Kubernetes Job faucet schedule pipeline.yaml # built-in cron: DST-correct, overlap policies, graceful drain faucet serve # HTTP control plane with a web console ``` - **`faucet run`** runs once and exits, which suits cron, CI and orchestrators like Airflow or Dagster ([orchestration](https://faucet-hq.github.io/faucet-stream/cookbook/orchestration.html)). - **`faucet schedule`** is a built-in cron runtime ([scheduling](https://faucet-hq.github.io/faucet-stream/cookbook/scheduling.html)). - **`faucet serve`** is a long-running control plane. It covers: - a REST API for submitting, polling and cancelling runs ([serve](https://faucet-hq.github.io/faucet-stream/cookbook/serve.html) · [HTTP API](https://faucet-hq.github.io/faucet-stream/reference/http-api.html)) - event triggers on object arrival, webhooks and queue depth ([triggers](https://faucet-hq.github.io/faucet-stream/reference/triggers.html)) - clustered execution ([cluster](https://faucet-hq.github.io/faucet-stream/cookbook/cluster.html)) - an MCP server for agents ([MCP](https://faucet-hq.github.io/faucet-stream/cookbook/mcp.html)) - multi-tenant embedding with per-tenant credentials, hosted OAuth and isolated state ([embedded integrations](https://faucet-hq.github.io/faucet-stream/cookbook/embedded-integrations.html)) - **Deployment**: a [Helm chart](deploy/helm/faucet-stream) and container images are provided ([deploying](https://faucet-hq.github.io/faucet-stream/operations/deploying.html)). ## Embed it in Rust The CLI is a thin layer over a library, and the same engine is available through typed traits: ```rust use faucet_stream::{Pipeline, RestStream, RestStreamConfig, PaginationStyle}; use faucet_stream::sink::file::{FileSink, FileSinkConfig}; #[tokio::main] async fn main() -> Result<(), Box> { let source = RestStream::new( RestStreamConfig::new("https://api.example.com", "/v1/users") .records_path("$.data[*]") .pagination(PaginationStyle::Cursor { next_token_path: "$.meta.next_cursor".into(), param_name: "cursor".into(), }), )?; let sink = FileSink::new(FileSinkConfig::new("./users.jsonl"))?; let result = Pipeline::new(&source, &sink).run().await?; println!("wrote {} records", result.records_written); Ok(()) } ``` - **Compile in only what you use.** Every connector is a Cargo feature, for example `--features source-postgres,sink-bigquery`. - **Write your own connector.** Depend on `faucet-core`, implement `Source` or `Sink`, and publish it as `faucet-source-*` / `faucet-sink-*`. The [library tutorial](https://faucet-hq.github.io/faucet-stream/tutorials/library.html) and the [connector spec](docs/spec/faucet-connector-spec-v0.md) cover the contract, and `faucet-conformance` tests it. ## When not to use faucet - **You need a long-tail SaaS connector today and don't want to write a template.** Airbyte and Meltano have far larger catalogs. - **You want a hosted service that someone else operates.** Use Fivetran or Airbyte Cloud. - **Your main job is modelling data already in the warehouse.** Use dbt: faucet gets the data there clean, and dbt models it. - **You need a never-ending, record-at-a-time stream processor.** Use Redpanda Connect or Vector. faucet runs pipelines as discrete, resumable runs. For detailed comparisons that are honest about where each tool wins, see [vs. Meltano](https://faucet-hq.github.io/faucet-stream/comparison/meltano.html), [vs. Airbyte](https://faucet-hq.github.io/faucet-stream/comparison/airbyte.html) and [vs. Singer](https://faucet-hq.github.io/faucet-stream/comparison/singer.html). ## Install ```bash brew install faucet-hq/faucet-stream/faucet-cli # macOS / Linux curl -LsSf https://github.com/faucet-hq/faucet-stream/releases/latest/download/faucet-cli-installer.sh | sh cargo install faucet-cli # from source cargo add faucet-stream # as a library ``` Prebuilt archives with SHA-256 checksums are on the [releases page](https://github.com/faucet-hq/faucet-stream/releases). The minimum supported Rust version is 1.96. See the [installation guide](https://faucet-hq.github.io/faucet-stream/getting-started/installation.html) for container images and slim builds. Prebuilt binaries cover macOS and Linux. Pin the version a project uses in its `mise.toml` (`faucet init` writes it) and declare the oldest faucet a config needs with `requires_faucet`; see [pinning](https://faucet-hq.github.io/faucet-stream/operations/pinning.html). **Agent skills.** Teach Claude Code, Codex or any Agent Skills reader to write, debug and deploy faucet pipelines with the skills that ship with each release: ```text /plugin marketplace add faucet-hq/faucet-stream /plugin install faucet@faucet ``` Codex and other agents: [agent skills](https://faucet-hq.github.io/faucet-stream/getting-started/agent-skills.html). ## Star history Star History Chart **Using faucet-stream in production?** Open a PR to add your team here — real adopters are the best signal for the next person deciding whether to bet a pipeline on it. ## Project - [Roadmap](https://github.com/orgs/faucet-hq/projects/2): what's being built now and what's next. - [Project structure](PROJECT_STRUCTURE.md): where everything lives in the repository. - [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Security policy](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md) ## License Licensed under either of [Apache License 2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT), at your option.