Help us shape the future of Go! Take the 2026 Go Developer Survey

application

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 24 Imported by: 0

README

Own a service lifecycle

Use application when one process must start dependencies in order, supervise workers, and release acquired resources on failure or shutdown. Construct dependencies explicitly. A Composer registers lifecycle work; the application controls execution and exposes logging, events, telemetry, and probes.

Register work with its cleanup owner

Given a configured, unstarted *server.Server named listener, this function takes ownership of its lifecycle. Run returns after startup failure or after waiting and shutdown; the caller uses its error for exit status or recovery. The snippet uses context, arkoren.dev/core/application, and arkoren.dev/core/http/server.

func runService(ctx context.Context, listener *server.Server) error {
	app := application.New()

	return app.Run(ctx, func(compose *application.Composer) {
		compose.Startup(listener.Start)
		compose.Cleanup(listener.Stop)
		compose.Run(listener.Serve)
	})
}

At a process entry point, obtain ctx with application.NewSignalContext and immediately defer its stop function.

All composition finishes before telemetry and startup callbacks run. Cleanup is active as soon as it is registered and executes once in reverse order, including when a later composition or startup callback fails. Register releases at the acquiring owner, not at every borrower. Defer puts an infallible release on this same stack; it does not run at the end of the composing Go function.

An executing startup callback can register Cleanup or Defer for resources it acquires. It cannot add startup callbacks, workers, checks, or diagnostic tools. Must, MustDefer, Require, and child composition are composition-only.

Cancellation is cooperative. Runners and checks must honor their contexts; the application does not forcibly interrupt arbitrary callbacks. Shutdown can stop waiting for runners when its context expires, but still attempts acquired cleanup. Cleanup and telemetry finalization receive independent minimum budgets, so the configured runner-wait timeout is not a hard wall-clock bound on Run.

Distinguish readiness from liveness

Register dependency checks during composition, and mount probes explicitly on your router. Reading a readiness probe never executes a dependency check.

compose.Check("database", connection.Ping)
routes.Get(application.LivePath, app.LiveHandler(), application.LiveOperation)
routes.Get(application.ReadyPath, app.ReadyHandler(), application.ReadyOperation)
routes.Get(application.StartupPath, app.StartupHandler(), application.StartupOperation)

Here connection supplies Ping(context.Context) error, routes is your router, and app is the composing application. The first background pass starts immediately; later passes follow a ticker, not a full pause after each completion. Passes do not overlap. A result older than three readiness intervals is stale and does not permit traffic, even if its last verdict passed. RunChecks and CheckHealth explicitly run checks without replacing the background cache.

?verbose returns cached readiness details. Error text is withheld unless you enable HealthDetails, which also affects MCP health diagnostics. Enable that setting only when all affected endpoints are operator-protected. The separate InfoHandler also needs operator access because it exposes build and runtime identity. The welcome handler includes neither kind of information.

Capture diagnostics without changing lifecycle ordering

During composition, compose.MCPTools("app") attaches local diagnostic capture and returns SDK-independent tools. It does not mount a transport or establish authentication or authorization. Select tools, apply capability policy, and register them on a protected MCP endpoint in your composition.

tools := compose.Must(compose.MCPTools("app"))
_ = tools // Register the selected tools with your MCP adapter here.

Capture activates after composition, before startup callbacks. A logger obtained earlier reaches that capture once telemetry starts. Without an explicit driver or diagnostic capture, application telemetry records nothing. Local log correlation is separate: a valid span context adds trace and span IDs even before application startup.

The Stopped event runs after runner waiting and cleanup, while state is still StateStopping and telemetry can accept a listener's final records. Telemetry finalization and publication of StateStopped follow. To observe Stopped or rollback Failed events, keep the subscription until Run returns; do not unsubscribe it on the application's cleanup stack before the event is emitted.

The application reports lifecycle failures before telemetry finalization. Finalization errors go to the configured local handler instead of back to the failed exporter. Do not duplicate this reporting by logging Run's result after telemetry has stopped.

See the application API for construction defaults, callback phases, event payloads, and examples.

Documentation

Overview

Package application coordinates ordered startup, workers, health checks, lifecycle events, and graceful shutdown. It has no dependency-injection layer; construct objects explicitly inside a Composer passed to Application.Start or Application.Run:

app := application.New()
err := app.Run(ctx, func(composer *application.Composer) {
	composer.Startup(connect)
	composer.Cleanup(disconnect)
	composer.Check("database", ping)
	composer.Run(serve)
})
if err != nil {
	return err
}

An Application is optional: the router, server, and services work without one. At the process entry point, NewSignalContext requests graceful shutdown on SIGINT or SIGTERM using standard os/signal cancellation, without terminal output changes. Always defer its stop function to release signal handling:

ctx, stop := application.NewSignalContext(context.Background())
defer stop()
if err := app.Run(ctx, compose); err != nil {
	return err
}

Composer.Startup, Composer.Cleanup, Composer.Defer, Composer.Run, and Composer.Check return nothing. They panic with ErrNilFunc for a nil function (or an empty check name) and with ErrInvalidPhase when called in a phase that does not allow registration. The panic value is an error wrapping the sentinel, so errors.Is works on the recovered value. Both are wiring mistakes found at startup, not runtime failures.

Composition callbacks execute synchronously, in order, before any startup hook. Composer.App exposes the existing application services, and Composer.Context carries startup cancellation. Composer.Must unwraps constructor results; Composer.Require aborts on an error. Both preserve the original error for Start or Run. Ordinary composition panics trigger cleanup and propagate unchanged, rather than being converted into lifecycle errors. Composer.Defer registers an infallible func() release on the same cleanup stack; Composer.MustDefer combines (release, error) acquisition with that registration, aborting composition on error. Neither release runs at the end of the enclosing Go function. MustDefer is composition-only like Must and Require; Defer is also allowed inside executing Startup callbacks.

Factory is a defined function type that returns a typed dependency from composition. Action is a defined function type for composition that returns nothing, such as route registration. Composer.Use executes exactly one Factory or Action inline on the same composer. Its result type is inferred from the supplied operation; ordinary typed parameters pass dependencies between operations. Root ComposerFunc callbacks remain ordinary void functions accepted directly by Start and Run:

makeRoutes := application.Factory[*router.Router](func(composer *application.Composer) *router.Router {
	return router.New()
})
err := app.Start(ctx, func(composer *application.Composer) {
	routes := composer.Use(makeRoutes)
	composer.Use(application.Action(func(*application.Composer) {
		routes.Get(application.LivePath, app.LiveHandler())
	}))
})
if err != nil {
	return err
}
// The owner must eventually call app.Shutdown with a shutdown context.

Construct resources inside the operation, not while obtaining it. There is no runtime registry, automatic lifecycle adoption, or memoization: each Use invokes the operation anew. Returning a resource does not start or stop it. Register cleanup once, immediately after acquisition, in its owning factory; consumers borrowing the returned dependency do not register another cleanup.

Composer.UseWhen, Composer.UseDebug, Composer.UseIn, and Composer.UseInAny execute one Factory or Action only when its condition matches. They return a (value, executed) pair: skipped factories yield the result type's zero value and false, while selected factories can legitimately return zero and true. Action callers normally ignore the pair. Constructor failures raised through Must, MustDefer, or Require abort Start or Run; they never become a false presence result. For example, a conditionally created router can be used only when present:

err := app.Start(ctx, func(composer *application.Composer) {
	if routes, executed := composer.UseDebug(makeRoutes); executed {
		routes.Get(application.LivePath, app.LiveHandler())
	}
})
if err != nil {
	return err
}

All operations share the parent composer, cleanup ownership, and failure handling. UseInAny matches any environment in its slice once; nil or empty slices skip execution. UseDebug tests the configured debug flag independently of environment and runtime log-level changes. These methods are composition-only. Nil operations, including typed nil Factory and Action values, and invalid phases panic even when the condition would skip execution. An untyped nil operation requires an explicit type argument, such as composer.UseWhen[int](false, nil), and still panics with ErrNilFunc.

Composer.Startup hooks run sequentially in registration order. Composer.Cleanup and Composer.Defer releases become active at registration and run in reverse order exactly once, including composition and startup failure. Cleanup is not paired positionally with startup hooks. A startup hook may register cleanup for a resource it acquired, but cannot add startup hooks, runners, checks, or diagnostic tools. Retained composers reject registration outside their permitted callback phase; they are not runtime service handles.

Composer.Run runners launch only after every start hook succeeded, on a context detached from the start context. A runner that returns an error or panics while not canceled fails the application and cancels the others. Application.Shutdown cancels the runners, waits for them, and then runs acquired cleanup. Application.Run combines Start, Wait, and Shutdown. Builder.ShutdownTimeout supplies the runner-wait budget; cleanup and telemetry finalization retain their minimum budgets. Callbacks must honor cancellation, so these deadlines do not forcibly interrupt user code.

Application.CheckHealth runs the checks registered with Composer.Check; check names are unique. Application.LiveHandler, Application.ReadyHandler, and Application.StartupHandler are router.Handler probes that answer "ok" or "unavailable". Application.ReadyHandler never runs checks per request: while the application runs, a background runner runs every check every Builder.ReadinessInterval with a context deadline from Builder.ReadinessTimeout, and the handler reads the latest result. Checks must honor cancellation; the application waits for them rather than forcibly interrupting callbacks. With ?verbose it answers the same status with a JSON report of each check, withholding errors unless Builder.HealthDetails is enabled. The runner logs each check that starts failing or recovers. Register the probes at LivePath, ReadyPath, and StartupPath:

r.Get(application.LivePath, app.LiveHandler())
r.Get(application.ReadyPath, app.ReadyHandler())
r.Get(application.StartupPath, app.StartupHandler())

Application.WelcomeHandler serves a self-contained HTML welcome page with only the escaped application name and Arkoren resource links. It includes no runtime, configuration, or build details. Register it explicitly at a chosen path; creating an application or handler never mounts routes:

r.Get("/", app.WelcomeHandler(), application.WelcomeOperation)

Application.InfoHandler is a separate opt-in handler that answers JSON describing the service (name, version, environment, state, log level, uptime) and the running binary (Go version, module, VCS revision). Register it at InfoPath. Verbose readiness and info reveal internals and are not protected; serve them where only operators can reach them, such as a second server on an internal port. Application.Info returns the same description as a Info value, and Application.Checks the cached state of each health check, for tools that inspect the running service.

Composer.MCPTools attaches bounded diagnostic capture during composition, before startup hooks, and returns native MCP tools over application state and telemetry. Composition selects a subset, applies capability policy, and registers those tools on a protected MCP endpoint; the factory creates no HTTP listener or authentication policy. Resource authorization remains in shared typed application methods.

Example:

var tools []mcp.Tool
err := app.Start(ctx, func(composer *application.Composer) {
	tools = composer.Must(composer.MCPTools("app"))
	// Select and register tools through the application's MCP adapter.
})
if err != nil {
	return err
}
_ = tools

The application dispatches Starting, Started, Stopping, Stopped, and Failed on Application.Events, and the readiness runner dispatches Ready, Unready, CheckFailed, and CheckRecovered on each transition. Pass Events() to the Events builder setting of components and to application code instead of the application; only the application dispatches its own topics. Listener errors are logged and never affect the lifecycle. The bus is an arkoren.dev/core/event.Bus; see that package for topics, listeners, and name rules.

The application owns a slog.Logger, returned by Application.Logger and tagged with an env attribute. Builder.Environment selects the Environment (Development, Staging, or Production); development logs text with source locations to stderr, every other environment logs JSON. Builder.Debug lowers the level to debug, and Application.SetLogLevel changes it at runtime. Builder.LogHandler replaces the handler; its factory receives the shared level. The lifecycle is logged as "app started", "app stopping", "app stopped", and "app failed". Shutdown wait and cleanup failures are logged as "app shutdown failed" before telemetry stops. Rollback includes cleanup failures in its "app failed" record. Callers use Run's error for exit status or recovery rather than logging it again after telemetry finalization. Pass Logger() to services, never the application.

Builder.Name and Builder.Version identify the service: when set, the logger also carries service and version attributes next to env, and telemetry drivers receive them in their resource. Without Builder.Version the version is the main module version the go command stamped into the binary, unless it is unknown ("(devel)").

Builder.Telemetry enables telemetry with a telemetry.Telemetry built by telemetry.New from a driver (the in-process memory driver, a composed set of drivers, ...). The application owns its lifecycle: telemetry starts after all composition succeeds and before user Startup callbacks. Starting is dispatched after telemetry startup and before user Startup callbacks, so listeners registered during composition can log through active telemetry. Composition or telemetry startup failure dispatches Failed without Starting. There is no built-in "app starting" log. Its Stop runs after acquired cleanup and final lifecycle logs/events on every exit path (normal stop, runner failure, and failed-start rollback), with the remaining shutdown budget but at least ten seconds. The logger's handler is always wrapped with telemetry.Telemetry.Handler, configured or not. Records logged with a valid span context carry trace_id and span_id in local output even before startup; export and diagnostic capture require started telemetry. A logger taken at any time reaches diagnostic capture attached later. The driver is started with the resource built from the configuration and the unwrapped logger, so exporter failures are not exported again. Application.Telemetry returns the stable application handle, like Application.Logger. Without a configured driver it remains a no-op unless Composer.MCPTools attaches local capture. Application.SetDefaults publishes the logger and telemetry as the process defaults (slog.SetDefault and telemetry.SetDefault); it is a method so the global mutation is visible at the call site.

app := application.New(
	func(app *application.Builder) {
		app.Name("billing")
		app.Version(version)
		app.Telemetry(telemetry.New(memorytelemetry.New()))
	},
)
app.SetDefaults()

The lifecycle states are StateNew, StateStarting, StateRunning, StateStopping, StateStopped, and StateFailed.

Construction

New invokes callbacks directly once, synchronously in order on one fresh Builder, then resolves defaults and wires services. Settings return that draft; retained drafts cannot reconfigure an application. Dependencies and handler closures remain shared. Omitted callbacks and nil/empty expanded slices make no edits; supplied nil entries and callback panics propagate. Composer.MCPTools has its own MCPToolsBuilder for capture CaptureLimits. While composition is active, it runs callbacks once outside the lifecycle lock and publishes SDK-independent owner tools for explicit application composition. Authentication, capability policies, and transport registration are external.

err := app.Run(ctx, func(composer *application.Composer) {
	tools := composer.Must(composer.MCPTools("app"))
	tools = append(tools, public.MCPTools("public")...)
	// Register the selected tools on a protected endpoint here.
	_ = tools
})
if err != nil {
	return err
}

Index

Examples

Constants

View Source
const (
	// LivePath is the liveness probe path served by [Application.LiveHandler].
	// Example: routes.Get(application.LivePath, app.LiveHandler())
	LivePath = "/livez"

	// ReadyPath is the readiness probe path served by [Application.ReadyHandler].
	// Example: routes.Get(application.ReadyPath, app.ReadyHandler())
	ReadyPath = "/readyz"

	// StartupPath is the startup probe path served by [Application.StartupHandler].
	// Example: routes.Get(application.StartupPath, app.StartupHandler())
	StartupPath = "/startupz"
)

Conventional probe paths, following the Kubernetes "z-pages" naming. Use them when registering the probe handlers so that routes and probe configuration stay in sync:

r.Get(application.LivePath, app.LiveHandler())
r.Get(application.ReadyPath, app.ReadyHandler())
r.Get(application.StartupPath, app.StartupHandler())
View Source
const InfoPath = "/infoz"

InfoPath is the conventional path of Application.InfoHandler:

r.Get(application.InfoPath, app.InfoHandler())

Variables

View Source
var (
	// ErrInvalidPhase reports an operation outside its lifecycle phase.
	// Lifecycle methods return it wrapped; invalid composer registration panics
	// with a wrapping error instead. Use errors.Is rather than string matching.
	//
	//
	//	if err := app.Start(ctx); err != nil {
	//		if errors.Is(err, application.ErrInvalidPhase) {
	//			return fmt.Errorf("startup requires a new application: %w", err)
	//		}
	//		return err
	//	}
	ErrInvalidPhase = lifecycle.ErrInvalidPhase

	// ErrNilFunc reports a registration without a function.
	// Composer registration panics with a wrapping error for nil callbacks or
	// empty check names; it is programmer misuse, not a runtime retry signal.
	//
	// Example: matched := errors.Is(registrationError, application.ErrNilFunc); fmt.Println(matched)
	ErrNilFunc = lifecycle.ErrNilFunc

	// ErrDuplicateCheck reports a health check registered under a name that
	// is already taken.
	// Composer.Check panics with a wrapping error before appending the duplicate.
	//
	// Example: matched := errors.Is(registrationError, application.ErrDuplicateCheck); fmt.Println(matched)
	ErrDuplicateCheck = health.ErrDuplicateCheck
)
View Source
var (
	// Starting is dispatched after composition and successful telemetry startup,
	// before user Startup callbacks. Listeners registered during composition can
	// log through the application's active telemetry. Composition or telemetry
	// startup failures dispatch Failed without Starting.
	//
	//	compose.MustDefer(events.Listen(application.Starting, onStarting))
	Starting = event.NewSignal("app.starting")

	// Started is dispatched once every runner has been launched.
	// The app is running, but background health can still be pending.
	//
	//	compose.MustDefer(events.Listen(application.Started, onStarted))
	Started = event.NewSignal("app.started")

	// Stopping is dispatched when shutdown begins.
	// It precedes runner cancellation and cleanup. Configured or diagnostic
	// telemetry is still active for the listener's final records.
	//
	//	compose.MustDefer(events.Listen(application.Stopping, onStopping))
	Stopping = event.NewSignal("app.stopping")

	// Stopped is dispatched after runner waiting and cleanup, before telemetry
	// finalization. State is still StateStopping, so listeners can emit their
	// final telemetry; Shutdown publishes StateStopped after finalization.
	// Runner waiting or cleanup may have failed; inspect Shutdown's result.
	//
	//	unsubscribe, err := app.Events().Listen(application.Stopped, onStopped)
	//	if err != nil {
	//		return err
	//	}
	//	defer unsubscribe() // Keep the subscription until Run has returned.
	//	return app.Run(ctx, compose)
	Stopped = event.NewSignal("app.stopped")

	// Failed is dispatched when startup fails or a runner fails.
	// Its payload carries the original failure; listeners cannot change lifecycle
	// outcomes. Listener errors and panics are logged by the event bus.
	//
	//	unsubscribe, err := app.Events().Listen(application.Failed, onFailed)
	//	if err != nil {
	//		return err
	//	}
	//	defer unsubscribe() // Rollback dispatches Failed after acquired cleanup.
	//	return app.Run(ctx, compose)
	Failed = event.NewTopic[FailedEvent]("app.failed")

	// Ready is dispatched when the first readiness pass succeeds and when the
	// verdict turns from not ready to ready.
	// It is a background-health observation, not a replacement for the live probe.
	//
	//	compose.MustDefer(events.Listen(application.Ready, onReady))
	Ready = event.NewSignal("app.ready")

	// Unready is dispatched when the first readiness pass fails and when the
	// verdict turns from ready to not ready.
	// The payload contains failing names, never backend error text.
	//
	//	compose.MustDefer(events.Listen(application.Unready, onUnready))
	Unready = event.NewTopic[UnreadyEvent]("app.unready")

	// CheckFailed is dispatched when a health check fails after passing, or
	// on its first readiness pass.
	// The payload's error is intended for trusted in-process listeners.
	//
	//	compose.MustDefer(events.Listen(application.CheckFailed, onCheckFailed))
	CheckFailed = event.NewTopic[CheckEvent]("app.check.failed")

	// CheckRecovered is dispatched when a health check passes after failing.
	// Its CheckEvent retains the name and has a nil Err.
	//
	//	compose.MustDefer(events.Listen(application.CheckRecovered, onRecovered))
	CheckRecovered = event.NewTopic[CheckEvent]("app.check.recovered")
)

Functions

func InfoOperation

func InfoOperation(operation *router.OperationBuilder)

InfoOperation describes the JSON Info returned by Application.InfoHandler without embedding application or binary values. Serve this endpoint only to operators: the annotation neither configures nor infers authentication.

Example:

routes.Get(application.InfoPath, app.InfoHandler(), application.InfoOperation)

func LiveOperation

func LiveOperation(operation *router.OperationBuilder)

LiveOperation describes Application.LiveHandler on the route chosen by the caller. It advertises plain 200/503 responses and probe headers, without running the handler, registering routes, or changing access policy.

Example:

routes.Get(application.LivePath, app.LiveHandler(), application.LiveOperation)

func NewSignalContext

func NewSignalContext(parent context.Context) (context.Context, context.CancelFunc)

NewSignalContext returns a child canceled by an interrupt (SIGINT), SIGTERM, parent cancellation, or stop. Use it at the process entry point to request graceful shutdown through Application.Run. Like signal.NotifyContext, it changes process-wide signal handling until stop releases its registration; always call stop, including after parent cancellation or startup failure. Stop is safe to call repeatedly and cancels the returned context. A nil parent panics, following the standard context convention.

Signal delivery follows the platform's os/signal support. This helper does not change terminal settings or erase the terminal's echoed ^C: cancellation behaves identically with terminal, redirected, and service-manager output.

Example:

ctx, stop := application.NewSignalContext(context.Background())
defer stop()
app := application.New()
if err := app.Run(ctx, compose); err != nil {
	return err
}

func ReadyOperation

func ReadyOperation(operation *router.OperationBuilder)

ReadyOperation describes Application.ReadyHandler, including the optional presence-based verbose query parameter. The output schema includes optional cached health fields, never actual check names or failures. Descriptions do not enable health details or infer authentication requirements.

Example:

routes.Get(application.ReadyPath, app.ReadyHandler(), application.ReadyOperation)

func StartupOperation

func StartupOperation(operation *router.OperationBuilder)

StartupOperation describes Application.StartupHandler without sampling lifecycle state. Success records completed startup, including during shutdown and after stop; this annotation installs no handler or middleware.

Example:

routes.Get(application.StartupPath, app.StartupHandler(), application.StartupOperation)

func WelcomeOperation

func WelcomeOperation(operation *router.OperationBuilder)

WelcomeOperation describes Application.WelcomeHandler at the caller's chosen path. It advertises HTML without capturing the application name or mounting an endpoint; the annotation does not change handler behavior.

Example:

routes.Get("/", app.WelcomeHandler(), application.WelcomeOperation)

Types

type Action

type Action func(*Composer)

Action composes dependencies without publishing a value. Action helpers return function literals of this defined type and defer construction and registration until invocation. Use returns an empty struct for an Action; conditional Use methods additionally return whether the action ran. Nothing is automatically started or released. A nil Action is invalid even when skipped.

bind := application.Action(func(composer *application.Composer) {
	composer.Run(serve)
})
composer.Use(bind)

type Application

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

Application coordinates startup hooks, runners, health checks, lifecycle events, and shutdown. Private lifecycle and health modules synchronize their state without holding locks while user callbacks run. Start and Run wire concrete dependencies before startup. Construction configures settings only.

app := application.New()
if err := app.Run(ctx, func(composer *application.Composer) {
	composer.Run(serve)
}); err != nil {
	return err
}

func New

func New(configure ...BuilderFunc) *Application

New creates an application in StateNew. Construction callbacks run directly once, synchronously in order on one fresh draft. Supplied nil callbacks and callback panics propagate; omitted callbacks and expanded nil/empty slices make no edits. Retained drafts cannot change the result. It does no lifecycle I/O; the configured LogHandler factory runs once.

app := application.New(func(app *application.Builder) {
	app.Name("billing")
})

func (*Application) CheckHealth

func (application *Application) CheckHealth(ctx context.Context) error

CheckHealth runs every registered check as Application.RunChecks does and returns their failures joined in registration order, each prefixed with its check name. It returns nil when every check passes. ctx is passed to each check with the RunChecks timeout; checks must cooperate with cancellation. This explicit run does not update the cached readiness result.

if err := app.CheckHealth(ctx); err != nil {
	return fmt.Errorf("dependencies unavailable: %w", err)
}

func (*Application) Checks

func (application *Application) Checks() []CheckStatus

Checks returns the state of every health check as of the latest completed background pass, in registration order. It never runs a check, so it is as cheap as the readiness probe and shows what the probe sees. It returns nil until the first pass completes: before the application runs, when no check is registered, and for a short time after Start. Use Application.CheckHealth to run the checks now. The result is a copy.

Example:

for _, status := range app.Checks() {
	fmt.Println(status.Name, status.Err, status.Since)
}

func (*Application) Debug

func (application *Application) Debug() bool

Debug reports the construction-time debug flag, independently of environment and changes made through SetLogLevel. It controls Composer.UseDebug selection.

Example: app := application.New(); fmt.Println(app.Debug()) // false

func (*Application) Environment

func (application *Application) Environment() Environment

Environment returns the construction-time deployment name. Empty configuration has already resolved to Development; custom nonempty names are unchanged.

Example: app := application.New(); fmt.Println(app.Environment())

func (*Application) Events

func (application *Application) Events() *event.Bus

Events returns the bus that receives the lifecycle topics Starting, Started, Stopping, Stopped, and Failed, and the readiness topics Ready, Unready, CheckFailed, and CheckRecovered. Pass it to the Events builder setting of Arkoren packages so they publish their topics on it, and to application code that listens or dispatches its own topics, instead of passing the application. Only the application dispatches the lifecycle and readiness topics.

consumer := queue.NewConsumer(driver, func(consumer *queue.ConsumerBuilder) {
	consumer.Events(app.Events())
})

func (*Application) Info

func (application *Application) Info() Info

Info returns the description Application.InfoHandler serves: name, version, environment, lifecycle state, log level, start time, uptime, and build. It is a snapshot taken now and safe to call in any phase; StartedAt, Uptime, and UptimeSeconds are zero, empty, and nil until the application has reached StateRunning. It reveals versions and revisions, so expose it only to operators.

Example:

info := app.Info()
fmt.Println(info.Name, info.State, info.Build.GoVersion)

func (*Application) InfoHandler

func (application *Application) InfoHandler() router.Handler

InfoHandler returns a handler that answers 200 with a JSON description of the application and the running binary:

{
  "name": "billing",
  "version": "v1.4.0",
  "environment": "production",
  "state": "running",
  "log_level": "INFO",
  "started_at": "2026-10-10T09:00:00Z",
  "uptime": "1h0m0s",
  "uptime_seconds": 3600,
  "build": {
    "go_version": "go1.27.1",
    "module_path": "example.com/billing",
    "module_version": "v1.4.0",
    "vcs_revision": "abc123",
    "vcs_time": "2026-10-09T18:00:00Z"
  }
}

Empty fields are omitted; started_at, uptime, and uptime_seconds appear once the application has reached StateRunning. log_level follows Application.SetLogLevel. The body exposes versions and revisions, so serve it where only operators can reach it.

internalRoutes.Get(application.InfoPath, app.InfoHandler(), application.InfoOperation)

func (*Application) Live

func (application *Application) Live() bool

Live reports whether the application is running, no runner has failed, and the runner group has not finished. It does not run health checks or inspect their cached verdict; use ReadyHandler for traffic readiness.

Example: app := application.New(); fmt.Println(app.Live()) // false

func (*Application) LiveHandler

func (application *Application) LiveHandler() router.Handler

LiveHandler returns a handler that answers 200 while the application is live (see Application.Live) and 503 otherwise. It reads lifecycle state only, never runs checks, and writes a non-cacheable, newline-terminated plain body.

routes.Get(application.LivePath, app.LiveHandler(), application.LiveOperation)

func (*Application) Logger

func (application *Application) Logger() *slog.Logger

Logger returns the stable application logger, tagged with env and configured service/version attributes. A valid span context adds trace_id and span_id to local output even before startup or without application telemetry. Export and diagnostic capture begin only when telemetry starts. A logger obtained before Composer.MCPTools still reaches capture attached later. Pass this handle to services instead of the application.

app := application.New()
app.Logger().InfoContext(ctx, "billing configured")

func (*Application) ReadyHandler

func (application *Application) ReadyHandler() router.Handler

ReadyHandler returns a handler that answers 200 while the application is live and the latest background health pass succeeded, and 503 otherwise. It never runs checks itself: while running, the application runs every check every Builder.ReadinessInterval, starting immediately, and each pass receives a deadline from Builder.ReadinessTimeout. Checks must honor it; the application cannot forcibly interrupt callbacks. It is not ready until the first pass succeeds, and a result older than three intervals counts as failed. Without registered checks readiness equals liveness.

With the verbose query parameter (?verbose, any value) it answers the same status with a JSON report of the latest pass instead of "ok" or "unavailable":

{
  "status": "unavailable",
  "checked_at": "2026-10-10T09:00:05Z",
  "checks": {
    "cache": {"status": "failed", "error": "reason withheld"},
    "database": {"status": "ok"}
  }
}

Failed checks report "reason withheld" unless Builder.HealthDetails is enabled; the reason is logged when a check starts failing either way. When the result cannot be trusted the report carries a reason ("not live", "no health pass completed", or "health result stale"), and checks only when a stale pass exists.

routes.Get(application.ReadyPath, app.ReadyHandler(), application.ReadyOperation)

func (*Application) Run

func (application *Application) Run(ctx context.Context, compose ...ComposerFunc) error

Run starts the application, waits for runners or ctx, then shuts down with the configured shutdown timeout. Composition has the same ordering, failure, and panic semantics as Application.Start. Lifecycle failures are already logged when Run returns. Telemetry has been finalized, so callers should use the result for exit status or recovery rather than logging it again. Telemetry finalization failures reach the log handler directly because export is no longer available.

if err := app.Run(ctx, func(composer *application.Composer) {
	composer.Run(serve)
}); err != nil {
	return err
}

func (*Application) RunChecks

func (application *Application) RunChecks(ctx context.Context) []CheckStatus

RunChecks runs every registered check now, concurrently and without holding any lock, and returns one CheckStatus per check in registration order, passing and failing alike. Each callback receives a context deadline from Builder.ReadinessTimeout and ctx. Callbacks must honor cancellation; RunChecks waits for all of them and cannot forcibly interrupt user code. Since is zero, because a single run has no history, and CheckedAt is when the run finished. It returns nil when no check is registered. A panicking check reports the recovered value. Use Application.Checks for the cached state instead.

Example:

for _, status := range app.RunChecks(ctx) {
	fmt.Println(status.Name, status.Err)
}

func (*Application) SetDefaults

func (application *Application) SetDefaults()

SetDefaults publishes the application's logger and telemetry as the process-wide defaults with slog.SetDefault and telemetry.SetDefault, so code that logs or traces without being handed them is correlated and exported. It is a method, not an option, so the global mutation is visible at the call site. It publishes the same handles, so diagnostic capture attached later during composition also reaches callers using the process defaults. Without a driver or diagnostic capture, the published telemetry records nothing.

app := application.New()
app.SetDefaults()
slog.InfoContext(ctx, "using application defaults")

func (*Application) SetLogLevel

func (application *Application) SetLogLevel(level slog.Level)

SetLogLevel changes the minimum log level at runtime. It affects every handler built on the application's level variable, without changing Debug or the construction-time decisions made by Composer.UseDebug.

Example: app := application.New(); app.SetLogLevel(slog.LevelWarn)

func (*Application) Shutdown

func (application *Application) Shutdown(ctx context.Context) error

Shutdown cancels the runners, waits for them until ctx ends, and then runs every cleanup callback exactly once in reverse order, even if waiting timed out. Cleanup receives an uncanceled context with the remaining budget of ctx but at least ten seconds. The application ends in StateStopped only after telemetry finalization. Concurrent and later calls return the result of that single pass. Shutdown after a failed Start returns nil because Start already cleaned up.

When telemetry is configured it is shut down last, after the "app stopped" log and the Stopped event, with the remaining budget of ctx but at least ten seconds, on every exit path; its error is joined into the result. These budgets are context deadlines, not forced interruption: a callback that ignores cancellation can keep cleanup or finalization from returning.

While starting, Shutdown cancels startup and waits for its owner to roll back; ctx bounds that wait. Shutdown must not be called synchronously from composition, Startup, a runner, or a Cleanup callback.

if err := app.Shutdown(ctx); err != nil {
	return err
}

func (*Application) Start

func (application *Application) Start(ctx context.Context, compose ...ComposerFunc) error

Start composes dependencies synchronously in order, starts telemetry, then executes Startup callbacks in order and launches runners and health checks. All composers finish before any Startup callback runs. On failure or context cancellation every registered Cleanup runs once in reverse order with a bounded uncanceled context, followed by telemetry finalization. Failures are logged by the application before telemetry finalization; errors before telemetry starts reach the configured log handler only. Starting is dispatched after composition and telemetry startup, before user Startup callbacks, so its listeners can log through active telemetry.

ctx supplies startup cancellation only; synchronous composition and startup callbacks must cooperate with it. Runners use a context that keeps ctx's values but is canceled solely by Shutdown or a runner failure. Composition programmer panics trigger best-effort rollback and are re-panicked; Startup, runner, and Cleanup panics retain the lifecycle panic-to-error behavior. Do not call Shutdown synchronously from composition or Startup.

err := app.Start(ctx, func(composer *application.Composer) {
	composer.Startup(connect)
	composer.Cleanup(disconnect)
})
if err != nil {
	return err
}

func (*Application) StartupHandler

func (application *Application) StartupHandler() router.Handler

StartupHandler returns a handler that answers 200 once startup has completed, including while stopping and after stop, and 503 otherwise. It reads lifecycle state without running checks; failed startup stays 503.

routes.Get(application.StartupPath, app.StartupHandler(), application.StartupOperation)

func (*Application) State

func (application *Application) State() State

State returns the current lifecycle phase under the application's state lock. StateStopped is published only after cleanup and telemetry finalization.

Example: app := application.New(); fmt.Println(app.State()) // new

func (*Application) Telemetry

func (application *Application) Telemetry() *telemetry.Telemetry

Telemetry returns the stable, non-nil application telemetry handle. Without Builder.Telemetry or Composer.MCPTools it records nothing. Diagnostic capture enables local recording without requiring a primary export driver; recording starts after composition. The application owns the handle's lifecycle. This is not the process default unless SetDefaults publishes it.

handle := app.Telemetry()
ctx, span := handle.Tracer("example.com/billing").Start(ctx, "billing charge")
defer span.End()

func (*Application) Wait

func (application *Application) Wait(ctx context.Context) error

Wait blocks until the runner group finishes or ctx ends, then returns the first runner failure, if any. Without runners it blocks until ctx ends or Shutdown begins. Caller cancellation alone is a normal wait completion and does not return ctx.Err or stop the application. Call Shutdown afterwards, or use Run to combine startup, waiting, and shutdown.

if err := app.Wait(ctx); err != nil {
	return errors.Join(err, app.Shutdown(context.WithoutCancel(ctx)))
}
return app.Shutdown(context.WithoutCancel(ctx))

func (*Application) WelcomeHandler

func (application *Application) WelcomeHandler() router.Handler

WelcomeHandler returns a self-contained HTML welcome page with documentation, getting-started, and source links. Only the configured application name is displayed, escaped as text; an empty name selects a generic welcome heading. Choose a name suitable for public display. No configuration, runtime state, version, or build information is included, and no assets are fetched.

The handler registers no routes and does not depend on application startup or debug mode. Rendering completes in memory before writing the response, so template failures return without a partial page. Template and response-write errors are returned to the router. Successful responses disable caching and sniffing, prohibit framing and scripts, and allow only the inline stylesheet and embedded data fonts and images used by the page.

Example:

app := application.New(func(app *application.Builder) {
	app.Name("billing")
})
routes := router.New()
routes.Get("/", app.WelcomeHandler(), application.WelcomeOperation)

type BuildInfo

type BuildInfo struct {
	// GoVersion is the Go toolchain that built the binary.
	// Example: fmt.Println(app.Info().Build.GoVersion)
	GoVersion string `json:"go_version,omitzero"`

	// ModulePath is the main module path.
	// Example: fmt.Println(app.Info().Build.ModulePath)
	ModulePath string `json:"module_path,omitzero"`

	// ModuleVersion is the main module version, "(devel)" when unknown.
	// Example: fmt.Println(app.Info().Build.ModuleVersion)
	ModuleVersion string `json:"module_version,omitzero"`

	// VCSRevision is the commit the binary was built from.
	// Example: fmt.Println(app.Info().Build.VCSRevision)
	VCSRevision string `json:"vcs_revision,omitzero"`

	// VCSTime is the commit time in RFC 3339 format.
	// Example: fmt.Println(app.Info().Build.VCSTime)
	VCSTime string `json:"vcs_time,omitzero"`

	// VCSModified reports whether the working tree had uncommitted changes.
	// Example: fmt.Println(app.Info().Build.VCSModified)
	VCSModified bool `json:"vcs_modified,omitzero"`
}

BuildInfo describes the running binary as recorded by the go command. Every field is empty when the binary carries no build information. Values come from the binary, not application configuration, and are safe to retain as ordinary strings and a boolean. Expose them only to operators.

build := app.Info().Build
fmt.Println(build.GoVersion, build.ModulePath)

type Builder

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

Builder configures an application before its lifecycle is wired by New. Settings replace previous values and return the same draft. Retained drafts cannot reconfigure an application; service pointers remain shared.

app := application.New(func(app *application.Builder) {
	app.Name("billing")
	app.Environment(application.Production)
})

func (*Builder) Debug

func (application *Builder) Debug(enabled bool) *Builder

Debug enables debug level logging when true. The default is false, which logs at info level in every environment.

app := application.New(func(app *application.Builder) {
	app.Debug(true)
})

func (*Builder) Environment

func (application *Builder) Environment(environment Environment) *Builder

Environment sets the deployment environment. The default is Development.

app := application.New(func(app *application.Builder) {
	app.Environment(application.Production)
})

func (*Builder) HealthDetails

func (application *Builder) HealthDetails(details bool) *Builder

HealthDetails includes failing check error text in verbose readiness and MCP health diagnostics when true. False reports "reason withheld". Error text can contain backend internals; enable it only when all affected diagnostic endpoints are operator-protected. It does not change trusted in-process check/event values.

app := application.New(func(app *application.Builder) {
	app.HealthDetails(true)
})

func (*Builder) LogHandler

func (application *Builder) LogHandler(build func(level slog.Leveler) slog.Handler) *Builder

LogHandler replaces the default log handler. The factory receives the application's level variable so that Application.SetLogLevel keeps working; handlers should honor it. Nil restores the environment's default handler. The factory runs once after all construction callbacks, with the final log level; the method itself only stores it and returns the same draft. Factory panics propagate from New.

app := application.New(func(app *application.Builder) {
	app.LogHandler(func(level slog.Leveler) slog.Handler {
		return slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level})
	})
})

func (*Builder) Name

func (application *Builder) Name(name string) *Builder

Name sets the service name. It becomes the service log attribute and the telemetry resource's service name. The default is empty, which omits both. It returns the same draft.

app := application.New(func(app *application.Builder) {
	app.Name("billing")
})

func (*Builder) ReadinessInterval

func (application *Builder) ReadinessInterval(interval time.Duration) *Builder

ReadinessInterval sets the ticker cadence for background readiness passes. The first pass starts immediately; later passes wait for a tick, not a full interval after completion. Passes never overlap, and slow checks can delay the next pass. Zero or negative selects 5 seconds. A result older than three intervals counts as not ready. It returns the same draft.

app := application.New(func(app *application.Builder) {
	app.ReadinessInterval(time.Second)
})

func (*Builder) ReadinessTimeout

func (application *Builder) ReadinessTimeout(timeout time.Duration) *Builder

ReadinessTimeout sets each background readiness pass's context deadline. Checks must honor cancellation: the application waits for all callbacks and cannot forcibly interrupt them. Zero or negative selects two fifths of the final interval. It returns the same draft.

app := application.New(func(app *application.Builder) {
	app.ReadinessTimeout(time.Second)
})

func (*Builder) ShutdownTimeout

func (application *Builder) ShutdownTimeout(timeout time.Duration) *Builder

ShutdownTimeout bounds the shutdown performed by Application.Run. Zero or negative selects the default of 30 seconds. It returns the same draft.

app := application.New(func(app *application.Builder) {
	app.ShutdownTimeout(time.Minute)
})

func (*Builder) Telemetry

func (application *Builder) Telemetry(handle *telemetry.Telemetry) *Builder

Telemetry enables telemetry with handle, built with telemetry.New from a driver such as the in-process memory driver or a [telemetry.Compose]d one. The application starts handle after composition, before Startup callbacks, and stops it after cleanup and the final lifecycle log and event. The same handle is returned by Application.Telemetry; callers must not start or stop it separately. Nil leaves recording disabled unless Composer.MCPTools attaches local diagnostic capture. Logger correlation needs only a valid span context, not this setting or application startup.

app := application.New(func(app *application.Builder) {
	app.Telemetry(telemetry.New(nil))
})

func (*Builder) Version

func (application *Builder) Version(version string) *Builder

Version sets the service version. It becomes the version log attribute and the telemetry resource's version. The default is the main module version stamped into the binary by the go command, or empty when there is none (for example "(devel)" builds), which omits both.

app := application.New(func(app *application.Builder) {
	app.Version("1.2.3")
})

type BuilderFunc

type BuilderFunc = func(*Builder)

BuilderFunc edits one fresh application draft during New. Callbacks execute directly once in order before defaults are resolved; nil entries panic. It is an alias, so ordinary function slices retain their type identity.

policies := []application.BuilderFunc{func(app *application.Builder) {
	app.Name("billing")
}}
app := application.New(policies...)

type CaptureLimits

type CaptureLimits struct {
	// Spans bounds finished spans; zero selects 2000.
	// Example: limits.Spans = 100
	Spans int
	// Logs bounds log records; zero selects 5000.
	// Example: limits.Logs = 100
	Logs int
	// Series bounds label combinations per instrument; zero selects 1000.
	// Example: limits.Series = 100
	Series int
	// Events bounds event metadata records; zero selects 500, never payloads.
	// Example: limits.Events = -1
	Events int
}

CaptureLimits bounds newly attached application diagnostic capture. Zero fields select defaults; negative fields disable that kind of capture. Existing configured memory telemetry keeps its own capacities.

tools := composer.Must(composer.MCPTools("app", func(tools *application.MCPToolsBuilder) {
	tools.Limits(application.CaptureLimits{Spans: 100, Events: -1})
}))

type CheckEvent

type CheckEvent struct {
	// Name is the health check name given to [Composer.Check], so listeners
	// can identify the dependency without inspecting its error text.
	//
	// Example: fmt.Println(failure.Name)
	Name string

	// Err is the error the check returned; nil for [CheckRecovered].
	// Example: fmt.Println(report.Err == nil) // true for a recovery
	Err error
}

CheckEvent describes a background check transition published by CheckFailed or CheckRecovered. Err is nil for recovery; names identify dependencies without parsing error text. Error values are shared, not copied or redacted for listeners.

onRecovered := func(_ context.Context, report application.CheckEvent) error {
	fmt.Println(report.Name, report.Err == nil)

	return nil
}

type CheckStatus

type CheckStatus struct {
	// Name is the name the check was registered with.
	// Example: for _, status := range app.Checks() { fmt.Println(status.Name) }
	Name string

	// Err is the failure of the latest pass, or nil when the check passed. A
	// panicking check reports the recovered value.
	// Example: if status.Err != nil { logger.Warn("dependency unavailable", "err", status.Err) }
	Err error

	// Since is when the check last changed between passing and failing, or
	// when it first completed if it never changed. It is the time of the pass
	// that observed the change.
	// Example: fmt.Println(status.Since.UTC())
	Since time.Time

	// CheckedAt is when the latest pass that ran the check finished.
	// Example: age := time.Since(status.CheckedAt); fmt.Println(age)
	CheckedAt time.Time
}

CheckStatus is the cached state of one health check as of the latest completed background pass. Application.Checks returns detached records; error values remain shared, and callbacks do not run when reading the cache.

for _, status := range app.Checks() {
	fmt.Println(status.Name, status.Err, status.CheckedAt)
}

type Composer

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

Composer wires concrete dependencies for one Start execution. App exposes shared logging, telemetry, and events; construct other dependencies directly and register their lifecycle callbacks. Registration is synchronous: do not retain the composer for use by workers. Cleanup and Defer may also be called inside an executing Startup callback; Must, MustDefer, Require, and child composition remain composition-only.

err := app.Run(ctx, func(composer *application.Composer) {
	composer.Startup(connect)
	composer.Cleanup(disconnect)
	composer.Run(serve)
})
if err != nil {
	return err
}

func (*Composer) App

func (composer *Composer) App() *Application

App returns the existing application's stable service handles and settings. No dependency lookup or automatic construction occurs here.

logger := composer.App().Logger()
logger.InfoContext(composer.Context(), "wiring dependencies")

func (*Composer) Check

func (composer *Composer) Check(name string, fn func(context.Context) error)

Check registers a nonempty, unique name and readiness callback during synchronous composition. Checks run once monitoring begins after Startup. Empty names or nil callbacks panic with ErrNilFunc; duplicate names panic with ErrDuplicateCheck and late registrations with ErrInvalidPhase.

composer.Check("database", connection.Ping)

func (*Composer) Cleanup

func (composer *Composer) Cleanup(fn func(context.Context) error)

Cleanup registers an immediately active release, independent of whether any Startup callback runs. All releases run once in reverse registration order on failure or shutdown, before telemetry finalization, with bounded uncanceled context. Errors and recovered panics are joined without skipping releases. Registration is allowed during composition and executing Startup callbacks.

composer.Cleanup(listener.Stop)

func (*Composer) Context

func (composer *Composer) Context() context.Context

Context returns the startup context shared by composition and Startup. Shutdown cancels it while startup is in progress. Runners receive their own lifecycle context; do not retain this one for background work.

composer.Require(loadSettings(composer.Context()))

func (*Composer) Defer

func (composer *Composer) Defer(release func())

Defer registers an infallible release on the same stack as Composer.Cleanup. It runs once on failure or shutdown in reverse registration order, not when the enclosing Go function returns. Registration is allowed during composition and executing Startup callbacks. A nil release panics with ErrNilFunc; release panics are handled like other cleanup panics. Use Cleanup when release needs a context or can return an error.

composer.Defer(cancel)
Example

ExampleComposer_Defer registers release for application shutdown, not scope exit.

package main

import (
	"context"
	"fmt"

	"arkoren.dev/core/application"
)

func main() {
	app := application.New()

	if err := app.Start(context.Background(), func(compose *application.Composer) {
		compose.Defer(func() {
			fmt.Println("released")
		})
	}); err != nil {
		fmt.Println(err)

		return
	}

	fmt.Println("running")

	if err := app.Shutdown(context.Background()); err != nil {
		fmt.Println(err)
	}
}
Output:
running
released

func (*Composer) MCPTools

func (composer *Composer) MCPTools(prefix string, configure ...MCPToolsBuilderFunc) ([]mcp.Tool, error)

MCPTools creates SDK-independent application and telemetry tools during synchronous composition. Prefix must be nonempty and is used directly: "app" produces app.info, app.health, app.started, app.healthy, app.ready, app.events, app.traces, app.trace, app.logs, and app.metrics. CLI groups use the same prefix. A negative Events limit omits the events tool. Other tools remain available even when their record/series retention is disabled. Existing memory stores keep their limits. An empty prefix returns mcp.ErrInvalidTool before callbacks or capture. Each supplied callback runs synchronously once, outside the lifecycle lock; nil callbacks and callback panics propagate. Retained drafts cannot alter published tools. Calling outside composition returns ErrInvalidPhase.

Capture reuses configured memory telemetry or attaches an observer preserving the primary backend's trace IDs. An unconfigured app gains trace IDs and log capture before its first startup hook. Events retain names, payload types, and trace IDs only. Each call attaches its own capture; call once per app. Declaration errors are returned after capture attachment without rollback. No transport, authentication, or HTTP dispatch is performed here.

Example:

tools := composer.Must(composer.MCPTools("app"))

func (*Composer) Must

func (composer *Composer) Must[T any](value T, err error) T

Must returns value when err is nil, otherwise aborts this composition through Require. The first failure remains recorded even if application code recovers the execution-specific private panic. Calling outside synchronous composition panics with ErrInvalidPhase, including successful results. Ordinary constructor panics are never converted into composition errors. Dependencies and values are returned unchanged; no copying or dependency lookup occurs.

err := app.Start(ctx, func(composer *application.Composer) {
	file := composer.Must(os.Open("settings.json"))
	composer.Cleanup(func(context.Context) error {
		return file.Close()
	})
})
if err != nil {
	return err
}

func (*Composer) MustDefer

func (composer *Composer) MustDefer(release func(), err error)

MustDefer aborts composition through Composer.Require when err is non-nil; otherwise it registers release through Composer.Defer. A failed acquisition registers no release, even if one is returned alongside the error. The caller that performs acquisition owns cleanup of partial results on failure. Like Must and Require, MustDefer is valid only during synchronous composition, not in Startup callbacks or runners. With nil err, a nil release is programmer misuse and panics with ErrNilFunc.

events := composer.App().Events()

composer.MustDefer(events.Listen(application.Started, onStarted))
Example

ExampleComposer_MustDefer owns the successful listener registration automatically.

package main

import (
	"context"
	"fmt"

	"arkoren.dev/core/application"
	"arkoren.dev/core/event"
)

func main() {
	app := application.New()

	if err := app.Start(context.Background(), func(compose *application.Composer) {
		events := compose.App().Events()

		compose.MustDefer(events.Listen(application.Started, func(context.Context, event.Void) error {
			fmt.Println("started")

			return nil
		}))
	}); err != nil {
		fmt.Println(err)

		return
	}

	if err := app.Shutdown(context.Background()); err != nil {
		fmt.Println(err)
	}

	app.Events().Signal(context.Background(), application.Started)
}
Output:
started

func (*Composer) Require

func (composer *Composer) Require(err error)

Require aborts composition with err; nil is a no-op. The first failure is recorded before raising an execution-specific private panic, so recovering that panic does not make Start succeed. Calling outside an executing composer panics with ErrInvalidPhase, even for nil. Ordinary panics still propagate.

err := app.Start(ctx, func(composer *application.Composer) {
	composer.Require(loadSettings(composer.Context()))
})
if err != nil {
	return err
}

func (*Composer) Run

func (composer *Composer) Run(fn func(context.Context) error)

Run registers a worker launched only after all Startup callbacks succeed. An unexpected error or panic cancels all workers; errors after cancellation are normal stops. Register only during synchronous composition.

composer.Run(listener.Serve)

func (*Composer) Startup

func (composer *Composer) Startup(fn func(context.Context) error)

Startup registers a callback run after every composer succeeds and telemetry starts. Callbacks run sequentially in registration order; an error prevents later startups and all runners. Panics become errors with a stack trace. Registration is permitted only during synchronous composition.

composer.Startup(listener.Start)

func (*Composer) Use

func (composer *Composer) Use[T any](operation step[T]) T

Use invokes one Factory or Action synchronously in this composition scope and returns its result unchanged. An Action returns struct{} and can be called as a statement. Callbacks share cleanup ownership and lifecycle registrations; Use does not memoize, start, or automatically release dependencies.

Nil operations, including typed nil functions, panic with ErrNilFunc. Calls outside composition panic with ErrInvalidPhase. A recorded composition failure aborts before invocation and prevents publication of a returned value, even if the callback recovered the private abort. Ordinary panics propagate through the enclosing composition after best-effort cleanup. Callbacks run outside the lifecycle lock, before any Startup callback.

logger := composer.App().Logger()

create := application.Factory[int](func(composer *application.Composer) int {
	return 8080
})
port := composer.Use(create)
logger.Info("listener", "port", port)

func (*Composer) UseDebug

func (composer *Composer) UseDebug[T any](operation step[T]) (T, bool)

UseDebug invokes a Factory or Action when the application's construction-time debug setting is enabled, independently of runtime log levels or environment. It returns the result and selection flag with UseWhen's validation, failure, and cleanup rules. A skipped operation returns zero and false.

composer.UseDebug(application.Action(func(composer *application.Composer) {
	logger := composer.App().Logger()

	logger.Debug("composing diagnostics")
}))

func (*Composer) UseIn

func (composer *Composer) UseIn[T any](environment Environment, operation step[T]) (T, bool)

UseIn invokes a Factory or Action when environment exactly matches the application's environment. Debug mode does not affect matching. It returns the result and selection flag with UseWhen's validation, failure, and cleanup rules; a skipped operation returns zero and false.

composer.UseIn(application.Development, application.Action(func(composer *application.Composer) {
	logger := composer.App().Logger()

	logger.Info("composing development tools")
}))

func (*Composer) UseInAny

func (composer *Composer) UseInAny[T any](environments []Environment, operation step[T]) (T, bool)

UseInAny invokes a Factory or Action once when any supplied environment exactly matches the application's environment. Empty or nil slices skip it; duplicates do not repeat it. The slice is read immediately and not retained. It returns the result and selection flag with UseWhen's validation, failure, and cleanup rules; a skipped operation returns zero and false.

composer.UseInAny([]application.Environment{
	application.Development,
	application.Staging,
}, application.Action(func(composer *application.Composer) {
	logger := composer.App().Logger()

	logger.Info("composing nonproduction tools")
}))

func (*Composer) UseWhen

func (composer *Composer) UseWhen[T any](condition bool, operation step[T]) (T, bool)

UseWhen invokes a Factory or Action synchronously when condition is true. Selected operations return their actual result and true, including legitimate zero results. Skipped operations return the result type's zero value and false without invoking the callback. The caller evaluates condition immediately.

Operations share the enclosing composition and cleanup owner. Nil interfaces and typed nil functions panic with ErrNilFunc even when skipped. Invalid phases panic with ErrInvalidPhase, and recorded composition failures abort even when skipped. A failure recovered inside an operation still prevents result publication. Callbacks run outside the lifecycle lock; ordinary panics receive the enclosing composition's cleanup and re-panic behavior.

logger := composer.App().Logger()

create := application.Factory[int](func(composer *application.Composer) int {
	return 8080
})
if port, selected := composer.UseWhen(enableHTTP, create); selected {
	logger.Info("listener", "port", port)
}

type ComposerFunc

type ComposerFunc = func(*Composer)

ComposerFunc composes concrete application dependencies during Start or Run. Callbacks execute once, synchronously in order, before any Startup callback. Nil callbacks are programmer errors and panic after best-effort rollback.

var compose application.ComposerFunc = func(composer *application.Composer) {
	composer.Run(serve)
}
err := app.Run(ctx, compose)
if err != nil {
	return err
}

type Environment

type Environment string

Environment names a deployment for configuration and diagnostic output. New resolves the empty value to Development; custom nonempty names are kept unchanged and use the JSON default log handler, like Staging and Production. Environment matching during composition is exact and case-sensitive.

app := application.New(func(app *application.Builder) {
	app.Environment(application.Production)
})
fmt.Println(app.Environment())
const (
	// Development identifies local development. It is the default.
	// It selects the text log handler but does not enable debug logging.
	//
	// Example: app := application.New(); _ = app.Environment() == application.Development
	Development Environment = "development"

	// Staging identifies a pre-production deployment.
	// Like other nondevelopment environments, it selects JSON logging.
	//
	//
	//	app := application.New(func(app *application.Builder) {
	//		app.Environment(application.Staging)
	//	})
	//	fmt.Println(app.Environment())
	Staging Environment = "staging"

	// Production identifies a production deployment.
	// It selects JSON logging; access controls and debug policy remain explicit.
	//
	//
	//	app := application.New(func(app *application.Builder) {
	//		app.Environment(application.Production)
	//	})
	//	fmt.Println(app.Environment())
	Production Environment = "production"
)

type Factory

type Factory[T any] func(*Composer) T

Factory composes a dependency and returns its concrete value. Factory helpers capture explicit inputs and return function literals of this defined type; acquisition belongs inside the callback so skipped operations acquire nothing. Use returns the callback's value without cloning it or managing its lifecycle. Register cleanup explicitly with the composer that owns the acquired resource. A nil Factory is invalid, including when conditional composition skips it.

logger := composer.App().Logger()

create := application.Factory[string](func(composer *application.Composer) string {
	return string(composer.App().Environment())
})
name := composer.Use(create)
logger.Info("environment", "name", name)

type FailedEvent

type FailedEvent struct {
	// Err is the error that failed the application.
	// Example: fmt.Println(failure.Err)
	Err error
}

FailedEvent describes the original startup or runner failure reported by Failed. Cleanup and telemetry-finalization errors are returned by lifecycle methods, not added to this payload. Error values remain shared with listeners.

onFailed := func(ctx context.Context, failure application.FailedEvent) error {
	logger.ErrorContext(ctx, "application failed", "err", failure.Err)

	return nil
}

type Info

type Info struct {
	// Name is the service name.
	// Example: fmt.Println(app.Info().Name)
	Name string `json:"name,omitzero"`

	// Version is the service version.
	// Example: fmt.Println(app.Info().Version)
	Version string `json:"version,omitzero"`

	// Environment is the deployment environment.
	// Example: fmt.Println(app.Info().Environment == application.Production)
	Environment Environment `json:"environment"`

	// State is the lifecycle phase, such as "running".
	// Example: fmt.Println(app.Info().State)
	State string `json:"state"`

	// LogLevel is the current minimum log level, such as "INFO".
	// Example: app.SetLogLevel(slog.LevelWarn); fmt.Println(app.Info().LogLevel)
	LogLevel string `json:"log_level"`

	// StartedAt is when the application reached the running state.
	// Example: fmt.Println(app.Info().StartedAt.IsZero()) // true before startup
	StartedAt time.Time `json:"started_at,omitzero"`

	// Uptime is the duration since StartedAt truncated to whole seconds in Go
	// duration string format; empty before startup completes.
	// Example: fmt.Println(app.Info().Uptime)
	Uptime string `json:"uptime,omitzero"`

	// UptimeSeconds is the whole seconds since StartedAt; nil before.
	// Example: if seconds := app.Info().UptimeSeconds; seconds != nil { fmt.Println(*seconds) }
	UptimeSeconds *int64 `json:"uptime_seconds,omitzero"`

	// Build describes the running binary.
	// Example: fmt.Println(app.Info().Build.GoVersion)
	Build BuildInfo `json:"build"`
}

Info describes an application at one moment: the body of Application.InfoHandler and the result of Application.Info. The JSON tags are the wire format of the info endpoint. The value is a snapshot, not configuration, and can reveal operational details.

info := app.Info()
fmt.Println(info.Name, info.State)

type MCPToolsBuilder

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

MCPToolsBuilder configures application-owned capture only. Transport, authentication, authorization, and other owners' tools belong in composition. The completed limits are copied before capture is attached.

tools := composer.Must(composer.MCPTools("app", func(tools *application.MCPToolsBuilder) {
	tools.Limits(application.CaptureLimits{Events: 100})
}))

func (*MCPToolsBuilder) Limits

func (tools *MCPToolsBuilder) Limits(limits CaptureLimits) *MCPToolsBuilder

Limits replaces capture capacities and returns the same construction draft. Existing configured memory drivers are reused without resizing them.

tools := composer.Must(composer.MCPTools("app", func(tools *application.MCPToolsBuilder) {
	tools.Limits(application.CaptureLimits{Logs: 500})
}))

type MCPToolsBuilderFunc

type MCPToolsBuilderFunc = func(*MCPToolsBuilder)

MCPToolsBuilderFunc configures application-owned diagnostic capture before publication. Callbacks execute synchronously once outside the lifecycle lock.

tools := composer.Must(composer.MCPTools("app", func(tools *application.MCPToolsBuilder) {
	tools.Limits(application.CaptureLimits{Events: 100})
}))

type State

type State string

State identifies the application lifecycle phase.

State values are stable lowercase strings suitable for diagnostics and JSON. Empty input retains the meaning of StateNew when formatted with String.

Example: _ = application.State("running") == application.StateRunning

const (
	// StateNew identifies an application that has not started.
	//
	// Example: _ = application.New().State() == application.StateNew
	StateNew State = "new"

	// StateStarting identifies an application composing dependencies or
	// executing startup callbacks.
	//
	// Example: _ = application.StateStarting.String() // "starting"
	StateStarting State = "starting"

	// StateRunning identifies an application whose runners are live.
	//
	// Example: _ = application.StateRunning.String() // "running"
	StateRunning State = "running"

	// StateStopping identifies an application draining runners, releasing
	// acquired resources, and finalizing telemetry.
	//
	// Example: _ = application.StateStopping.String() // "stopping"
	StateStopping State = "stopping"

	// StateStopped identifies an application whose cleanup and telemetry
	// finalization have finished.
	//
	// Example: _ = application.StateStopped.String() // "stopped"
	StateStopped State = "stopped"

	// StateFailed identifies an application whose startup failed and was
	// rolled back.
	//
	// Example: _ = application.StateFailed.String() // "failed"
	StateFailed State = "failed"
)

func (State) String

func (state State) String() string

String returns the state's wire value unchanged. An empty State denotes a new application for compatibility with the original zero-value state. Unknown nonempty values are preserved for diagnostics.

Example: _ = application.StateRunning.String() // "running"

type UnreadyEvent

type UnreadyEvent struct {
	// Failing names the health checks that failed the pass, in registration
	// order.
	// Example: fmt.Println(report.Failing)
	Failing []string
}

UnreadyEvent names checks in the background pass that made the app unready. It carries no backend error text. The names are in registration order; listeners must not mutate the shared payload's slice.

onUnready := func(_ context.Context, report application.UnreadyEvent) error {
	fmt.Println(report.Failing)

	return nil
}

Directories

Path Synopsis
internal
diagnostic
Package diagnostic declares application diagnostics over explicit observations.
Package diagnostic declares application diagnostics over explicit observations.
health
Package health owns check execution and cached background monitoring.
Package health owns check execution and cached background monitoring.
lifecycle
Package lifecycle owns application execution and composition admission.
Package lifecycle owns application execution and composition admission.
website
Package website renders the embedded public application welcome page.
Package website renders the embedded public application welcome page.

Jump to

Keyboard shortcuts

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