runtime

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package runtime is the Isopace component host: it owns the lifecycle of the long-running pieces of a deployment (links, listeners, switches, flows) and the cross-cutting plumbing they share — configuration, structured logging (log/slog), and an observability facade for traces and metrics.

A Host starts registered Component values in registration order and stops them in reverse, so dependencies come up before dependents and go down after. Components can also be deployed and undeployed while the host runs, which the Deployer uses to turn a directory of declarative JSON Descriptor files into running components and to hot-redeploy them when the files change.

Dependency policy

The package is stdlib-only, like the rest of Isopace. Observability is exposed as the Observer interface with a no-op default and an slog-backed implementation; a real OpenTelemetry exporter is a drop-in adapter that implements Observer without the core module taking on the dependency. Configuration loads from JSON plus environment overrides; YAML is a drop-in once a permissively licensed parser is vendored.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrDuplicate = errors.New("runtime: component already registered")
	ErrNotFound  = errors.New("runtime: component not found")
	ErrRunning   = errors.New("runtime: host already started")
)

Host errors.

Functions

func NewLogger

func NewLogger(w io.Writer, opts LogOptions) *slog.Logger

NewLogger builds a *slog.Logger writing to w with the given options. It is the one place runtime constructs handlers, so deployments get consistent output.

func ParseLevel

func ParseLevel(s string, def slog.Level) slog.Level

ParseLevel maps a case-insensitive level name ("debug", "info", "warn", "error") to an slog.Level, falling back to def for anything unrecognised.

Types

type Attr

type Attr struct {
	Key   string
	Value any
}

Attr is a single key/value annotation on a span or measurement.

func A

func A(key string, value any) Attr

A builds an Attr; it reads well inline: obs.Counter("tx").Add(1, runtime.A("mti", "0200")).

type Component

type Component interface {
	Name() string
	Start(ctx context.Context) error
	Stop(ctx context.Context) error
}

Component is a managed unit with a start/stop lifecycle. Start should return promptly, launching any long-running work on its own goroutines; Stop must quiesce that work and release resources before it returns. Both calls are bounded by their ctx, so a slow component does not stall host shutdown indefinitely. Name identifies the component in logs and for undeploy.

Implementations must tolerate Stop being called after a failed Start (so the host can unwind a partial startup) and need not be safe for concurrent Start/Stop — the host serialises lifecycle calls per component.

type Config

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

Config is a hierarchical configuration tree loaded from JSON and overlaid with environment variables. Values are addressed by dot-separated paths ("link.timeout", "logging.level"). It is safe for concurrent reads, and Config.Reload swaps the whole tree atomically so a ConfigWatcher can apply edits to a live process.

(YAML is a drop-in: parse it into the same map[string]any tree and the rest of this type is unchanged. JSON keeps the core dependency-free today.)

func Load

func Load(path string, opts ...LoadOption) (*Config, error)

Load reads and parses a JSON config file, then applies environment overrides.

func Parse

func Parse(data []byte, opts ...LoadOption) (*Config, error)

Parse builds a Config from JSON bytes (no file backing, so Reload is a no-op).

func (*Config) Bool

func (c *Config) Bool(path string, def bool) bool

Bool returns the boolean at path, accepting JSON bools and "true"/"false".

func (*Config) Duration

func (c *Config) Duration(path string, def time.Duration) time.Duration

Duration returns the time.Duration at path. It accepts a duration string ("250ms", "5s") or a JSON number interpreted as seconds.

func (*Config) Get

func (c *Config) Get(path string) (any, bool)

Get returns the raw value at path and whether it was present.

func (*Config) Int

func (c *Config) Int(path string, def int) int

Int returns the integer at path, accepting JSON numbers and numeric strings.

func (*Config) Reload

func (c *Config) Reload() error

Reload re-reads the source file and re-applies environment overrides, swapping the tree atomically. It returns nil without changes for a Config that was parsed from bytes (no file backing).

func (*Config) String

func (c *Config) String(path, def string) string

String returns the string at path, or def if absent or not a string.

func (*Config) Unmarshal

func (c *Config) Unmarshal(v any) error

Unmarshal decodes the whole config tree into v (a pointer to a struct), using encoding/json tags.

func (*Config) UnmarshalKey

func (c *Config) UnmarshalKey(path string, v any) error

UnmarshalKey decodes the subtree at path into v.

type ConfigWatcher

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

ConfigWatcher reloads a Config when its backing file changes and invokes an optional callback with the refreshed config. It is a Component, so a Host can supervise hot configuration reload. Change detection polls the file's modification time and size, which is portable and dependency-free.

func NewConfigWatcher

func NewConfigWatcher(cfg *Config, opts ...WatcherOption) *ConfigWatcher

NewConfigWatcher builds a watcher for cfg. The config must be file-backed (loaded via Load); a bytes-parsed config never changes on disk.

func (*ConfigWatcher) Name

func (w *ConfigWatcher) Name() string

Name identifies the watcher as a component.

func (*ConfigWatcher) Start

func (w *ConfigWatcher) Start(context.Context) error

Start records the current file stamp and begins polling.

func (*ConfigWatcher) Stop

Stop ends the poll loop.

type Counter

type Counter interface {
	Add(n int64, attrs ...Attr)
}

Counter accumulates a monotonic total.

type Deployer

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

Deployer turns a directory of Descriptor files into running components on a Host and keeps them in sync with the directory: a new or changed file is (re)deployed and a removed or disabled one is undeployed. It is itself a Component, so a host can supervise it; while running it rescans the directory on an interval, giving hot (re)deploy.

func NewDeployer

func NewDeployer(host *Host, reg *Registry, dir string, opts ...DeployerOption) *Deployer

NewDeployer builds a Deployer that materialises *.json descriptors from dir onto host using reg.

func (*Deployer) Name

func (d *Deployer) Name() string

Name identifies the deployer as a component.

func (*Deployer) Scan

func (d *Deployer) Scan(ctx context.Context) error

Scan reconciles the descriptor directory with the host once: it deploys new and changed descriptors and undeploys removed or disabled ones. It is safe to call directly (e.g. in tests) as well as from the rescan loop.

func (*Deployer) Start

func (d *Deployer) Start(context.Context) error

Start begins the rescan loop, which performs an immediate first scan and then rescans on the configured interval. The first scan runs on the loop goroutine (not under Start) so that, when the deployer is itself hosted, it does not re-enter the host's lifecycle lock via Deploy while the host starts it.

func (*Deployer) Stop

func (d *Deployer) Stop(context.Context) error

Stop ends the rescan loop. Components deployed by the deployer are owned by the host and are stopped by it, not here.

type DeployerOption

type DeployerOption func(*Deployer)

DeployerOption configures a Deployer.

func WithScanInterval

func WithScanInterval(d time.Duration) DeployerOption

WithScanInterval sets the rescan period for hot redeploy (default 5s).

type Descriptor

type Descriptor struct {
	Name    string          `json:"name"`
	Type    string          `json:"type"`
	Enabled bool            `json:"enabled"`
	Config  json.RawMessage `json:"config,omitempty"`
}

Descriptor is a declarative deployment record: it names a component, the factory type that builds it, and an opaque per-component configuration block. A directory of these (one JSON object per file) is the deployable unit a Deployer watches.

type Env

type Env struct {
	Log    *slog.Logger
	Obs    Observer
	Config *Config
}

Env is the shared runtime context handed to component factories: the logger, the observability facade, and the root configuration. Passing it explicitly keeps components free of package-level globals and makes them testable in isolation. The zero Env is unusable; obtain one from a Host via Host.Env.

func (*Env) Logger

func (e *Env) Logger() *slog.Logger

Logger returns the environment logger, or the default logger if unset, so callers never have to nil-check.

func (*Env) Observer

func (e *Env) Observer() Observer

Observer returns the environment observer, or a no-op observer if unset.

type Factory

type Factory func(name string, config json.RawMessage, env *Env) (Component, error)

Factory builds a Component from a descriptor's config block and the shared environment. Registered by type in a Registry.

type Histogram

type Histogram interface {
	Observe(v float64, attrs ...Attr)
}

Histogram records a distribution of observed values.

type Host

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

Host owns the lifecycle of a set of Component values. It starts them in registration order and stops them in the reverse order, so a component is always started after — and stopped before — the components it depends on. Components may also be added and removed while the host runs via Host.Deploy and Host.Undeploy.

All lifecycle methods are serialised on a single lock; a Component's Start or Stop must not call back into its Host *synchronously* (within the Start/Stop call itself), which would re-enter the non-reentrant lock. A component may freely interact with the host from its own background goroutines after Start returns — that is ordinary lock contention, not re-entrancy, and is how the Deployer drives Deploy/Undeploy.

func New

func New(opts ...Option) *Host

New builds a Host. Defaults: slog.Default logger, no-op observer, empty config, 30s shutdown timeout.

func (*Host) Components

func (h *Host) Components() []string

Components returns the registered component names in registration order.

func (*Host) Deploy

func (h *Host) Deploy(ctx context.Context, c Component) error

Deploy adds a component and, if the host is already running, starts it immediately. Before Start it behaves like Register.

func (*Host) Env

func (h *Host) Env() *Env

Env returns the shared environment handed to component factories.

func (*Host) Register

func (h *Host) Register(c Component) error

Register adds a component before the host starts. After Start, use Deploy.

func (*Host) Run

func (h *Host) Run(ctx context.Context) error

Run starts the host, blocks until ctx is cancelled, then stops it within the shutdown timeout. Pass a context from signal.NotifyContext for signal-driven shutdown. The returned error joins a start error (if any) with the stop error.

func (*Host) Start

func (h *Host) Start(ctx context.Context) error

Start starts every registered component in registration order. If one fails, the components already started are stopped in reverse and the error returned.

func (*Host) Stop

func (h *Host) Stop(ctx context.Context) error

Stop stops all running components in reverse registration order, joining any stop errors. The host can be started again afterwards.

func (*Host) Undeploy

func (h *Host) Undeploy(ctx context.Context, name string) error

Undeploy stops a component (if running) and removes it from the registry.

type LoadOption

type LoadOption func(*loadCfg)

LoadOption configures Load / Parse.

func WithEnvPrefix

func WithEnvPrefix(prefix string) LoadOption

WithEnvPrefix enables environment overrides: a variable PREFIX_A_B becomes the config path "a.b". The value is parsed as JSON when possible (so "5" is a number and "true" a bool), otherwise kept as a string. An empty prefix (the default) disables overrides. An override whose path would have to traverse through a non-map scalar already declared in the file is skipped, so a malformed deeper override never silently destroys declared config.

func WithEnviron

func WithEnviron(env []string) LoadOption

WithEnviron supplies the environment to read overrides from, in os.Environ "KEY=value" form. Intended for tests; defaults to os.Environ().

type LogFormat

type LogFormat int

LogFormat selects the slog handler used by NewLogger.

const (
	// LogText uses slog's human-readable text handler.
	LogText LogFormat = iota
	// LogJSON uses slog's structured JSON handler (production default).
	LogJSON
)

type LogOptions

type LogOptions struct {
	// Level is the minimum level to emit (default slog.LevelInfo).
	Level slog.Level
	// Format selects text or JSON output (default LogText).
	Format LogFormat
	// AddSource includes source file:line in records.
	AddSource bool
}

LogOptions configures NewLogger.

type NopObserver

type NopObserver struct{}

NopObserver discards everything. It is the zero-cost default so instrumented code can call the facade unconditionally.

func (NopObserver) Counter

func (NopObserver) Counter(string) Counter

func (NopObserver) Histogram

func (NopObserver) Histogram(string) Histogram

func (NopObserver) StartSpan

func (NopObserver) StartSpan(ctx context.Context, _ string, _ ...Attr) (context.Context, Span)

type Observer

type Observer interface {
	// StartSpan begins a unit of work. The returned context carries the span
	// for propagation; the caller must End the returned Span (typically via
	// defer). attrs annotate the span.
	StartSpan(ctx context.Context, name string, attrs ...Attr) (context.Context, Span)
	// Counter returns a monotonically increasing instrument by name.
	Counter(name string) Counter
	// Histogram returns a distribution instrument by name (e.g. latencies).
	Histogram(name string) Histogram
}

Observer is the traces-and-metrics facade. It is deliberately small and dependency-free so the core never imports a telemetry SDK: the default is NopObserver, SlogObserver emits spans and measurements to a slog logger, and a real OpenTelemetry bridge is an external adapter that implements this interface. A nil Observer must never be dereferenced — use Env.Observer, which substitutes a no-op.

type Option

type Option func(*Host)

Option configures a Host.

func WithConfig

func WithConfig(c *Config) Option

WithConfig sets the root configuration shared with components.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the host (and component env) logger.

func WithObserver

func WithObserver(o Observer) Option

WithObserver sets the observability facade shared with components.

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) Option

WithShutdownTimeout bounds how long Run waits for Stop to complete.

type Registry

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

Registry maps descriptor types to factories. It is safe for concurrent use.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty factory registry.

func (*Registry) Build

func (r *Registry) Build(d Descriptor, env *Env) (Component, error)

Build instantiates the component described by d.

func (*Registry) Register

func (r *Registry) Register(typ string, f Factory)

Register binds a factory to a type name, replacing any previous binding.

type SlogObserver

type SlogObserver struct {
	Log *slog.Logger
}

SlogObserver implements Observer by emitting spans and measurements as structured log records. It is a useful default in development and a faithful stand-in until a real metrics/tracing backend is wired in. It is safe for concurrent use (slog.Logger is).

func (SlogObserver) Counter

func (o SlogObserver) Counter(name string) Counter

func (SlogObserver) Histogram

func (o SlogObserver) Histogram(name string) Histogram

func (SlogObserver) StartSpan

func (o SlogObserver) StartSpan(ctx context.Context, name string, attrs ...Attr) (context.Context, Span)

StartSpan logs span.start immediately and returns a span that logs span.end when ended; slog timestamps both records, so duration is recoverable from the log without this facade tracking wall-clock itself.

type Span

type Span interface {
	End()
	SetError(err error)
	SetAttr(attrs ...Attr)
}

Span is an in-flight unit of work. End records its completion; it must be called exactly once. SetError marks the span as failed.

type WatcherOption

type WatcherOption func(*ConfigWatcher)

WatcherOption configures a ConfigWatcher.

func OnReload

func OnReload(fn func(*Config)) WatcherOption

OnReload registers a callback invoked after each successful reload.

func WithWatchInterval

func WithWatchInterval(d time.Duration) WatcherOption

WithWatchInterval sets the poll period (default 2s).

func WithWatchLogger

func WithWatchLogger(l *slog.Logger) WatcherOption

WithWatchLogger sets the watcher's logger (default slog.Default).

Jump to

Keyboard shortcuts

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