Platform endpoints

go.putnami.dev/platform mounts the standard operational HTTP surface every workload needs — liveness, readiness, version, and (optionally) pprof — and auto-discovers per-plugin probes from the application's module tree.

It replaces the per-workload boilerplate (and the legacy http.NewHealthPlugin()'s single /_/health endpoint) with one plugin you opt in to.

Plugin setup

import (
    "go.putnami.dev/app"
    "go.putnami.dev/http"
    "go.putnami.dev/platform"
)

server := http.NewServerPlugin(http.ServerConfig{Port: 8080})
platformPlugin := platform.NewPlugin(platform.Config{
    Version: platform.VersionInfo{Name: "my-service", Version: "1.0.0"},
})

a := app.New("my-service").
    Use(server).
    Use(platformPlugin)
a.ListenAndServe()

Use(platformPlugin) is the only wiring step. When the application configures, the plugin mounts its endpoints on the application's single http.ServerPlugin, in any plugin order. An application that holds no server, or several, fails configure with an error that names RegisterOn. Call platformPlugin.RegisterOn(server) before the application configures to choose the server.

Endpoints

Path Purpose
/livez Lightweight liveness. Returns 200 {"status":"ok"} whenever the handler can run. No probes, no flags. Safe to use as a Kubernetes liveness probe.
/healthz Liveness aggregate. 200 when running and every app.HealthChecker probe passes. 503 {"status":"unavailable"} before Start and after Stop. 503 {"status":"degraded","checks":{…}} when any probe fails.
/readyz Readiness aggregate. Same shape as /healthz, driven by app.ReadinessChecker probes, and 503 {"status":"unavailable"} until the application completed startup: every Starter and module OnStart hook returned. The endpoint to use as a Kubernetes readiness probe.
/version Build metadata as JSON. Caller-supplied VersionInfo wins; empty fields fall back to runtime/debug.ReadBuildInfo.
/debug/pprof/* net/http/pprof index, named profiles (heap, goroutine, allocs, …) and on-demand collectors (profile, trace, cmdline, symbol). Disabled by default.

Default mount is root. Set Config.Prefix to namespace (Prefix: "/_" → /_/healthz, …).

Capability interfaces

The platform plugin discovers two interfaces by walking the module tree from the root and registers each implementation under its Name().

// in go.putnami.dev/app

type HealthChecker interface {
    Plugin
    CheckHealth(ctx context.Context) error
}

type ReadinessChecker interface {
    Plugin
    CheckReadiness(ctx context.Context) error
}
Interface Semantics Failure consequence (k8s)
HealthChecker Dependency is broken in a way only a restart fixes (liveness) Pod restart
ReadinessChecker Dependency is temporarily down — drain traffic but don't restart Traffic drained, no restart

A single plugin can implement both: CheckHealth contributes to /healthz, CheckReadiness contributes to /readyz.

Implementations must be safe to call concurrently and respect context cancellation. The plugin enforces a per-request Config.ProbeTimeout (default 5s) so a hung probe can't stall the endpoint.

Built-in contributor: database.Plugin implements app.HealthChecker — pool.Ping shows up under the database key on /healthz automatically when both plugins are mounted.

If a probe only needs DI-managed dependencies, contribute an injected function instead of resolving and storing the dependency in the plugin:

func (p *Plugin) Configure(ctx context.Context, owner *app.Module) error {
    app.Contribute[app.ReadinessChecker](owner, app.HealthFunc("platform_configs", func(ctx context.Context, pools *psql.Pools) error {
        pool, err := pools.For("platform_configs")
        if err != nil {
            return err
        }
        return pool.Ping(ctx)
    }))
    return nil
}

Explicit probes

For probes not owned by a plugin (an external URL, an ad-hoc check), register directly:

if err := platformPlugin.AddHealthChecker("upstream", func(ctx context.Context) error {
    return checkUpstreamURL(ctx, "https://example.com/health")
}); err != nil {
    return err
}

if err := platformPlugin.AddReadinessChecker("warm", func(ctx context.Context) error {
    if !cacheWarmed.Load() {
        return errors.New("cache warming")
    }
    return nil
}); err != nil {
    return err
}

Explicit registrations win over auto-discovered probes of the same name — useful for overriding a plugin-provided probe in a specific environment.

Configuration

type Config struct {
    Prefix       string        // path prefix (default: "" → root)
    EnablePprof  bool          // expose /debug/pprof/* (default: false)
    Version      VersionInfo   // /version payload
    ProbeTimeout time.Duration // per-probe timeout (default: 5s)
}

type VersionInfo struct {
    Name      string `json:"name,omitempty"`
    Version   string `json:"version,omitempty"`
    SHA       string `json:"sha,omitempty"`
    Branch    string `json:"branch,omitempty"`
    BuildTime string `json:"buildTime,omitempty"`
}

Build metadata

For production builds, embed version info at link time:

var version = "dev"
var commit = ""

platform.NewPlugin(platform.Config{
    Version: platform.VersionInfo{
        Name:    "my-service",
        Version: version,
        SHA:     commit,
    },
})

Build with go build -ldflags "-X main.version=1.2.3 -X main.commit=$(git rev-parse HEAD)". Leaving Version empty falls back to runtime/debug.ReadBuildInfo (module path, vcs.revision, vcs.time when built with -buildvcs).

pprof

/debug/pprof/* exposes Go's runtime profiler. Off by default — profiles reveal heap layout and goroutine details, and the CPU/trace collectors are expensive.

platform.NewPlugin(platform.Config{EnablePprof: true})

Do not expose pprof on a public port. Mount the platform plugin on a separate admin server, or restrict access via a sidecar / network policy.

Migrating from http.NewHealthPlugin()

The legacy http.HealthPlugin (single /_/health endpoint) still works and now also auto-discovers app.HealthChecker implementations. To get the full operational surface:

// before
a.Use(server).Use(http.NewHealthPlugin())

// after
platformPlugin := platform.NewPlugin(platform.Config{Prefix: "/_"}) // keep /_ namespace
a.Use(server).Use(platformPlugin)

/_/health becomes /_/healthz, and you also get /_/livez, /_/readyz, /_/version. Drop the Prefix for plain k8s-style paths (/healthz, /livez, …). Both plugins can share one server: each discovers the same probes and reports them on its own route. The go-server starter composes both, because putnami qualify and deployment probes wait on /readyz.

Support and contract

go.putnami.dev/platform is stable in the workspace support catalog. Its behavior is defined by the platform endpoints specification and its accepted decision record next to the package source (go/framework/platform/specs/ and go/framework/platform/doc/adr/). Before v1.0.0 a minor 0.x release may still contain a documented breaking change; strict compatibility between every pre-1.0 minor is not promised.