Kotlin and Java

One library for both languages: a suspend API for Kotlin, and sendBlocking, sendAsync and builders for Java, on java.net.http.HttpClient. Its only dependencies are the Kotlin standard library and coroutines.

Package
app.honk-me:sdk Coming soon
Source
github.com/honk-me/honk-kotlin · MIT License
Requirements
JDK 17 or later.

The Maven Central release waits for the Sonatype namespace. Until then, build it from the repository (./gradlew publishToMavenLocal) or use cURL.

Install

Everything is in the package app.honkme.

build.gradle.kts
// build.gradle.kts
dependencies { implementation("app.honk-me:sdk:0.1.0") }
pom.xml
<!-- pom.xml -->
<dependency>
  <groupId>app.honk-me</groupId>
  <artifactId>sdk</artifactId>
  <version>0.1.0</version>
</dependency>

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

Create one Honk per application and close it on shutdown.

Kotlin
import app.honkme.Honk

val honk = Honk.fromEnvironment()                 // HONK_URL, HONK_KEY (+ HONK_SOURCE, HONK_ENVIRONMENT, HONK_CHANNEL)
honk.beep("Backup finished", "nightly pg_dump took 42 s")   // suspend
Java
import app.honkme.Honk;
import app.honkme.Message;

Honk honk = Honk.fromEnvironment();
honk.sendBlocking(Message.beep("Backup finished", "nightly pg_dump took 42 s").build());

Recipe: a customer request with Spring Boot or Ktor

The library has no Spring dependency; this is plain code in a Spring service. Send after the transaction commits, so the customer never waits for the network.

Spring Boot
@Configuration
class HonkConfig {
    @Bean(destroyMethod = "close")
    Honk honk(@Value("${honk.url}") String url, @Value("${honk.key}") String key) {
        return Honk.builder().url(url).key(key).build();          // one client, keep-alive
    }
}

@Service
class CustomerRequestNotifier {
    private static final Logger log = LoggerFactory.getLogger(CustomerRequestNotifier.class);
    private final Honk honk;

    CustomerRequestNotifier(Honk honk) { this.honk = honk; }

    // After the request is saved: never block the customer's request on the network.
    @TransactionalEventListener
    void on(CustomerRequestCreated event) {
        CustomerRequest r = event.request();
        Message message = Message.light("New request: " + r.subject(), r.name() + " (" + r.company() + ") asked: " + r.body())
            .priority("high")                                     // push right away
            .category(Category.CUSTOMERS)
            .channel("requests")
            .groupKey("requests/" + r.id())                       // one group per request
            .url("https://shop.example.com/admin/requests/" + r.id())   // https only
            .meta("request_id", String.valueOf(r.id()))
            .build();
        honk.sendAsync(message, "request-" + r.id())              // same request, same key
            .exceptionally(e -> { log.warn("honk: {}", e.getMessage()); return null; });
    }
}
Kotlin
scope.launch {
    honk.light("New request: ${r.subject}", "${r.name} (${r.company}) asked: ${r.body.take(2000)}") {
        priority(Priority.HIGH)
        category(Category.CUSTOMERS)
        groupKey("requests/${r.id}")
        url("https://shop.example.com/admin/requests/${r.id}")
        idempotencyKey("request-${r.id}")
    }
}

The Honk scale and incidents

In Java, Message.longHonk(…) stands in for long, which is a keyword.

honk.loud("Disk 91%", "/var on app-01") { groupKey("disk/app-01/var") }
honk.problem("db/backup", "Backup failed", "pg_dump exited with 1")      // a long honk by default
honk.recovery("db/backup", "Backup OK", "pg_dump finished in 41 s")      // a beep by default
honk.send(Message("Imported 1 204 rows", severity = Severity.BEEP, channel = "imports"))

Errors

All exceptions are unchecked and extend HonkException, with isRetryable and the idempotency key to retry with.

try {
    honk.sendBlocking(Message.longHonk("Payment failed", "Stripe declined order 1042").groupKey("payments/stripe").build());
} catch (HonkValidationException e) {
    log.error("bug: {}", e.getFields());
} catch (HonkException e) {
    if (e.isRetryable()) queue.retryLater(e.getIdempotencyKey(), e.getRetryAfter());
    else throw e;
}

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.