Documentation
¶
Overview ¶
Package app provides a ready-to-use application context for new GitLab Go services, bundling a structured logger, an OpenTelemetry tracer, and a Prometheus metrics registry with a pluggable component lifecycle.
Quick start ¶
func main() {
ctx := context.Background()
a, err := app.New(ctx)
if err != nil {
log.Fatal(err)
}
a.Register(httpserver.New(a.Logger(), ":8080"))
if err := a.Run(ctx); err != nil {
log.Fatal(err)
}
}
Components ¶
Any type that implements Component can be plugged into an App:
type Component interface {
Start(ctx context.Context) error
Shutdown(ctx context.Context) error
}
Components are started by App.Start in registration order and shut down by App.Shutdown in reverse registration order (LIFO), so dependencies are torn down cleanly. The tracer is flushed last, after all components have stopped, to capture any spans emitted during shutdown.
Non-critical components ¶
Not all components are equally critical for request handling. Use App.RegisterNonCritical for components that are allowed to fail at startup without halting the application (for example, an OTel Collector sidecar that may not be ready at the exact moment the app starts):
a.Register(db) // critical: halt on failure a.RegisterNonCritical(collector) // non-critical: warn and retry
When a non-critical component fails to start, App.Start logs a warning, marks the component as degraded, and launches a background goroutine that retries with exponential backoff (default: 1 s initial, 30 s cap) until the component recovers or App.Shutdown is called.
Use App.ReadinessCheck to expose the degraded state to Kubernetes readiness probes. The returned function is compatible with httpserver.Server.AddReadinessCheck and returns an error while any non-critical component is still degraded:
srv.AddReadinessCheck("app-components", a.ReadinessCheck())
Configuration ¶
To customise the logger, tracer, metrics registry, or retry behaviour, pass a Config:
a, err := app.NewWithConfig(ctx, &app.Config{
Log: &log.Config{
LogLevel: slog.LevelDebug,
},
Trace: &trace.Config{
ServiceName: "my-service",
ServiceVersion: "v1.2.3",
Endpoint: "https://collector.example.com:4318",
},
Metrics: &metrics.Config{
Namespace: "myservice",
},
// Tune non-critical component retry behaviour (optional).
RetryInitialBackoff: 500 * time.Millisecond,
RetryMaxBackoff: 10 * time.Second,
})
Testing ¶
Use the apptest package to obtain an App with in-memory log and trace recorders and an isolated metrics registry. Tests call App.Start and App.Shutdown directly rather than App.Run to avoid signal handling:
import "gitlab.com/gitlab-org/labkit/v2/testing/apptest" a, logRec, spanRec := apptest.New() a.Register(myComponent) require.NoError(t, a.Start(ctx)) // ... exercise the component ... require.NoError(t, a.Shutdown(ctx))
When testing non-critical component retry behaviour, pass small backoff values via NewWithConfig to keep tests fast:
a, err := app.NewWithConfig(ctx, &app.Config{
RetryInitialBackoff: 5 * time.Millisecond,
RetryMaxBackoff: 10 * time.Millisecond,
})
Example ¶
Example shows the typical main() pattern: create an App, register components, and call Run to start serving and block until SIGTERM or SIGINT.
package main
import (
"context"
"gitlab.com/gitlab-org/labkit/v2/app"
)
func main() {
ctx := context.Background()
a, err := app.New(ctx)
if err != nil {
panic(err)
}
// Register components — started in order, shut down in reverse (LIFO).
// a.Register(db)
// a.Register(srv)
if err := a.Run(ctx); err != nil {
panic(err)
}
}
Output:
Index ¶
- type App
- func (a *App) Logger() *slog.Logger
- func (a *App) Metrics() *metrics.Metrics
- func (a *App) ReadinessCheck() func(context.Context) error
- func (a *App) Register(c Component)
- func (a *App) RegisterNonCritical(c Component)
- func (a *App) Run(ctx context.Context) error
- func (a *App) Shutdown(ctx context.Context) error
- func (a *App) Start(ctx context.Context) error
- func (a *App) Tracer() *trace.Tracer
- type Component
- type Config
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App bundles a structured logger, a distributed-tracing Tracer, a Prometheus metrics registry, and a set of pluggable Component implementations. Construct one with New or NewWithConfig, register components with [Register], call [Start] to begin serving, and call [Shutdown] on exit.
func New ¶
New returns an App initialised with default logger and tracer configuration. It is shorthand for NewWithConfig(ctx, nil).
a, err := app.New(ctx)
if err != nil { ... }
defer a.Shutdown(ctx)
func NewForTesting ¶
func NewForTesting(logger *slog.Logger, tracer *trace.Tracer, shutdown func(context.Context) error, m *metrics.Metrics) *App
NewForTesting creates an App from pre-built logger, tracer, and metrics components. It is intended for use by the testing/apptest package; production code should use New or NewWithConfig instead.
func NewWithConfig ¶
NewWithConfig returns an App initialised with the provided Config. A nil cfg is equivalent to &Config{} (all defaults).
Example ¶
ExampleNewWithConfig shows how to configure the logger and tracer explicitly rather than relying on environment-variable defaults.
package main
import (
"context"
"log/slog"
"gitlab.com/gitlab-org/labkit/v2/app"
lablog "gitlab.com/gitlab-org/labkit/v2/log"
"gitlab.com/gitlab-org/labkit/v2/trace"
)
func main() {
ctx := context.Background()
level := slog.LevelDebug
a, err := app.NewWithConfig(ctx, &app.Config{
Log: &lablog.Config{
LogLevel: &level,
},
Trace: &trace.Config{
ServiceName: "my-service",
ServiceVersion: "v1.2.3",
Endpoint: "https://collector.example.com:4318",
},
})
if err != nil {
panic(err)
}
defer a.Shutdown(ctx)
}
Output:
func (*App) Logger ¶
Logger returns the structured logger. Safe to call on all goroutines for the lifetime of the App.
func (*App) Metrics ¶
Metrics returns the Prometheus metrics registry. Use this to register application-specific collectors or to obtain a [prometheus.Registerer] for injection into sub-components. Safe to call on all goroutines for the lifetime of the App.
func (*App) ReadinessCheck ¶
ReadinessCheck returns a function that reports whether all non-critical components are currently healthy. The returned function can be passed directly to httpserver.Server.AddReadinessCheck:
srv.AddReadinessCheck("app-components", a.ReadinessCheck())
The check returns nil when all non-critical components have successfully started (or there are none). It returns an error naming every component that is still in a degraded state.
ReadinessCheck must be called after all components have been registered (i.e. after [Start] returns). Calling it concurrently with [Register] or [RegisterNonCritical] is a data race.
func (*App) Register ¶
Register adds a Component to the App. Components are started by [Start] in registration order and shut down by [Shutdown] in reverse registration order. Register panics if called after Start.
Example ¶
ExampleApp_Register shows how to implement and register a custom Component.
package main
import (
"context"
"log/slog"
"gitlab.com/gitlab-org/labkit/v2/app"
)
func main() {
ctx := context.Background()
a, err := app.New(ctx)
if err != nil {
panic(err)
}
w := &worker{logger: a.Logger()}
a.Register(w)
if err := a.Start(ctx); err != nil {
panic(err)
}
defer a.Shutdown(ctx)
}
// worker is a minimal app.Component used in ExampleApp_Register.
type worker struct {
logger *slog.Logger
}
func (w *worker) Start(ctx context.Context) error {
w.logger.InfoContext(ctx, "worker started")
return nil
}
func (w *worker) Shutdown(ctx context.Context) error {
w.logger.InfoContext(ctx, "worker stopped")
return nil
}
Output:
func (*App) RegisterNonCritical ¶
RegisterNonCritical adds a Component to the App that is allowed to fail during startup without halting the application. If the component fails to start, App.Start logs a warning and continues starting other components. A background goroutine retries the failed component with exponential backoff until it succeeds or App.Shutdown is called. The initial backoff and cap are controlled by [Config.RetryInitialBackoff] and [Config.RetryMaxBackoff].
The degraded state of non-critical components is exposed via App.ReadinessCheck, which can be registered with httpserver.AddReadinessCheck to gate Kubernetes readiness probes.
RegisterNonCritical panics if called after Start.
Example ¶
ExampleApp_RegisterNonCritical shows how to register a component that is allowed to fail at startup. The app continues running and retries the component in the background. Use app.App.ReadinessCheck to gate Kubernetes readiness probes on recovery.
package main
import (
"context"
"time"
"gitlab.com/gitlab-org/labkit/v2/app"
)
func main() {
ctx := context.Background()
a, err := app.NewWithConfig(ctx, &app.Config{
// Optional: tune how quickly degraded components are retried.
RetryInitialBackoff: 500 * time.Millisecond,
RetryMaxBackoff: 30 * time.Second,
})
if err != nil {
panic(err)
}
// critical: any start failure halts the app.
// a.Register(db)
// non-critical: start failure logs a warning and triggers background retry.
// a.RegisterNonCritical(otelCollector)
// Wire degraded-component state into the /-/readiness health endpoint.
// srv.AddReadinessCheck("app-components", a.ReadinessCheck())
if err := a.Run(ctx); err != nil {
panic(err)
}
}
Output:
func (*App) Run ¶
Run starts all components, blocks until ctx is cancelled or a SIGTERM/SIGINT signal is received, then shuts down gracefully using [Config.ShutdownTimeout]. It is a convenience method that replaces the common boilerplate of calling [Start], waiting for a signal, and calling [Shutdown].
a, _ := app.New(ctx)
a.Register(srv)
if err := a.Run(ctx); err != nil {
log.Fatal(err)
}
func (*App) Shutdown ¶
Shutdown calls Shutdown on each registered Component in reverse registration order, then flushes buffered trace spans. All shutdown errors are collected and returned as a combined error so that a failure in one component does not prevent others from shutting down.
Background retry goroutines for non-critical components are cancelled before any component Shutdown is called. Non-critical components that never successfully started are skipped.
defer a.Shutdown(ctx)
func (*App) Start ¶
Start calls Start on each registered Component in registration order.
For critical components (registered via [Register]), any start failure triggers an immediate rollback: all previously started components are shut down in reverse order before the error is returned.
For non-critical components (registered via [RegisterNonCritical]), a start failure is logged as a warning and the component is marked degraded. A background goroutine retries with exponential backoff until the component succeeds or [Shutdown] is called.
type Component ¶
Component is the interface that pluggable app components must implement. Components are registered with App.Register and have their lifecycle managed by the App: App.Start calls Start in registration order, and App.Shutdown calls Shutdown in reverse registration order so that dependencies are torn down cleanly.
If a component has no meaningful start-up work it may return nil from Start. Shutdown must honour the context deadline and return promptly once it expires.
type Config ¶
type Config struct {
// Log customises the structured logger. A nil value uses the log package
// defaults (JSON format, stderr, Info level).
Log *log.Config
// Trace customises the OpenTelemetry tracer. A nil value uses the trace
// package defaults (always sample, OTLP/HTTP to localhost:4318).
Trace *trace.Config
// Metrics customises the Prometheus registry. A nil value uses the metrics
// package defaults (isolated registry, "gitlab" namespace).
Metrics *metrics.Config
// ShutdownTimeout is the maximum duration allowed per component during
// cleanup when a start failure triggers reverse shutdown. Defaults to 30s.
ShutdownTimeout time.Duration
// RetryInitialBackoff is the wait before the first retry attempt for a
// non-critical component that failed to start. Defaults to 1s.
RetryInitialBackoff time.Duration
// RetryMaxBackoff caps the exponential backoff for non-critical component
// retries. Defaults to 30s.
RetryMaxBackoff time.Duration
// RetryStartTimeout is the per-attempt deadline for a non-critical
// component's Start call during background retries. If a Start call does
// not return within this duration it is cancelled and counted as a failure.
// Defaults to 5s.
RetryStartTimeout time.Duration
}
Config holds configuration overrides for New and NewWithConfig. A nil or zero Config produces sensible defaults for the logger, tracer, and metrics registry.