Procyon|Docs
Runtime

Context

Work with the central runtime context after the application starts.

runtime.Context is the application boundary passed to runtime extensions. It embeds context.Context, exposes the environment, gives access to the component container, and owns refresh/close lifecycle behavior.

The interface shows what the runtime context is responsible for:

context.go
type Context interface {
	// context.Context provides cancellation and scoped values.
	context.Context

	// Lifecycle gives the runtime context start and stop behavior.
	Lifecycle

	// Environment returns profiles, property sources, and property resolution.
	Environment() Environment

	// Container returns the component container for resolving or inspecting components.
	Container() component.Container

	// ResourceResolver returns the shared runtime resource resolver.
	ResourceResolver() io.ResourceResolver

	// Refresh prepares the container and initializes the component graph.
	Refresh(ctx context.Context) error

	// Close stops lifecycle components and disposes initialized singletons.
	Close(ctx context.Context) error
}

context.Context gives runtime-aware code cancellation and request-scoped signals in the usual Go shape.

Lifecycle means the context has start and stop behavior. The application calls that lifecycle while booting and closing the process.

Environment() returns profiles and configuration sources that were prepared before component creation.

Container() returns the component container for framework-level code that must inspect or dynamically resolve components.

ResourceResolver() returns the shared resolver used for runtime resources such as configuration files.

Refresh() builds the application container and initializes the component graph. Close() stops lifecycle objects and disposes initialized singletons.

Use it when code is part of the runtime flow: command-line runners, context initializers, lifecycle resources, framework packages, and integration code that needs to inspect application state.

context.go
type DiagnosticsRunner struct{}

func (r *DiagnosticsRunner) Run(ctx runtime.Context, args *runtime.Args) error {
    env := ctx.Environment()
    container := ctx.Container()
    resolver := ctx.ResourceResolver()
    configResource, err := resolver.Resolve(ctx, "application.yaml")
    if err != nil {
        return err
    }

    slog.Info("runtime diagnostics",
        "profiles", env.ActiveProfiles(),
        "components", len(container.DefinitionNames()),
        "hasConfig", configResource.Exists(),
    )

    return nil
}

Context initializer

Use a runtime.ContextInitializer when you need to customize the context before it is refreshed and used by the application.

initializer.go
type ObservabilityInitializer struct{}

func NewObservabilityInitializer() *ObservabilityInitializer {
    return &ObservabilityInitializer{}
}

func (i *ObservabilityInitializer) InitializeContext(ctx runtime.Context) error {
    enabled := ctx.Environment().
        PropertyResolver().
        LookupOrDefault("observability.enabled", false)

    if enabled == true {
        slog.Info("observability is enabled")
    }

    return nil
}

Register the initializer as a component:

components.go
func init() {
    component.Register(NewObservabilityInitializer)
}

Refresh and close

The application calls Refresh during startup. Refresh creates the application container, loads component definitions, runs container customizers, registers initialization processors, initializes singletons, resolves the lifecycle manager, and starts lifecycle components.

Close stops lifecycle components, destroys initialized singletons, and marks the context as canceled.

Most applications let Application.Run call these methods. Extension code should only call Refresh or Close directly in tests or when building framework-level runtime packages.

Runtime values available for injection

During refresh, Procyon registers core runtime values so constructors can depend on them:

service.go
func NewDiagnostics(
    ctx runtime.Context,
    env runtime.Environment,
    resolver io.ResourceResolver,
) *Diagnostics {
    return &Diagnostics{ctx: ctx, env: env, resolver: resolver}
}

Use these dependencies for infrastructure and framework integration code. For normal business services, prefer passing only the specific services or typed configuration they actually need.