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 ¶
- Variables
- func NewLogger(w io.Writer, opts LogOptions) *slog.Logger
- func ParseLevel(s string, def slog.Level) slog.Level
- type Attr
- type Component
- type Config
- func (c *Config) Bool(path string, def bool) bool
- func (c *Config) Duration(path string, def time.Duration) time.Duration
- func (c *Config) Get(path string) (any, bool)
- func (c *Config) Int(path string, def int) int
- func (c *Config) Reload() error
- func (c *Config) String(path, def string) string
- func (c *Config) Unmarshal(v any) error
- func (c *Config) UnmarshalKey(path string, v any) error
- type ConfigWatcher
- type Counter
- type Deployer
- type DeployerOption
- type Descriptor
- type Env
- type Factory
- type Histogram
- type Host
- func (h *Host) Components() []string
- func (h *Host) Deploy(ctx context.Context, c Component) error
- func (h *Host) Env() *Env
- func (h *Host) Register(c Component) error
- func (h *Host) Run(ctx context.Context) error
- func (h *Host) Start(ctx context.Context) error
- func (h *Host) Stop(ctx context.Context) error
- func (h *Host) Undeploy(ctx context.Context, name string) error
- type LoadOption
- type LogFormat
- type LogOptions
- type NopObserver
- type Observer
- type Option
- type Registry
- type SlogObserver
- type Span
- type WatcherOption
Constants ¶
This section is empty.
Variables ¶
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 ¶
Types ¶
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) Duration ¶
Duration returns the time.Duration at path. It accepts a duration string ("250ms", "5s") or a JSON number interpreted as seconds.
func (*Config) Reload ¶
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).
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.
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) Scan ¶
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 ¶
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.
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 ¶
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.
type Factory ¶
Factory builds a Component from a descriptor's config block and the shared environment. Registered by type in a Registry.
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 ¶
New builds a Host. Defaults: slog.Default logger, no-op observer, empty config, 30s shutdown timeout.
func (*Host) Components ¶
Components returns the registered component names in registration order.
func (*Host) Deploy ¶
Deploy adds a component and, if the host is already running, starts it immediately. Before Start it behaves like Register.
func (*Host) Run ¶
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 ¶
Start starts every registered component in registration order. If one fails, the components already started are stopped in reverse and the error returned.
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 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
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 ¶
WithConfig sets the root configuration shared with components.
func WithLogger ¶
WithLogger sets the host (and component env) logger.
func WithObserver ¶
WithObserver sets the observability facade shared with components.
func WithShutdownTimeout ¶
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.
type SlogObserver ¶
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 ¶
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).