Management endpoints
The public management package provides an opt-in, standard-library-first
health subsystem. It has no global registry and mounts no hidden routes.
For generated applications, exposure must be declared explicitly on the application marker:
// @import { Application } from "github.com/spice-framework/spice/annotation/core"// @import { Enable } from "github.com/spice-framework/spice/annotation/management"
// @Application// @Enable(expose=["health", "liveness", "readiness", "info", "metrics", "configprops", "modules"], access="loopback")func main() { os.Exit(spiceapp.Main(os.Args[1:]))}Valid names are health, liveness, readiness, info, metrics,
configprops, and modules. The optional access setting is either public
or loopback; it defaults to loopback. Public exposure is an explicit
opt-in and should be placed behind an independently authenticated management
listener.
The compiler rejects unknown or duplicate names with source positions,
normalizes order deterministically, and verifies that the selected application
graph owns the required *http.ServeMux. The generated handler registers
exactly the requested routes directly on that mux. Metrics are constructed
only when metrics is exposed. Importing management or listing it in
go.mod never enables endpoints.
Each management.Check has a stable name, optional module import path, one or
more explicit groups, and a caller-owned func(context.Context) error probe.
management.New rejects invalid names, nil probes, missing or unknown groups,
and duplicate names within a group. It copies and sorts checks so report order
does not depend on registration order.
Reports expose only UP or DOWN, check name, and module ownership. The
underlying error is never included in JSON. Canceled request contexts mark
unexecuted probes down. Empty groups are up, which makes optional liveness or
readiness groups explicit rather than synthesized from unrelated dependencies.
management.LifecycleChecks adapts generated Application.State():
- constructed, starting, ready, and stopping are live;
- only ready accepts traffic;
- stopped, failed, and invalid are not live.
An isolated handler serves:
| Method and path | Contract |
|---|---|
GET /actuator/health |
broad health report |
GET /actuator/health/liveness |
process liveness report |
GET /actuator/health/readiness |
traffic readiness report |
GET /actuator/info |
caller-owned copied string metadata |
GET /actuator/metrics |
generated-route HTTP metrics when a collector is supplied |
GET /actuator/configprops |
generated configuration key/type/module/value/provenance metadata with mandatory secret redaction |
GET /actuator/modules |
generated spice.modules/v1 module, API, dependency-edge, and unassigned-package canvas |
Down reports use HTTP 503; up reports and info use HTTP 200. Responses use the
same secure JSON writer as generated controllers. The default base path is
/actuator; a custom path must be a clean absolute path below /.
With access="loopback", every management route requires the direct TCP peer
address to be IPv4 or IPv6 loopback and otherwise returns an RFC 9457 403
problem. The check deliberately ignores Forwarded, X-Forwarded-For, and
similar headers: trusting those headers without a configured trusted-proxy
boundary would let a remote caller forge loopback provenance. This policy
protects only management routes; it does not invent authentication semantics
for application routes.
configprops is never part of the runtime default set and is generated only
when explicitly allowlisted. Generation combines the exact schema and resolved
snapshot after configuration resolution. Each property reports its key, kind,
module, resolution state, source, default provenance, and safe value. Secret
values are always <redacted>; raw secret values never enter the report.
Schema/snapshot mismatches fail application construction, and the handler
copies the report before serving it.
modules is also explicit opt-in. The compiler carries its already-validated
Modulith graph into rendering, and generated Go constructs the runtime report
from stable module IDs, owned packages, named interfaces, allowed
dependencies, observed import edges, and unassigned packages. The runtime does
not scan packages or import compiler code. Invalid or inconsistent generated
metadata fails application construction, and the handler deep-copies the
canvas before serving it.
management.HTTPMetrics implements web.HTTPObserver. It records requests,
in-flight work, status counts, bytes, total/max duration, and panics per stable
generated route. Route labels contain only compiler-generated symbol, module,
method, and pattern values—never raw paths or other client-controlled
cardinality. The zero value is usable, snapshots are immutable and sorted, and
completion is idempotent. A hard 4,096-route cap bounds memory; excess
observations are counted in dropped_observations.
Mount the handler explicitly on the application’s mux:
checks, err := management.LifecycleChecks( "application", "example.com/commerce", application.State,)if err != nil { return err}manager, err := management.New(checks...)if err != nil { return err}handler, err := management.NewHandler(management.HandlerOptions{ Manager: manager, Metrics: metrics, Access: management.AccessLoopback, Expose: []management.Endpoint{ management.EndpointHealth, management.EndpointReadiness, management.EndpointMetrics, }, Info: map[string]string{ "name": "commerce", "version": buildVersion, },})if err != nil { return err}mux.Handle(handler.Pattern(), handler)Pass the same collector to the generated application:
application, err := spicegen.NewApplicationWithOptions(ctx, spicegen.ApplicationOptions{ HTTPObservers: []web.HTTPObserver{metrics},})External dependency checks normally belong to both health and readiness,
not liveness; a database outage should stop traffic without asking an
orchestrator to restart an otherwise healthy process.
The explicit runtime API remains available for custom base paths, checks,
metadata, mux ownership, and embedding. A nil HandlerOptions.Expose preserves
the runtime package’s full default set for handwritten integrations; generated
applications always pass their compile-time allowlist.