confish
Official Rust SDK for confish — typed configuration, actions, logs, feeds, and webhook verification.
- Async-first, built on
tokio+reqwest - Typed configuration via the standard
serde::Deserializegeneric —client.config().fetch::<MyConfig>().await? - Live feeds with declarative upserts and optional TTLs
- Long-running action consumer with
CancellationTokenand bounded concurrency - HMAC-SHA256 webhook verification that returns the parsed payload
Install
[]
= "0.2"
By default the SDK uses rustls. For native TLS, opt in:
= { = "0.2", = false, = ["native-tls"] }
Requires Rust 1.86+.
Quick start
use Client;
use Deserialize;
async
Reading and writing config
// GET /c/{env_id}
let config: MyConfig = client.config.fetch.await?;
// PATCH — only listed fields change
let updated: MyConfig = client.config.update.await?;
// PUT — replaces everything; omitted fields reset to defaults
let new_config: MyConfig = client.config.replace.await?;
Write access must be enabled in environment settings before
updateandreplacewill work.
Feeds
A feed holds your environment's live state — active jobs, crawl results, open incidents — keyed by an external ID you choose. client.feed(slug) returns a bound handle; construction performs no HTTP.
use FeedItem;
use ;
use Duration;
let crawls = client.feed;
// PUT — create-or-replace by external ID, with an optional TTL
crawls.set.await?;
// GET — live (non-expired) items, newest first
let items: = crawls.list.await?;
// DELETE — idempotent; deleting an item that's already gone succeeds
crawls.delete.await?;
set is declarative: the item's data becomes exactly what you send, and the TTL becomes exactly what you pass. Passing None for ttl makes the item permanent — it clears any TTL set by a previous call. TTLs are sent as whole seconds and must be between 1 second and 30 days; external_id must be 255 characters or fewer.
Replacing the whole feed
replace is a collection PUT built for sync-style cron jobs that push their full dataset in one request — the feed becomes exactly the items you send. Existing external IDs update in place, new ones are created, and anything absent is deleted; an empty slice clears the feed. It's all-or-nothing: duplicate external IDs, payloads over the plan's item cap, or any schema-invalid item are rejected with Error::Validation and nothing is written.
use FeedItemInput;
use json;
use Duration;
let result = client.feed.replace.await?;
println!;
An unknown feed slug returns Error::NotFound. A full feed or an item that fails the feed's schema returns Error::Validation.
Logging
use LogLevel;
use json;
client.logs.info.await?;
client.logs.error.await?;
// Or with an explicit level:
let log_id = client.logs.write.await?;
Levels (the full RFC 5424 set): Debug, Info, Notice, Warning, Error, Critical, Alert, Emergency. Because the levels follow RFC 5424 (syslog), they map cleanly onto the level vocabularies of the log and tracing crates.
Batch writes
write_batch sends up to 100 entries in a single request and returns their IDs in input order. Each entry carries a level, a message, optional context, and an optional ISO 8601 timestamp — set one to backdate entries you buffered yourself; leave it off and the server stamps the entry on receipt.
use ;
use json;
let ids = client.logs.write_batch.await?;
More than 100 entries fails fast client-side — no request is made — so chunk larger backfills. An empty slice sends nothing and returns no IDs.
Use as a tracing layer
If you already instrument your code with the tracing crate, you can ship those events to confish without touching a single call site. Enable the tracing feature:
[]
= { = "0.3", = ["tracing"] }
Then add the layer to your subscriber stack. Building the layer requires a running tokio runtime (it spawns a background sender), so build it inside #[tokio::main] — and hold on to the guard for as long as you log:
use TracingLayer;
use *;
async
How it behaves:
- Non-blocking. Emitting an event only pushes onto a bounded in-memory queue (default capacity 1000) — it never blocks and never panics. A background task batches entries and sends them: at 50 entries or every 5 seconds, whichever comes first, chunked to at most 100 entries per request.
- Level mapping.
tracinghas five levels, confish follows RFC 5424:TRACE→debug,DEBUG→debug,INFO→info,WARN→warning,ERROR→error. - Fields become context.
info!(job = "db-backup", attempt = 2, "Job started")produces the messageJob startedwith context{"job": "db-backup", "attempt": 2}. Timestamps are captured when you emit the event, not when the batch is sent. Span fields are not captured in this version — event fields only. - Overflow drops the newest. When the queue is full the incoming event is dropped rather than blocking you;
guard.dropped_count()tells you how many went missing. - Errors never come back. Delivery failures are retried per the client's retry policy, then counted in
dropped_count()and discarded — the layer never emitstracingevents of its own, so it can't feed back into itself. - Shutdown. Dropping the guard flushes whatever is buffered, blocking for at most
shutdown_timeout(default 5 seconds). On a current-thread runtime preferguard.shutdown().await, which flushes without blocking the runtime.
Tune the knobs on the builder: capacity, flush_after, flush_interval, and shutdown_timeout.
Actions
The action consumer polls for pending actions, acknowledges them, runs your handler, and reports completion or failure — including idempotent skip if another consumer claimed the action first.
use ;
use json;
use Duration;
use CancellationToken;
let cancel = new;
let cancel_clone = cancel.clone;
spawn;
client
.actions
.consume
.await?;
What happens automatically:
- Returning
Ok(Some(value))completes the action withvalueasresult. - Returning
Ok(None)completes with no result. - Returning
Err(_)fails the action with{"error": <message>}. - A
409 Conflicton ack is silently skipped — safe to run multiple consumers. - Cancelling the token halts new work and waits for in-flight handlers to finalize.
- After 3 consecutive empty polls the loop doubles its sleep up to
max_poll_interval, resetting topoll_intervalthe moment any action is processed. Idle consumers make ~240 requests/hour by default.
You can also drive the lifecycle manually:
let actions = client.actions.list.await?;
client.actions.ack.await?;
client.actions.progress.await?;
client.actions.complete.await?;
client.actions.fail.await?;
To extract typed params:
if let Some = action.params.clone
Webhook verification
verify checks the signature and parses the payload in one operation — the Stripe pattern. On success you get the parsed Payload; on failure a WebhookError that says why (InvalidSignature vs TimestampOutsideTolerance), so the failure mode can't be ignored.
use ;
async
verify uses constant-time comparison and rejects timestamps older than 5 minutes by default. Pass VerifyOptions { tolerance: Duration::ZERO, .. } to disable. Always pass the raw, unparsed body — re-serializing parsed JSON breaks verification.
Errors
use Error;
match client.config..await
Error::is_conflict() is a convenience for action consumers.
By default the client retries 429 (honoring Retry-After) and 5xx responses up to twice. Tune via the builder.
Builder options
let client = builder
.base_url // override for self-hosted
.user_agent
.max_retries
.max_retry_delay
.http_client // inject your own
.build?;
License
MIT