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-me Published
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.

Terminal
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 and duplicate: true, so a lost response never turns into a second message.
  • Only network errors, timeouts, 429 and 5xx are retried, with exponential backoff and full jitter, never sooner than the server’s Retry-After. Other 4xx responses 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.