-
Notifications
You must be signed in to change notification settings - Fork 0
Events
The event bus lets application code and extensions observe the framework without being wired into it.
import rxevent "github.com/kryovyx/rextension/event"
app.EventBus().Subscribe(rxevent.EventTypeRouterRequestHandled, func(e rxevent.Event) {
ev, ok := rxevent.As[rxevent.RouterRequestHandledEvent](e)
if !ok {
return
}
app.Logger().Info("%s %s → %d in %v", ev.Request.Method, ev.RoutePattern, ev.Status, ev.Duration)
})Subscribe before Run, or from an extension's OnInitialize.
| Constant | Type string | Fired when |
|---|---|---|
EventTypeRouterInitialized |
router.initialized |
a router's trie is built |
EventTypeRouterRouteRegistered |
router.route.registered |
a route is added |
EventTypeRouterRequestIncoming |
router.request.incoming |
a request arrives, after matching |
EventTypeRouterRequestHandled |
router.request.handled |
a request finishes |
EventTypeRouterUnresolvedRequest |
router.request.unresolved |
no route matched — 404 or 405 |
Every payload embeds RouterEvent, so ev.Name() gives the router name.
type RouterRequestHandledEvent struct {
RouterEvent
Request *http.Request
ResponseWriter http.ResponseWriter
Duration time.Duration
Status int // what was actually answered
BytesWritten int64
RoutePattern string // "/users/{id}", BaseURL already stripped
}The router wraps the ResponseWriter for matched routes so Status and
BytesWritten are real. Status is 200 when the handler wrote a body without
calling WriteHeader, matching net/http.
Use
RoutePattern, neverRequest.URL.Path, as a metric label. Labelling by path makes every distinct URL its own time series, which for a parameterized route is unbounded and attacker-controlled: an unauthenticated client requesting/users/1,/users/2, … grows the registry without limit.
RoutePattern is empty when no route matched. Note that the 404, 405 and
OPTIONS paths do not emit a handled event — they emit
router.request.unresolved instead.
The default bus has a 1024-event queue and 4 workers.
Emit never blocks. It is called from the request path, so blocking would
let one slow subscriber apply backpressure to every request. When the queue is
full, events are dropped.
Dispatch is asynchronous, on worker goroutines, in no guaranteed order relative to the emitting code.
Drops are countable:
if dc, ok := app.EventBus().(rxevent.DropCounter); ok {
gauge.Set(float64(dc.Dropped()))
}Type-assert rather than requiring it.
This is the rule that matters, and it is why several things in this ecosystem are middleware rather than subscribers:
- An in-flight request gauge cannot be a subscriber. It can lose an increment and its matching decrement independently, and the gauge drifts permanently. It belongs in middleware.
-
A route index cannot be built from
router.route.registered. That event is delivered asynchronously and races the synchronous startup path, so a subscriber sees an arbitrary subset of routes. Extensions use per-route middleware orRouteValidator, both of which hand over the complete table at a defined moment. - Anything billed, audited or alerted on wants middleware.
Events are right for observation that tolerates loss: access logging, debug tracing, cache warming, an approximate dashboard counter.
type CacheInvalidated struct {
rxevent.BaseEvent
Key string
}
app.EventBus().Emit(CacheInvalidated{
BaseEvent: rxevent.NewBaseEvent(ctx, "cache.invalidated", "mycache"),
Key: key,
})Use dot-delimited types prefixed with your own domain, matching the router.*
convention.
The bus is created by New and closed at the very end of shutdown — after
OnStop and OnShutdown, so a shutdown hook can still emit. Do not close it
yourself.
Before v0.3.0
Closewas called nowhere in the workspace, so every Rex instance leaked its four worker goroutines for the life of the process.
rex — Restful Extended eXperience · MIT · © 2026 Kryovyx · pre-1.0 (alpha), interfaces may change
Getting started
Core
Operating
Ecosystem