platform

package module
v0.9.1 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 23 Imported by: 30

README

platform - A modular system for building Go applications

Motivation

The platform package is an extensible, modular system for building HTTP servers and sidecar services in Go.

It provides a global registry for modules and middleware, a lifecycle for graceful shutdown, and named database connections, allowing you to structure services as composable, testable modules. It imports no sql driver: the binary registers the one its DSN names with a blank import.

Running a platform.Manager rather than a bare *Platform adds a SIGHUP reload that replaces the platform without dropping the socket. cmd.Main already does. A bare platform.Start leaves SIGHUP at its default disposition, which terminates the process.

Application examples, with database use:

Status: the app and maillist repositories still need implementation surface.

Coverage

Status Package Coverage Cognitive Lines
✅ titpetric/platform 93.65% 146 1057
✅ titpetric/platform/cmd 46.67% 2 20
✅ titpetric/platform/cmd/platform 0.00% 0 3
✅ titpetric/platform/internal 88.51% 46 246
✅ titpetric/platform/internal/assert 98.57% 67 270
✅ titpetric/platform/internal/httpcontext 100.00% 1 18
✅ titpetric/platform/internal/pidfile 95.83% 11 44

For more detail, see: Testing Coverage.

Development docs

  • The Platform - key concepts, logging, lifecycle, the SIGHUP reload and the pidfile.
  • API documentation - generated api documentation for the platform package.
  • Creating Modules - module API, lifecycle, and using UnimplementedModule.
  • Common Patterns - routing, GET/POST, background jobs, middleware and validation patterns.
  • SQL Database Usage - named connections, DSN examples, and bringing your own sql driver.
  • Telemetry - recording traces and spans with oida, and the /debug/oida dashboard.
  • Structural diagram - the two Go modules, and generated package import and class diagrams.
  • FAQ - short practical answers to common questions.

Documentation

Overview

The platform is an extensible modular system for writing HTTP servers.

1. Provides a global registry for middleware and module registration 2. Provides a lifecycle to the modules for graceful shutdown 3. Provides a router the modules can attach to

It's advised to use `platform.RegisterFunc` from `init` functions. Similarly, `platform.Use` should be used from `main` or any descendant setup functions. Don't use these functions from tests as they create a shared state.

It's possible to use the platform in an imperative way.

```go svc := platform.New(platform.NewOptions()) svc.Use(middleware.Logger) svc.Register(user.NewModule()) ```

The platform lifecycle is extensively tested to ensure no races, no goroutine leaks. Each platform object creates a copy of the global state and holds scoped allocations only, enabling test parallelism. Modules are part of that copy when registered with `RegisterFunc`, which is called once per platform. A value registered with the deprecated `Register` is shared by every platform in the process.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Error

func Error(w http.ResponseWriter, r *http.Request, status int, data error)

Error writes an error payload as JSON.

func JSON

func JSON(w http.ResponseWriter, r *http.Request, status int, data any)

JSON writes any payload as JSON. If the payload is nil, the write is omitted. If an error occurs in encoding, a telemetry error is logged.

func Param added in v0.0.3

func Param(r *http.Request, name string) string

Param will return a named URL parameter, or query string.

func QueryParam added in v0.0.3

func QueryParam(r *http.Request, name string) string

QueryParam will return a named query parameter from the request.

func Register deprecated

func Register(m Module)

Register will register a module in the platform global registry. It should not be relied upon in tests, keeping global state empty. This enables registering modules using blank imports.

Deprecated: use RegisterFunc. One value is shared by every platform in the process, including the generations of a reload, so its state outlives the platform it was started with.

func RegisterFunc added in v0.7.1

func RegisterFunc(f func() Module)

RegisterFunc will register a module constructor in the platform global registry. It is called once per platform, so every platform, and every reload generation, starts a module of its own.

It should not be relied upon in tests, keeping global state empty. This enables registering modules using blank imports.

func SetupConnections added in v0.4.8

func SetupConnections(environment []string)

SetupConnections will parse the env for named connection strings.

func Transaction

func Transaction(ctx context.Context, db *sqlx.DB, fn func(context.Context, *sqlx.Tx) error) error

Transaction wraps a function in a transaction. If the function returns an error, the transaction is rolled back. If the function returns nil, the transaction is committed.

func URLParam added in v0.0.3

func URLParam(r *http.Request, name string) string

URLParam will return a named parameter value from the request URL.

func Use

func Use(mw Middleware)

Use will add a middleware to the platform router. It should not be relied upon in tests, keeping global state empty. This should be used from main() to define any global middleware.

Types

type DatabaseProvider

type DatabaseProvider interface {
	// Open takes a context, a different implementation may use it for context awareness.
	// For example, Open may retrieve connection details from the database. The context
	// is used for tracing that operation, and the error returned may be the
	// result of a query issued against the database.
	Open(ctx context.Context, names ...string) (*sqlx.DB, error)

	// Connect has the semantics of Open + PingContext against the database,
	// verifying that the connection is live. An error is returned if
	// the storage is unreachable.
	//
	// A real database provider options may dictate additional behaviour.
	// For example, if a connection fails, it may retry a number of times,
	// it may back-off, it may continue to retry the connection in a
	// blocking way. It may also grab additional fail-over information from
	// keys based on the input parameters. An example of that would be to
	// define `name` and `name/failover` connections. If the connection to
	// the first fails, the second upstream is used.
	Connect(ctx context.Context, names ...string) (*sqlx.DB, error)
}

DatabaseProvider is the implementation interface for working with named connections. If no connection name is passed, the "default" connection will be used. It makes no assumption about how the connection is provided, and only enforces context awareness.

Connection names, singleton behaviour, retries, fallback mechanisms, multiple-name logic and everything else needed to produce a *sql.DB is left to the implementation. The first party one decodes `PLATFORM_DB_*` from the process environment, using the variable name as the key and its value as the connection definition, and carries no other logic like reconnecting.

var Database DatabaseProvider = global.db

Database is a holder of the database provider api in package namespace.

type ErrorResponse

type ErrorResponse struct {
	Error ErrorResponseBody `json:"error"`
}

ErrorResponse is the JSON structure for error responses.

type ErrorResponseBody

type ErrorResponseBody struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

ErrorResponseBody is an inner type for ErrorResponse.Error.

type Logger added in v0.7.1

type Logger interface {
	Info(msg string, args ...any)
	Error(msg string, args ...any)
}

Logger is the interface the platform writes its own output through. It is the subset of *slog.Logger the platform needs, so a *slog.Logger can be assigned to Platform.Logger as it is.

type Manager added in v0.7.1

type Manager struct {
	// Logger receives the manager's own output, and is handed to every
	// platform generation it creates. Defaults as Platform.Logger does.
	Logger Logger

	// Setup runs against every platform generation before it starts.
	// Registration against a platform value belongs here, as a reload
	// discards the value it was made against. Assign it before Start: a
	// reload runs it from the signal handler's goroutine.
	Setup func(*Platform) error

	// Check runs before a reload retires the generation that is serving,
	// and a non-nil error abandons that reload with the generation left
	// alone. Reading the configuration a reload would apply belongs here:
	// Setup runs against the new generation, which exists only once the old
	// one has stopped. Nil reloads unconditionally. Assign before Start, as
	// with Setup, because the signal handler reads it from a goroutine.
	Check func() error
	// contains filtered or unexported fields
}

Manager owns the socket, and the platform generation serving on it. It replaces the platform under it on SIGHUP, keeping the address it serves.

func NewManager added in v0.7.1

func NewManager(options *Options) *Manager

NewManager creates a manager for the passed options. If no options are passed, the defaults from NewOptions() are in use.

func (*Manager) Context added in v0.7.1

func (m *Manager) Context() context.Context

Context returns the cancellation context for the manager. When the context finishes, the platform has shut down and the socket is closed.

func (*Manager) Platform added in v0.7.1

func (m *Manager) Platform() *Platform

Platform returns the generation currently serving, and nil when there is none: before Start, after Stop, and after a reload that failed.

func (*Manager) Reload added in v0.7.1

func (m *Manager) Reload(ctx context.Context) error

Reload stops the running platform and starts a new one on the same socket, unless Check refuses it first. Generations never overlap, so a module registered as a value has to survive a restart.

func (*Manager) Start added in v0.7.1

func (m *Manager) Start(ctx context.Context) error

Start binds the listener, starts the first platform generation on it, writes Options.PidFile and arms the SIGHUP handler. Cancelling ctx stops the manager.

func (*Manager) Stop added in v0.7.1

func (m *Manager) Stop()

Stop shuts down the running platform and closes the socket. A stopped manager does not start again.

func (*Manager) URL added in v0.7.1

func (m *Manager) URL() string

URL gives the e2e endpoint URL for requests. A reload does not change it. Before a successful Start there is no socket and the URL is empty.

func (*Manager) Wait added in v0.7.1

func (m *Manager) Wait()

Wait will pause until the manager is stopped. A reload does not end it.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is a type alias for middleware functions.

func TestMiddleware

func TestMiddleware() Middleware

TestMiddleware returns a middleware that just passes along the request.

type Module

type Module interface {
	// Name should return a meaningful name for your module.
	Name() string

	// Start is used to create any goroutines or otherwise
	// set up the module by starting a server. It allows
	// to implement a lifecycle of the service.
	Start(context.Context) error

	// Stop should clean up any goroutines, clean up leaks.
	Stop(context.Context) error

	// Mount runs before the server starts, and allows you to
	// register new routes to your module.
	Mount(context.Context, Router) error
}

Module is the implementation contract for modules.

The interface should only be used to enforce the API contract as shown below. It's also used to provide `platform.Register()`.

type Options

type Options struct {
	// ServerAddr is the address the server listens to.
	ServerAddr string

	// PidFile is the file the process records its own id in, for a service
	// manager or a command line that signals it. Empty writes none. The
	// directory has to exist, an existing file is overwritten, and the file
	// is removed on a clean stop. A Manager writes it for the process, so a
	// reload leaves it alone.
	PidFile string

	// Quiet silences the platform's own output: New installs a discarding
	// logger as Platform.Logger instead of the default one. Set to true in
	// tests. Assigning Platform.Logger afterwards overrules it.
	Quiet bool

	// Modules controls which modules get loaded. If the list
	// is empty (unconfigured, zero value), all modules load.
	Modules []string

	// ConfigFS can be used for configuration purposes by modules. It's optional and may be nil.
	// The application running with the platform may use `go:embed` to carry config for the
	// composed service.
	ConfigFS fs.FS

	// Telemetry configures the recorder and the debug dashboard. It is
	// off unless asked for: the zero value disables it, and NewOptions
	// disables it too, because the dashboard reports the internals of the
	// process and is unauthenticated unless Telemetry.Authorize, its
	// network allow list or its users say otherwise. Set Enabled, or
	// PLATFORM_TELEMETRY_ENABLED, to record.
	Telemetry oida.Options
}

Options is a configuration struct for platform behaviour.

func NewOptions

func NewOptions() *Options

NewOptions provides default options for the platform.

func NewTestOptions

func NewTestOptions() *Options

NewTestOptions produces default options for tests.

func OptionsFromContext added in v0.3.4

func OptionsFromContext(ctx context.Context) *Options

OptionsFromContext returns the *Options instance attached to the context.

func OptionsFromRequest added in v0.3.4

func OptionsFromRequest(r *http.Request) *Options

OptionsFromRequest returns the *Options instance attached to the request.

type Platform

type Platform struct {
	// Logger receives the platform's own output. New sets it to
	// slog.Default(), or to a discarding logger when Options.Quiet is set.
	// Assign to it before Start to reuse the platform logger from a test,
	// or to route output into a consumer application's logger.
	Logger Logger
	// contains filtered or unexported fields
}

Platform is our world struct.

func FromContext added in v0.0.2

func FromContext(ctx context.Context) *Platform

FromContext returns the *Platform instance attached to the context.

func FromRequest added in v0.0.2

func FromRequest(r *http.Request) *Platform

FromRequest returns the *Platform instance attached to the request.

func New

func New(options *Options) *Platform

New will create a new *Platform object. It is the allocation point for each platform instance. If no options are passed, the defaults are in use. The defaults options are provided by NewOptions().

func Start

func Start(ctx context.Context, options *Options) (*Platform, error)

Start is a shorthand to create a new *Platform instance and immediately starts the server listener and handles requests.

func (*Platform) Context

func (p *Platform) Context() context.Context

Context returns the cancellation context for the service. When the context finishes, the server has shut down.

func (*Platform) Find added in v0.0.2

func (p *Platform) Find(target any) bool

Find fills target with the module matching the type.

func (*Platform) Register

func (p *Platform) Register(m Module)

Register will add a registry.Module into the internal platform registry. This function should be called before Serve is called.

func (*Platform) Start

func (p *Platform) Start(ctx context.Context) error

Start starts the server, writes Options.PidFile and prints the registered routes. It stops on a cancelled context, SIGINT or SIGTERM.

func (*Platform) Stats

func (p *Platform) Stats() (int, int)

Stats will report how many middlewares and plugins are added to the registry.

func (*Platform) Stop

func (p *Platform) Stop()

Stop will gracefully shutdown the server and then cancel the server context when done.

Stop is an important part of the lifecycle tests. When closing the registry, each plugins Stop function gets invoked in parallel. This enables the plugin to clear background goroutine event loops, or flush a dirty buffer to storage.

Only after the server has fully shut down does the internal context get cancelled.

func (*Platform) URL

func (p *Platform) URL() string

URL gives the e2e endpoint URL for requests.

func (*Platform) Use

func (p *Platform) Use(m Middleware)

Use will add a middleware to the internal platform registry. This function should be called before Serve is called.

func (*Platform) Wait

func (p *Platform) Wait()

Wait will pause until the server is shut down.

type Registry

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

Registry provides a programmatic API to manage middleware and modules. A module registers middleware and has a contract to enforce lifecycle.

func (*Registry) Cleanup added in v0.2.1

func (r *Registry) Cleanup(fn func(context.Context))

Cleanup registers a function to run when the registry closes, as testing.T.Cleanup does for a test. The cleanups are registered in Start, and run in Close.

func (*Registry) Clone

func (r *Registry) Clone() *Registry

Clone provides a copy of the registry for use in the platform. Modules registered as constructors are built here, one set per clone.

func (*Registry) Close

func (r *Registry) Close(ctx context.Context)

Close will invoke all the modules close functions in parallel. When finished, it will clear the registered modules list, as well as any defined middleware and invoked cleanups.

func (*Registry) Find added in v0.0.2

func (r *Registry) Find(target any) bool

Find gets a Module from the registry. The target argument can be a pointer or an interface. The function returns true if a module matching the type or interface was found and assigned to `target`.

func (*Registry) Register deprecated

func (r *Registry) Register(m Module)

Register adds a Module to the registry.

Deprecated: use RegisterFunc. One value is shared by every platform that clones the registry, including the generations of a reload, so its state outlives the platform it was started with.

func (*Registry) RegisterFunc added in v0.7.1

func (r *Registry) RegisterFunc(f func() Module)

RegisterFunc adds a module constructor to the registry. Clone calls it, so every platform starts a module of its own.

func (*Registry) Start

func (r *Registry) Start(ctx context.Context, mux Router, opts *Options) error

Start will invoke all the modules start functions sequentially. If an error occurs, execution is halted and an error is returned. The context is passed along for observability and access to the platform. The registry's own output goes to the logger of the platform in the context; without one it is discarded.

func (*Registry) Stats

func (r *Registry) Stats() (modules, middleware int)

Stats returns counts for modules and middlewares in the registry.

func (*Registry) Use

func (r *Registry) Use(f Middleware)

Use adds a Middleware to the registry.

type Router

type Router = chi.Router

Router is a local shim that aliases the chi router interface.

type TelemetryModule added in v0.6.0

type TelemetryModule struct {
	UnimplementedModule
	// contains filtered or unexported fields
}

TelemetryModule records the telemetry of a platform service. It is the recorder and the dashboard at once: HTTP middleware recording every request, and a module mounting the debug front end under Options.Path.

New registers one by default. A host that wires its own recorder disables Options.Telemetry, so only one dashboard is on the path and only one middleware records the request.

func NewTelemetryModule added in v0.6.0

func NewTelemetryModule(options oida.Options) (*TelemetryModule, error)

NewTelemetryModule returns a telemetry module recording into its own tracer. The tracer is explicit rather than the process wide one, so two services, or two tests, do not record into each other.

func (*TelemetryModule) Middleware added in v0.6.0

func (m *TelemetryModule) Middleware(next http.Handler) http.Handler

Middleware records requests handled by next.

func (*TelemetryModule) Mount added in v0.6.0

func (m *TelemetryModule) Mount(_ context.Context, r Router) error

Mount registers the debug front end on the platform router. The tracer is an http.Handler serving its own dashboard, and oida.Mount adds the subtree patterns each router understands.

func (*TelemetryModule) Options added in v0.6.0

func (m *TelemetryModule) Options() oida.Options

Options returns the options the tracer runs on, which is what the module was built with after the environment was applied. The retention driver is left out of the copy; a caller that needs it holds the storage it configured.

func (*TelemetryModule) Tracer added in v0.6.0

func (m *TelemetryModule) Tracer() *oida.Tracer

Tracer returns the tracer the module records into.

type UnimplementedModule

type UnimplementedModule struct {
	NameFn  func() string
	StartFn func(context.Context) error
	StopFn  func(context.Context) error
	MountFn func(context.Context, Router) error
}

UnimplementedModule implements the module contract. The module can embed the type to skip implementing any of the bound functions.

func NewUnimplementedModule added in v0.2.1

func NewUnimplementedModule(name string) *UnimplementedModule

NewUnimplementedModule will fill the module name.

func (UnimplementedModule) Mount

Mount returns nil (no error).

func (UnimplementedModule) Name

func (m UnimplementedModule) Name() string

Name returns an empty string.

func (UnimplementedModule) Start

Start returns nil (no error).

func (UnimplementedModule) Stop

Stop returns nil (no error).

Directories

Path Synopsis
cmd
platform command
assert
Package assert is a minimalistic drop-in replacement for testify/assert.
Package assert is a minimalistic drop-in replacement for testify/assert.
pidfile
Package pidfile records a process id in a file, for a service manager or a command line that signals the process later.
Package pidfile records a process id in a file, for a service manager or a command line that signals the process later.

Jump to

Keyboard shortcuts

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