Swift
The HonkMe package is a Swift 6.2 client with strict concurrency, async/await and no dependencies. It runs on macOS, iOS and Linux, for server-side Swift, tools and CI.
- Package
-
HonkMePublished - Source
- github.com/honk-me/honk-swift · MIT License
- Requirements
- Swift 6.2; macOS 14+, iOS 17+ or Linux.
Install
In Xcode: File ▸ Add Package Dependencies and paste https://github.com/honk-me/honk-swift.
// Package.swift
.package(url: "https://github.com/honk-me/honk-swift.git", from: "0.1.0"),
// target:
.product(name: "HonkMe", package: "honk-swift"), 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
Never embed an ingestion key in an iOS or macOS app: anything shipped inside an app can be extracted. An app that needs to notify you should call your own backend, which then calls Honk.
import HonkMe
let honk = try Honk.fromEnvironment() // HONK_URL, HONK_KEY (+ HONK_SOURCE, HONK_ENVIRONMENT, HONK_CHANNEL)
try await honk.beep("Backup finished", "nightly pg_dump took 42 s") Recipe: a customer request with Vapor
Message is Sendable, so you can build it in the handler and send it off the request path. Create one Honk at startup and share it: it owns a keep-alive URLSession.
import HonkMe
import Vapor
func routes(_ app: Application, honk: Honk) {
app.post("quote") { req async throws -> HTTPStatus in
let quote = try req.content.decode(QuoteRequest.self)
try await quote.save(on: req.db) // store it first
let id = try quote.requireID()
// Message is Sendable: build it here, send it off the request path.
let message = Message(
"\(quote.name) (\(quote.company)) asked: \(quote.body.prefix(2000))",
title: "New request: \(quote.subject)".prefix(150).description,
severity: .light,
priority: .high, // push right away
category: .customers,
channel: "requests",
groupKey: "requests/\(id)", // one group per request
url: "https://shop.example.com/admin/requests/\(id)", // https only
metadata: ["request_id": .string("\(id)")]
)
let logger = req.logger
Task {
do { try await honk.send(message, idempotencyKey: "request-\(id)") } // same request, same key
catch { logger.warning("honk: \(error)") }
}
return .accepted
}
} The Honk scale and incidents
Helpers take a closure that edits the message.
try await honk.loud("Disk 91%", "/var on app-01") { $0.groupKey = "disk/app-01/var" }
try await honk.problem(groupKey: "db/backup", "Backup failed", "pg_dump exited with 1") // a long honk by default
try await honk.recovery(groupKey: "db/backup", "Backup OK", "pg_dump finished in 41 s") // a beep by default Errors
HonkError cases carry a Failure with the status, the code, every invalid field and retryAfter; isRetryable tells you what to do.
do {
try await honk.long("Payment failed", "Stripe declined order 1042") { $0.groupKey = "payments/stripe" }
} catch HonkError.validation(let failure) {
logger.error("bug: \(failure.fields)")
} catch let error as HonkError where error.isRetryable {
queue.retryLater(key: error.failure?.idempotencyKey)
} 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.