Skip to content
Spice Framework on GitHub

Management endpoints

frameworkMaturity: previewSource: spice@c0641b3Exact reviewed source

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.