Help us shape the future of Go! Take the 2026 Go Developer Survey

observe

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 6 Imported by: 0

README

Go Reference License Stay with Ukraine

observe

observe gives every Go service the same operational-status endpoints - liveness, readiness and build info - without copy-pasted handlers. Zero dependencies, standard library only.

It is deliberately not a metrics/tracing platform: no Prometheus client, no OpenTelemetry SDK. It stays compatible with those tools without depending on them.

Install

go get github.com/goloop/observe

Quick start

obs := observe.New(
	observe.WithBuildInfo(observe.BuildInfo{Service: "api", Version: "v1.2.3"}),
	observe.WithErrorRedaction(true), // hide details in production
)

obs.Check("postgres", func(ctx context.Context) error { return pool.Ping(ctx) })
obs.Check("redis", func(ctx context.Context) error { return rdb.Ping(ctx) })

mux.Handle("/healthz", obs.HealthHandler()) // 200 while the process lives
mux.Handle("/readyz", obs.ReadyHandler())   // 503 if a dependency is down
mux.Handle("/buildz", obs.BuildHandler())

Health vs readiness

  • Health (/healthz) is process-level liveness: "is the process up". It never runs dependency checks and is always 200.
  • Readiness (/readyz) runs the registered checks in parallel (each bounded by a timeout, even if a check ignores its context; a panicking check is recovered) and returns 503 when any dependency is down, so a load balancer stops routing traffic.

Response shape:

{
  "status": "error",
  "service": "api",
  "version": "v1.2.3",
  "checks": {
    "postgres": {"status": "ok", "latency_ms": 3},
    "redis": {"status": "error", "latency_ms": 200}
  }
}

Every check reports latency_ms. Error details are redacted by default, so the failing status is reported without leaking the underlying error (which can carry hostnames, DSNs or credentials). Pass WithErrorRedaction(false) to add an "error" field for local debugging - not in production.

Decoupling

observe never imports a lifecycle kernel. To surface application lifecycle state, register it as an ordinary check over a status snapshot:

obs.Check("app", func(context.Context) error {
	if !a.Status().Healthy() {
		return errors.New("a component failed")
	}
	return nil
})

Documentation

License

MIT - see LICENSE.

Release v1.0.0

Publish the existing health check API as version 1.0.0. The v1 release keeps the existing import path and public API. Future incompatible changes require a new major version.

Documentation

Overview

Package observe provides health, readiness and build-info endpoints for Go services, standard library only.

It owns the operational-status model so every service exposes the same /healthz, /readyz and build-info shape without copy-pasted handlers. It is deliberately not a metrics/tracing platform: no Prometheus client, no OpenTelemetry SDK.

Health vs readiness

Health is process-level liveness: it answers "is the process up" and is always ok. Readiness runs the registered dependency checks (database, cache, queue, ...) and fails when any dependency is unusable, so a load balancer can stop routing traffic.

obs := observe.New(
    observe.WithBuildInfo(observe.BuildInfo{Service: "api", Version: "v1.2.3"}),
    // Error details are redacted by default; opt in with
    // observe.WithErrorRedaction(false) for local debugging.
)
obs.Check("postgres", func(ctx context.Context) error { return pool.Ping(ctx) })
obs.Check("redis", func(ctx context.Context) error { return rdb.Ping(ctx) })

mux.Handle("/healthz", obs.HealthHandler()) // 200 while the process lives
mux.Handle("/readyz", obs.ReadyHandler())   // 503 if a dependency is down
mux.Handle("/buildz", obs.BuildHandler())

Decoupling

observe never imports the lifecycle kernel. To surface application lifecycle state, register it as an ordinary check over the kernel's status snapshot:

obs.Check("app", func(context.Context) error {
    if !a.Status().Healthy() {
        return errors.New("a component failed")
    }
    return nil
})

See DOC.md (English) and DOC.UK.md (Ukrainian) for the full reference.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BuildInfo

type BuildInfo struct {
	Service string `json:"service,omitempty"`
	Version string `json:"version,omitempty"`
	Commit  string `json:"commit,omitempty"`
	BuiltAt string `json:"built_at,omitempty"`
}

BuildInfo identifies the running build. It is surfaced in health and readiness responses and by BuildHandler. Populate it at build time with -ldflags, or from runtime/debug.ReadBuildInfo.

type Check

type Check func(ctx context.Context) error

Check reports whether a dependency is usable. It should be fast and honour the context deadline. A nil error means healthy.

type CheckResult

type CheckResult struct {
	Status    string `json:"status"`
	LatencyMS int64  `json:"latency_ms"`
	Error     string `json:"error,omitempty"`
}

CheckResult is the outcome of a single dependency check.

func (CheckResult) OK

func (c CheckResult) OK() bool

OK reports whether the check passed.

type Option

type Option func(*Registry)

Option configures a Registry at construction time.

func WithBuildInfo

func WithBuildInfo(b BuildInfo) Option

WithBuildInfo sets the build metadata. If the service name is not set separately, it is taken from the build info.

func WithCheckTimeout

func WithCheckTimeout(d time.Duration) Option

WithCheckTimeout sets the per-check deadline used by Ready. A non-positive value disables the per-check timeout (the request context still applies).

func WithErrorRedaction

func WithErrorRedaction(redact bool) Option

WithErrorRedaction controls whether check error details appear in responses; the failing status is always reported either way. Redaction is ON by default so internals never leak by accident. Pass false to expose details for local debugging - not in production.

func WithService

func WithService(name string) Option

WithService sets the service name reported in responses.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds health/readiness checks and build info. It owns the health model: liveness (Health) is process-level and always ok; readiness (Ready) runs the registered dependency checks. It is safe for concurrent use.

Example

ExampleRegistry wires health, readiness and build endpoints onto a mux.

package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/goloop/observe"
)

func main() {
	obs := observe.New(
		observe.WithBuildInfo(observe.BuildInfo{Service: "api", Version: "v1.2.3"}),
		observe.WithErrorRedaction(true),
	)

	// Register dependency checks (database, cache, ...).
	obs.Check("cache", func(context.Context) error { return nil })

	mux := http.NewServeMux()
	mux.Handle("/healthz", obs.HealthHandler())
	mux.Handle("/readyz", obs.ReadyHandler())
	mux.Handle("/buildz", obs.BuildHandler())

	st := obs.Ready(context.Background())
	fmt.Println(st.Status, st.Service, st.Version)
}
Output:
ok api v1.2.3
Example (LifecycleBridge)

ExampleRegistry_lifecycleBridge shows registering an external lifecycle snapshot as an ordinary check, so observe stays decoupled from the kernel that produced it.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/goloop/observe"
)

func main() {
	obs := observe.New()

	// healthyFromKernel would come from your lifecycle kernel's status.
	healthyFromKernel := func() bool { return true }

	obs.Check("app", func(context.Context) error {
		if !healthyFromKernel() {
			return errors.New("a component failed")
		}
		return nil
	})

	fmt.Println(obs.Ready(context.Background()).OK())
}
Output:
true

func New

func New(opts ...Option) *Registry

New creates a Registry.

func (*Registry) Build

func (r *Registry) Build() BuildInfo

Build returns the configured build info.

func (*Registry) BuildHandler

func (r *Registry) BuildHandler() http.Handler

BuildHandler serves the build info as JSON with HTTP 200.

func (*Registry) Check

func (r *Registry) Check(name string, fn Check)

Check registers or replaces a readiness check. A nil function or an empty name is ignored (an empty name would produce a blank JSON key).

func (*Registry) Health

func (r *Registry) Health(context.Context) Status

Health returns process-level liveness. It never runs dependency checks and is always ok: it answers "is the process up", not "are dependencies usable".

func (*Registry) HealthHandler

func (r *Registry) HealthHandler() http.Handler

HealthHandler serves liveness as JSON with HTTP 200. It does not run dependency checks: it answers whether the process is up. Mount it at /healthz (or your convention).

func (*Registry) Ready

func (r *Registry) Ready(ctx context.Context) Status

Ready runs all registered checks (in parallel, each bounded by the check timeout) and returns the aggregated status: error if any check fails.

func (*Registry) ReadyHandler

func (r *Registry) ReadyHandler() http.Handler

ReadyHandler serves readiness as JSON. It runs the registered dependency checks and returns HTTP 200 when all pass, or HTTP 503 when any fails, so a load balancer can stop routing traffic. Mount it at /readyz (or your convention).

type Status

type Status struct {
	Status  string                 `json:"status"`
	Service string                 `json:"service,omitempty"`
	Version string                 `json:"version,omitempty"`
	Checks  map[string]CheckResult `json:"checks,omitempty"`
}

Status is a health or readiness snapshot rendered as JSON.

func (Status) OK

func (s Status) OK() bool

OK reports whether the overall status is healthy.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL