app

package
v2.0.0 Latest Latest
Warning

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

Go to latest
Published: Apr 20, 2026 License: MIT Imports: 14 Imported by: 0

README

app

Package app provides a ready-to-use application context for GitLab Go services, bundling a structured logger, an OpenTelemetry tracer, and a pluggable component lifecycle.

Quick start

func main() {
    ctx := context.Background()
    a, err := app.New(ctx)
    if err != nil {
        log.Fatal(err)
    }

    db, _ := postgres.NewWithConfig(&postgres.Config{DSN: dsn, Tracer: a.Tracer()})
    srv := httpserver.NewWithConfig(&httpserver.Config{Addr: ":8080", Logger: a.Logger(), Tracer: a.Tracer()})

    a.Register(db)  // starts first, shuts down last
    a.Register(srv) // starts second, shuts down first

    if err := a.Run(ctx); err != nil {
        log.Fatal(err)
    }
}

Run starts all components, blocks until SIGTERM or SIGINT is received, then shuts down gracefully. For finer control, use Start and Shutdown directly.

Components

Any type that implements the Component interface can be registered:

type Component interface {
    Start(ctx context.Context) error
    Shutdown(ctx context.Context) error
}

Components are started by Start in registration order and shut down by Shutdown in reverse order (LIFO). If a component fails to start, all previously started components are shut down automatically.

Optionally implement Name() string to identify the component in log lines and error messages. Without it, the type name is used as a fallback.

Configuration

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",
    },
    ShutdownTimeout: 15 * time.Second, // default: 30s
})

Observability

Every component start and shutdown is logged with:

  • Component name (from Name() or %T)
  • Registration order index
  • Duration

Failed shutdowns include the error and duration for debugging slow teardowns.

Testing

Use the apptest package for in-memory log and trace recorders. Tests call Start and Shutdown directly rather than Run to avoid signal handling:

a, logRec, spanRec := apptest.New()
a.Register(myComponent)

require.NoError(t, a.Start(ctx))
// ... exercise the component ...
require.NoError(t, a.Shutdown(ctx))

records := logRec.Records()
spans := spanRec.Ended()

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)
	}
}

Index

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

func New(ctx context.Context) (*App, error)

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

func NewWithConfig(ctx context.Context, cfg *Config) (*App, error)

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)
}

func (*App) Logger

func (a *App) Logger() *slog.Logger

Logger returns the structured logger. Safe to call on all goroutines for the lifetime of the App.

func (*App) Metrics

func (a *App) Metrics() *metrics.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

func (a *App) ReadinessCheck() func(context.Context) error

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

func (a *App) Register(c Component)

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
}

func (*App) RegisterNonCritical

func (a *App) RegisterNonCritical(c Component)

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)
	}
}

func (*App) Run

func (a *App) Run(ctx context.Context) error

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

func (a *App) Shutdown(ctx context.Context) error

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

func (a *App) Start(ctx context.Context) error

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.

func (*App) Tracer

func (a *App) Tracer() *trace.Tracer

Tracer returns the distributed-tracing Tracer. Safe to call on all goroutines for the lifetime of the App.

type Component

type Component interface {
	Start(ctx context.Context) error
	Shutdown(ctx context.Context) error
}

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.

Jump to

Keyboard shortcuts

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