-
Notifications
You must be signed in to change notification settings - Fork 0
Events
type Event interface {
Type() string
Context() context.Context
}
type EventBus interface {
Subscribe(eventType string, handler EventHandler)
Emit(event Event)
SetLogger(logger BusLogger)
Close()
}Import github.com/kryovyx/rextension/event directly; do not depend on
rex/event.
Subscribe from OnInitialize, and use event.As to recover the concrete type:
import rxevent "github.com/kryovyx/rextension/event"
r.EventBus().Subscribe(rxevent.EventTypeRouterRequestHandled, func(e rxevent.Event) {
ev, ok := rxevent.As[rxevent.RouterRequestHandledEvent](e)
if !ok {
return
}
log.Info("%s %s → %d (%s)", ev.Request.Method, ev.RoutePattern, ev.Status, ev.Duration)
})| Constant | Type string | Emitted when |
|---|---|---|
EventTypeRouterInitialized |
router.initialized |
a router's trie is built |
EventTypeRouterRouteRegistered |
router.route.registered |
a route is added to a router |
EventTypeRouterRequestIncoming |
router.request.incoming |
an HTTP request arrives |
EventTypeRouterRequestHandled |
router.request.handled |
a request finishes handling |
EventTypeRouterUnresolvedRequest |
router.request.unresolved |
no route matched |
Every one embeds RouterEvent, so ev.Name() gives the router name.
The richest payload, and the one worth reading carefully:
type RouterRequestHandledEvent struct {
RouterEvent
Request *http.Request
ResponseWriter http.ResponseWriter
Duration time.Duration
Status int // captured by a wrapper the router installs
BytesWritten int64
RoutePattern string // "/users/{id}", not "/users/42" — BaseURL stripped
}Status is 200 when the handler wrote a body without calling WriteHeader,
matching what net/http does.
RoutePatternis what a metric label must use. Labelling byRequest.URL.Pathmakes 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 metric registry without limit.
It is empty when no route matched.
type RouterRouteRegisteredEvent struct {
RouterEvent
Route Route // Method + Path
BaseURL string // normalised: no trailing slash, "" for the root
}BaseURL is carried because a subscriber that needs the URL a route is served
at has to combine Path with the prefix its router mounts it under, and the
route does not know that prefix.
Emit never blocks. Emit is called from the request path, so blocking would
let one slow subscriber apply backpressure to every request. The default bus has
a bounded queue and a worker pool; when the queue is full, events are
dropped.
Dispatch is asynchronous. Handlers run on worker goroutines, in no guaranteed order relative to the code that emitted.
Drops are observable:
if dc, ok := bus.(rxevent.DropCounter); ok {
gauge.Set(float64(dc.Dropped()))
}Type-assert rather than requiring DropCounter — a bus that never drops has no
reason to implement it.
Because delivery is lossy and asynchronous:
- An in-flight request gauge cannot be an event 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 races the synchronous startup path, so an extension collecting routes from it sees an arbitrary subset. UsePerRouteMiddlewareorRouteValidator, both of which hand you the complete table. - Anything billed, audited or alerted on wants middleware, not a subscriber.
Events are right for observation that tolerates loss: access logging, debug tracing, cache warming, a dashboard counter that is allowed to be approximate.
type CacheInvalidated struct {
rxevent.BaseEvent
Key string
}
func NewCacheInvalidated(ctx context.Context, key string) CacheInvalidated {
return CacheInvalidated{
BaseEvent: rxevent.NewBaseEvent(ctx, "cache.invalidated", "mycache"),
Key: key,
}
}
r.EventBus().Emit(NewCacheInvalidated(ctx, key))Use dot-delimited type strings, prefixed with your extension's domain, matching
the router.* convention.
The bus is owned by the Rex instance and closed at the very end of shutdown —
after OnStop and OnShutdown, so a shutdown hook can still emit. Do not close
it yourself.
rextension — the Rex extension contract · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Building an extension
Contracts
Reference
Ecosystem