Rust
The honk-me crate is an async client on Tokio and reqwest with rustls, so no OpenSSL. The blocking feature adds a synchronous client for scripts and CLIs.
- Package
-
honk-mePublished - Source
- github.com/honk-me/honk-rust · MIT License
- Requirements
- Rust 1.85 or later.
Install
For synchronous code: cargo add honk-me --features blocking.
cargo add honk-me
cargo add tokio --features macros,rt-multi-thread # if you don't have a runtime yet Anyone with an ingestion key (honk_…) can post to its project. Keep it on servers, in jobs and in CI secrets, never in a browser, mobile or desktop app.
Send an event
Or explicitly: Honk::new("https://honk-me.app", key)?. Honk is cheap to clone: create one and share it, so connections stay alive.
use honk_me::Honk;
#[tokio::main]
async fn main() -> honk_me::Result<()> {
let honk = Honk::from_env()?; // HONK_URL, HONK_KEY (+ HONK_SOURCE, HONK_ENVIRONMENT, HONK_CHANNEL)
honk.beep("Backup finished", "nightly pg_dump took 42 s").await?;
Ok(())
} Recipe: a customer request on your phone
One group per request and your own idempotency key, so a retried webhook never buzzes twice.
use honk_me::{Category, Message, Priority, Severity};
let msg = Message::new(format!("{} asked for a quote: {}", req.name, req.summary))
.title("New quote request")
.severity(Severity::LOUD)
.priority(Priority::High)
.category(Category::Customers)
.group_key(format!("requests/{}", req.id))
.url(format!("https://shop.example.com/admin/requests/{}", req.id)); // https only, "Open link"
honk.send(&msg, format!("request-{}", req.id).as_str()).await?; The Honk scale and incidents
Every helper returns a PendingSend: chain more fields, then .await it.
use honk_me::Priority;
honk.loud("Disk 91% full", "db-1 /var is at 91%")
.group_key("disk/db-1/var")
.priority(Priority::High)
.metadata("used_percent", 91)
.await?;
honk.problem("db/backup", "Backup failed", "pg_dump exited with 1").await?;
honk.recovery("db/backup", "Backup OK", "pg_dump finished").await?; The blocking client
It runs the async client on a private single-threaded runtime, with the same retries. Don’t call it from inside an async runtime.
// Cargo.toml: honk-me = { version = "0.2", features = ["blocking"] }
use honk_me::blocking::Honk;
let honk = Honk::from_env()?;
honk.beep("Backup finished", "nightly pg_dump took 42 s").send()?;
honk.send(&honk_me::Message::new("Imported 1 204 rows"), None)?; Errors
Each Error variant carries a Failure with the status, the code, every invalid field and retry_after.
use honk_me::Error;
match honk.send(&msg, "order-1042-failed").await {
Ok(_) => {}
Err(Error::Validation(f)) => tracing::error!(?f.fields, "a bug: don't retry"),
Err(e) if e.is_retryable() => queue.retry_later(e.idempotency_key(), e.retry_after()),
Err(e) => return Err(e.into()),
} Retries that never send twice
- Every send carries an
Idempotency-Key: yours, or a fresh UUIDv7. The same key is reused on every retry, and within 24 hours Honk answers a replay with the original id andduplicate: true, so a lost response never turns into a second message. - Only network errors, timeouts,
429and5xxare retried, with exponential backoff and full jitter, never sooner than the server’sRetry-After. Other4xxresponses are never retried; fix the request instead. - Each attempt times out after 5 seconds and everything stops after 30. A wait that would cross that deadline, like a daily quota that resets at midnight, fails right away and tells you when to retry.
- Fields are validated before sending, and all invalid fields are reported together.