mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
An op learns its address from zip.OpOf and zip.AddressOf, so the claim table, the five declaring verbs and the address bookkeeping are gone. A host that declares one of these addresses itself now gets a Build conflict from zip. Co-authored-by: Hanzo Dev <[email protected]>
103 lines
4.4 KiB
Go
103 lines
4.4 KiB
Go
package o11y
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/hanzoai/o11y/pkg/factory"
|
|
"github.com/hanzoai/o11y/pkg/http/render"
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// The service-health probe group — liveness, readiness, health — dispatches on
|
|
// the native zip/fiber router instead of the embedded gorilla/mux tree. It is
|
|
// the first route group moved off mux at the Hanzo-owned mount seam: the three
|
|
// probes are one cohesive group backed by the ONE runtime health handler
|
|
// (factory.Handler), so the health computation keeps its single home while the
|
|
// path→handler dispatch becomes native. Every other /v1/o11y route still reaches
|
|
// the mux tree through the delegation wildcard (see Mount); the staged migration
|
|
// moves further groups here.
|
|
//
|
|
// Routing model: mountHealth registers the probes AHEAD of the /v1/o11y/*
|
|
// wildcard so Fiber's in-order match gives them precedence. Until the runtime
|
|
// registers its handler via SetHealth, each probe falls through (Next) to the
|
|
// wildcard, so behavior is identical to the delegated path — the native
|
|
// dispatch activates the moment SetHealth is wired, with no route change.
|
|
|
|
var (
|
|
healthMu sync.RWMutex
|
|
healthSource factory.Handler
|
|
)
|
|
|
|
// SetHealth registers the runtime's service-health handler so the liveness,
|
|
// readiness and health probes under /v1/o11y/* dispatch on the native
|
|
// router. The embedding host calls it after constructing the runtime, passing
|
|
// factory.NewHandler(runtime.Registry). Safe for concurrent use; pass nil to
|
|
// unset (the probes then fall through to the delegated runtime handler).
|
|
func SetHealth(h factory.Handler) {
|
|
healthMu.Lock()
|
|
healthSource = h
|
|
healthMu.Unlock()
|
|
}
|
|
|
|
func getHealth() factory.Handler {
|
|
healthMu.RLock()
|
|
h := healthSource
|
|
healthMu.RUnlock()
|
|
return h
|
|
}
|
|
|
|
// mountHealth registers the probe group. The paths are the SAME literals the
|
|
// runtime registers, so native dispatch and the fall-through answer the same
|
|
// request — one spelling per probe, not two, and each probe's fall-through is now
|
|
// the runtime's handler for that probe's OWN address rather than for the whole
|
|
// surface.
|
|
func mountHealth(app *zip.App) {
|
|
app.Raw(http.MethodGet, o11yRoot+"/livez", livez(o11yRoot+"/livez"))
|
|
app.Raw(http.MethodGet, o11yRoot+"/healthz", probe(o11yRoot+"/healthz", func(h factory.Handler) http.HandlerFunc { return h.Healthz }))
|
|
app.Raw(http.MethodGet, o11yRoot+"/readyz", probe(o11yRoot+"/readyz", func(h factory.Handler) http.HandlerFunc { return h.Readyz }))
|
|
}
|
|
|
|
// The fall-through is the runtime's handler for the probe's own address, reached
|
|
// through the same hatch every un-typed route uses (hatch.go).
|
|
//
|
|
// It used to be c.Next(), which worked only because a /v1/o11y/* catch-all was
|
|
// registered after the probes and caught whatever they declined. Every route is
|
|
// named now and the catch-all is gone, so c.Next() would fall through to a 404
|
|
// — the probes would start FAILING the moment the deployment had not called
|
|
// SetHealth yet, which is exactly when a liveness probe matters most. Naming the
|
|
// destination is both the fix and the honest statement of what "fall through"
|
|
// always meant here.
|
|
|
|
// livez reports process liveness. factory.Handler.Livez renders an empty success
|
|
// envelope with 200 unconditionally, so it is rendered natively here through the
|
|
// shared render types rather than bridged. Falls through to the runtime when no
|
|
// handler is set.
|
|
func livez(path string) zip.Handler {
|
|
delegate := hatch(http.MethodGet, path)
|
|
return func(c *zip.Ctx) error {
|
|
if getHealth() == nil {
|
|
return delegate(c)
|
|
}
|
|
return c.JSON(http.StatusOK, render.SuccessResponse{Status: render.StatusSuccess.String()})
|
|
}
|
|
}
|
|
|
|
// probe dispatches a stateful health check (healthz, readyz) through the runtime
|
|
// handler selected by sel. The check reads the service registry, so its body
|
|
// stays in factory.Handler (one home) and is reached over the net/http bridge;
|
|
// the routing is native. Falls through to the runtime when no handler is set.
|
|
//
|
|
// Both sources are net/http handlers, so the choice between them is one pick on
|
|
// the far side of ONE bridge rather than two handlers with a branch in front —
|
|
// and the state is read once, where the answer is chosen, so a SetHealth landing
|
|
// mid-request cannot make the branch and the call disagree.
|
|
func probe(path string, sel func(factory.Handler) http.HandlerFunc) zip.Handler {
|
|
return bridge(func() http.Handler {
|
|
if h := getHealth(); h != nil {
|
|
return sel(h)
|
|
}
|
|
return answer(http.MethodGet, path)
|
|
})
|
|
}
|