Kotlin y Java

Una sola biblioteca para los dos lenguajes: una API suspend para Kotlin, y sendBlocking, sendAsync y builders para Java, sobre java.net.http.HttpClient. Sus únicas dependencias son la biblioteca estándar de Kotlin y las corrutinas.

Paquete
app.honk-me:sdk Próximamente
Código fuente
github.com/honk-me/honk-kotlin · Licencia MIT
Requisitos
JDK 17 o posterior.

La publicación en Maven Central está a la espera del namespace de Sonatype. Mientras tanto, compílala desde el repositorio (./gradlew publishToMavenLocal) o usa cURL.

Instalación

Todo está en el paquete 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>

Quien tenga una clave de ingesta (honk_…) puede enviar mensajes a su proyecto. Guárdala en servidores, tareas y secretos de CI, nunca en un navegador ni en una app móvil o de escritorio.

Enviar un evento

Crea un solo Honk por aplicación y ciérralo al apagarla.

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());

Receta: una solicitud de cliente con Spring Boot o Ktor

La biblioteca no depende de Spring; esto es código normal en un servicio de Spring. Envía después de confirmar la transacción, para que el cliente nunca espere a la red.

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}")
    }
}

La escala Honk e incidentes

En Java, Message.longHonk(…) sustituye a long, que es una palabra reservada.

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"))

Errores

Todas las excepciones son no comprobadas y extienden HonkException, con isRetryable y la clave de idempotencia con la que reintentar.

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;
}

Reintentos que nunca envían dos veces

  • Cada envío lleva una Idempotency-Key: la tuya o un UUIDv7 nuevo. La misma clave se reutiliza en cada reintento y, durante 24 horas, Honk responde a una repetición con el id original y duplicate: true, así que una respuesta perdida nunca crea un segundo mensaje.
  • Solo se reintentan los errores de red, los tiempos de espera agotados, 429 y 5xx, con backoff exponencial y jitter completo, y nunca antes del Retry-After del servidor. Las demás respuestas 4xx no se reintentan nunca: hay que corregir la solicitud.
  • Cada intento se corta a los 5 segundos y todo se detiene a los 30. Una espera que superaría ese plazo, como una cuota diaria que se reinicia a medianoche, falla al momento y te dice cuándo reintentar.
  • Los campos se validan antes de enviar, y todos los errores de validación se devuelven juntos.