Build And Deploy

Control Plane

Expose liveness, readiness, metrics, and Go diagnostics outside application routes.

By Guillermo Alvarez - Published - Updated

Enable probes in app config

Generated apps do not include same-handler control-plane routes by default. Add an empty control-plane config when the app should expose framework-owned operational endpoints on the same handler as the app:

return lazyapp.New(lazyapp.Config{
    Name:         "sample_app",
    Drawer:       Draw,
    Public:       app.Public,
    Views:        app.Views,
    Dependencies: Dependencies,
    ControlPlane: lazycontrolplane.Config{},
})

The empty config exposes:

GET /livez
GET /readyz

Applications that configure lazyapp.Config.Jobs also expose read-only job state at GET /jobs on the control plane. The endpoint reports registered job definitions, state counts, and recent jobs.

When the control plane is mounted on the app listener, control-plane routes are checked before application routes. If the app also draws /livez, the control-plane route wins.

Use a separate address

ListenAndServe activates the default control plane when CONTROL_PLANE_ADDR is set:

ADDR=0.0.0.0:8080 CONTROL_PLANE_ADDR=127.0.0.1:9090 ./app

When CONTROL_PLANE_ADDR differs from the app address, ListenAndServe starts a second HTTP server for the control plane. The public app server then serves only application traffic. GET / on the control-plane listener shows a compact HTML index of the registered control-plane endpoints. GoLazy automatically registers /debug/pprof/ and the standard pprof subpaths on that separate control-plane listener.

If CONTROL_PLANE_ADDR matches ADDR, PORT, or the default 127.0.0.1:3000, GoLazy mounts the control plane into the app server instead of trying to bind the same port twice. In that mode the control-plane index is not mounted, so application / routes keep their normal behavior:

CONTROL_PLANE_ADDR=3000 ./app

If CONTROL_PLANE_ADDR is unset, production ListenAndServe does not mount control-plane endpoints on the public app listener, even when the app config builds a control plane. This keeps liveness, readiness, metrics, and diagnostics off public routes by default.

Add readiness checks

Readiness checks receive the request context and return an error when the app is not ready:

ControlPlane: lazycontrolplane.Config{
    Readiness: []lazycontrolplane.ReadinessCheck{{
        Name: "database",
        Check: func(ctx context.Context) error {
            return db.PingContext(ctx)
        },
    }},
},

/readyz returns 503 Service Unavailable when a check fails.

Attach metrics and pprof

Metrics are opt-in:

ControlPlane: lazycontrolplane.Config{
    Metrics: metricsHandler,
},

That registers GET /metrics. GoLazy does not add a Prometheus dependency for you; pass a handler from the exporter you choose.

Go diagnostics are automatic when CONTROL_PLANE_ADDR points at a separate listener. For same-listener or manually served control planes, opt in with config:

ControlPlane: lazycontrolplane.Config{
    Pprof: true,
},

That registers /debug/pprof/ and the standard pprof subpaths. Do not expose pprof on a public application listener.

Serve manually

If you need a custom server lifecycle, instantiate the control plane yourself:

plane := lazycontrolplane.New(lazycontrolplane.Config{})
server := &http.Server{
    Addr:    "127.0.0.1:9090",
    Handler: plane.StandaloneHandler(),
}

When you manage the server manually, lazyapp.ListenAndServe is not responsible for starting that control-plane server. Use plane directly or plane.Handler(next) instead when the control plane shares an application listener and / should remain application-owned.

Development endpoints

Apps started by lazy are built with the lazydev tag and receive a separate internal CONTROL_PLANE_ADDR. In that mode GoLazy adds development-only endpoints such as GET /routes, GET /cache, POST /cache/on, POST /cache/off, GET /jobs, GET /buildinfo, POST /views, and POST /_golazy/open-editor.

Those endpoints are not part of production builds. Read Lazydev Mode for the development-only surface.