Documentation
¶
Overview ¶
Package framework provides the core application lifecycle management for Forge microservices.
The framework package implements Clean Architecture principles with a component-based design that enables building production-ready microservices with built-in observability, health checks, graceful shutdown, and lifecycle management.
Basic Usage ¶
Create a service by implementing the Component interface and using the App builder:
cfg := config.DefaultBaseConfig()
cfg.ServiceName = "my-service"
myComponent := &MyComponent{}
app, err := framework.New(
framework.WithConfig(&cfg),
framework.WithVersion("1.0.0"),
framework.WithComponent(myComponent),
)
if err != nil {
log.Fatal(err)
}
if err := app.Run(context.Background()); err != nil {
log.Fatal(err)
}
Architecture ¶
The framework follows Clean Architecture with these key concepts:
- Components: Your business logic implementing Component interface
- Bundles: Pre-built integrations (PostgreSQL, Redis, etc.)
- App: Main application orchestrating lifecycle and dependencies
- Health: Comprehensive health check system with liveness/readiness
- Observability: Built-in OpenTelemetry tracing and metrics
Lifecycle Management ¶
The App handles sophisticated startup and shutdown orchestration:
- Initialize observability (tracing, metrics)
- Initialize bundles in registration order
- Execute startup hooks
- Start gRPC and HTTP servers
- Start components in registration order
- Mark service as ready
Shutdown happens in reverse order with proper timeout handling and graceful termination.
Index ¶
- func NewHealthLogger(logging *LoggingManager) *healthLoggerAdapter
- type App
- func (a *App) AddHTTPMiddleware(middleware func(http.Handler) http.Handler)
- func (a *App) AddStreamInterceptor(interceptor grpc.StreamServerInterceptor)
- func (a *App) AddUnaryInterceptor(interceptor grpc.UnaryServerInterceptor)
- func (a *App) Config() *config.BaseConfig
- func (a *App) HealthRegistry() *forgeHealth.Registry
- func (a *App) IsRunning() bool
- func (a *App) IsStopping() bool
- func (a *App) Logger() *LoggingManager
- func (a *App) Observability() *ObservabilityManager
- func (a *App) Run(ctx context.Context) error
- func (a *App) Start(ctx context.Context) error
- func (a *App) Stop(ctx context.Context) error
- func (a *App) Uptime() time.Duration
- type AppOption
- func WithBundle(bundle Bundle) AppOption
- func WithComponent(component Component) AppOption
- func WithConfig(config *config.BaseConfig) AppOption
- func WithGRPCRegistrar(registrar Registrar) AppOption
- func WithHTTPServerConfig(cfg HTTPServerConfig) AppOption
- func WithHealthContributor(contributor HealthContributor) AppOption
- func WithHealthRegistry(registry *forgeHealth.Registry) AppOption
- func WithLogging(logging *LoggingManager) AppOption
- func WithObservability(observability *ObservabilityManager) AppOption
- func WithShutdownHook(hook ShutdownHook) AppOption
- func WithStartupHook(hook StartupHook) AppOption
- func WithStreamInterceptor(interceptor grpc.StreamServerInterceptor) AppOption
- func WithUnaryInterceptor(interceptor grpc.UnaryServerInterceptor) AppOption
- func WithVersion(version string) AppOption
- type Bundle
- type Component
- type HTTPRegistrar
- type HTTPServerBuilder
- type HTTPServerConfig
- type HealthContributor
- type LoggingManager
- type ObservabilityConfig
- type ObservabilityManager
- type OrchestrationHook
- type Registrar
- type ShutdownHook
- type ShutdownHookFunc
- type ShutdownOrchestrator
- type StartupHook
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewHealthLogger ¶
func NewHealthLogger(logging *LoggingManager) *healthLoggerAdapter
NewHealthLogger wraps a LoggingManager in an adapter satisfying the health package's logger interface, so the health registry's background checks log through the same structured, service-tagged zerolog output as the rest of the application instead of a separate logging path.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App represents the main service application with lifecycle management. App orchestrates the entire application lifecycle including initialization, startup, runtime, and graceful shutdown of all registered components and services.
The App follows a sophisticated startup sequence:
- Configuration validation
- Logging and observability initialization
- Bundle initialization (in registration order)
- Startup hook execution
- gRPC and HTTP server startup
- Component startup (in registration order)
- Health check registration and readiness marking
Shutdown happens in reverse order with proper timeout handling. The App ensures all resources are cleaned up gracefully on termination.
func (*App) AddHTTPMiddleware ¶ added in v0.3.0
AddHTTPMiddleware registers HTTP middleware from a bundle's Initialize method (or any other code that runs before the HTTP server is built). Middleware is applied outermost-first in registration order — the first middleware added sees the request first and the response last — and wraps the complete handler built by startHTTPServer, including the health, metrics, pprof, and any user HTTPRegistrar routes.
The HTTP server is built partway through Start, in startHTTPServer, which runs after all bundles' Initialize methods, so calling AddHTTPMiddleware from Initialize is safe. Calling it after the server has already been built is a no-op.
func (*App) AddStreamInterceptor ¶ added in v0.3.0
func (a *App) AddStreamInterceptor(interceptor grpc.StreamServerInterceptor)
AddStreamInterceptor registers a gRPC stream server interceptor. See AddUnaryInterceptor for the ordering and timing guarantees.
func (*App) AddUnaryInterceptor ¶ added in v0.3.0
func (a *App) AddUnaryInterceptor(interceptor grpc.UnaryServerInterceptor)
AddUnaryInterceptor registers a gRPC unary server interceptor from a bundle's Initialize method (or any other code that runs before the gRPC server is built). Interceptors are applied in registration order — combined with any interceptors passed to New via WithUnaryInterceptor, which run first — so a bundle wanting to be outermost (e.g. to time the whole call for metrics) should be registered as one of the first bundles.
The gRPC server is built partway through Start, in startGRPCServer, which runs after all bundles' Initialize methods. Since Initialize always runs before that point, calling AddUnaryInterceptor from Initialize is safe. Calling it after the server has already been built is a no-op: interceptors are baked into grpc.NewServer at construction time and cannot be added afterward.
func (*App) Config ¶
func (a *App) Config() *config.BaseConfig
Config returns the application configuration.
func (*App) HealthRegistry ¶
func (a *App) HealthRegistry() *forgeHealth.Registry
HealthRegistry returns the health registry.
func (*App) IsStopping ¶
IsStopping returns true if the application is in the process of stopping.
func (*App) Observability ¶ added in v0.3.0
func (a *App) Observability() *ObservabilityManager
Observability returns the observability manager.
func (*App) Start ¶
Start starts all application components.
The app mutex is held only long enough to claim the starting state, not across the (I/O-bound) bundle, server, and component startup below: a component that calls IsRunning/IsStopping from its own Start would otherwise deadlock against this method holding the lock for the whole startup sequence. The lock is re-acquired only to record the outcome.
type AppOption ¶
AppOption configures the App during creation.
func WithBundle ¶
WithBundle adds a bundle to the application.
func WithComponent ¶
WithComponent adds a component to be managed by the app.
func WithConfig ¶
func WithConfig(config *config.BaseConfig) AppOption
WithConfig sets the base configuration.
func WithGRPCRegistrar ¶
WithGRPCRegistrar adds a gRPC service registrar.
func WithHTTPServerConfig ¶ added in v0.3.0
func WithHTTPServerConfig(cfg HTTPServerConfig) AppOption
WithHTTPServerConfig sets a custom HTTP server configuration, overriding the framework's default endpoint, CORS, and request logging settings.
func WithHealthContributor ¶
func WithHealthContributor(contributor HealthContributor) AppOption
WithHealthContributor adds health checks from a contributor.
func WithHealthRegistry ¶ added in v0.3.0
func WithHealthRegistry(registry *forgeHealth.Registry) AppOption
WithHealthRegistry injects a custom health registry, overriding the framework's built-in default (a forgeHealth.Registry logging through the app's logging manager).
func WithLogging ¶ added in v0.3.0
func WithLogging(logging *LoggingManager) AppOption
WithLogging injects a custom logging manager, overriding the framework's built-in default (a LoggingManager derived from the app config).
func WithObservability ¶ added in v0.3.0
func WithObservability(observability *ObservabilityManager) AppOption
WithObservability injects a custom observability manager, overriding the framework's built-in default (an ObservabilityManager derived from the app config and version).
func WithShutdownHook ¶
func WithShutdownHook(hook ShutdownHook) AppOption
WithShutdownHook adds a shutdown hook.
func WithStartupHook ¶
func WithStartupHook(hook StartupHook) AppOption
WithStartupHook adds a startup hook.
func WithStreamInterceptor ¶
func WithStreamInterceptor(interceptor grpc.StreamServerInterceptor) AppOption
WithStreamInterceptor adds a gRPC stream server interceptor.
func WithUnaryInterceptor ¶
func WithUnaryInterceptor(interceptor grpc.UnaryServerInterceptor) AppOption
WithUnaryInterceptor adds a gRPC unary server interceptor.
type Bundle ¶
type Bundle interface {
// Name returns a unique identifier for this bundle.
Name() string
// Initialize sets up the bundle's functionality within the application.
// This method is called during application startup before components are started.
Initialize(app *App) error
// Stop gracefully shuts down the bundle and cleans up resources.
// This method is called during application shutdown in reverse initialization order.
// Bundles should respect the context deadline for graceful termination.
Stop(ctx context.Context) error
}
Bundle represents a reusable collection of functionality that can be added to an app. Bundles encapsulate common integrations like database connections, message queues, monitoring, etc. They are initialized in registration order during startup and stopped in reverse order during shutdown.
Example bundle for PostgreSQL integration:
type PostgreSQLBundle struct {
config *DatabaseConfig
db *sql.DB
}
func (b *PostgreSQLBundle) Name() string {
return "postgresql"
}
func (b *PostgreSQLBundle) Initialize(app *App) error {
db, err := sql.Open("postgres", b.config.URL)
if err != nil {
return err
}
b.db = db
return nil
}
func (b *PostgreSQLBundle) Stop(ctx context.Context) error {
if b.db != nil {
return b.db.Close()
}
return nil
}
type Component ¶
type Component interface {
// Start initializes the component and starts any background processes.
// This method is called during application startup after all bundles
// have been initialized and servers have been started.
Start(ctx context.Context) error
// Stop gracefully shuts down the component and cleans up resources.
// This method is called during application shutdown with a timeout context.
// Components should respect the context deadline for graceful termination.
Stop(ctx context.Context) error
}
Component represents a service component that can be started and stopped. Components are the primary way to integrate your business logic with the Forge framework.
Components are started in registration order during application startup and stopped in reverse order during graceful shutdown. Each component should handle its own resource management and cleanup.
Example implementation:
type UserService struct {
db *sql.DB
}
func (s *UserService) Start(ctx context.Context) error {
// Initialize connections, start background workers, etc.
return s.db.PingContext(ctx)
}
func (s *UserService) Stop(ctx context.Context) error {
// Clean up resources, stop workers, close connections
return s.db.Close()
}
type HTTPRegistrar ¶
type HTTPRegistrar interface {
// RegisterHTTPRoutes registers HTTP routes with the provided mux.
RegisterHTTPRoutes(mux *http.ServeMux)
}
HTTPRegistrar represents a component that can register HTTP routes.
type HTTPServerBuilder ¶
type HTTPServerBuilder struct {
// contains filtered or unexported fields
}
HTTPServerBuilder builds an enhanced HTTP server with configurable endpoints.
func NewHTTPServerBuilder ¶
func NewHTTPServerBuilder(app *App, config HTTPServerConfig) *HTTPServerBuilder
NewHTTPServerBuilder creates a new HTTP server builder.
func (*HTTPServerBuilder) Build ¶
func (b *HTTPServerBuilder) Build() (*http.Server, error)
Build constructs the HTTP server with all configured endpoints and middleware.
func (*HTTPServerBuilder) RegisterCustomRoutes ¶
func (b *HTTPServerBuilder) RegisterCustomRoutes(registerFunc func(*http.ServeMux))
RegisterCustomRoutes registers custom HTTP routes with the server.
type HTTPServerConfig ¶
type HTTPServerConfig struct {
// CORS configuration
EnableCORS bool `yaml:"enable_cors" env:"ENABLE_CORS"`
CORSOrigins []string `yaml:"cors_origins" env:"CORS_ORIGINS"`
CORSMethods []string `yaml:"cors_methods" env:"CORS_METHODS"`
CORSHeaders []string `yaml:"cors_headers" env:"CORS_HEADERS"`
CORSCredentials bool `yaml:"cors_credentials" env:"CORS_CREDENTIALS"`
// Endpoint configuration
EnableMetrics bool `yaml:"enable_metrics" env:"ENABLE_METRICS"`
MetricsPath string `yaml:"metrics_path" env:"METRICS_PATH"`
HealthPathPrefix string `yaml:"health_path_prefix" env:"HEALTH_PATH_PREFIX"`
// MetricsBasicAuthUser and MetricsBasicAuthPass, when both set, gate the
// metrics endpoint behind HTTP Basic Auth. Left empty (the default) the
// endpoint is unauthenticated, matching prior behavior.
MetricsBasicAuthUser string `yaml:"metrics_basic_auth_user" env:"METRICS_BASIC_AUTH_USER"`
MetricsBasicAuthPass string `yaml:"metrics_basic_auth_pass" env:"METRICS_BASIC_AUTH_PASS"`
// Request logging
EnableRequestLogging bool `yaml:"enable_request_logging" env:"ENABLE_REQUEST_LOGGING"`
}
HTTPServerConfig contains configuration for the HTTP server endpoints.
func DefaultHTTPServerConfig ¶
func DefaultHTTPServerConfig() HTTPServerConfig
DefaultHTTPServerConfig returns HTTP server configuration with sensible defaults.
func (*HTTPServerConfig) Validate ¶
func (c *HTTPServerConfig) Validate() error
Validate performs comprehensive validation on HTTP server configuration.
type HealthContributor ¶
type HealthContributor interface {
// HealthChecks returns a slice of health checks that this component provides.
// These checks will be automatically registered during application startup.
HealthChecks() []forgeHealth.Check
}
HealthContributor represents a service that contributes health checks. Components implementing this interface can provide health checks that will be automatically registered with the health registry.
Example:
func (s *UserService) HealthChecks() []health.Check {
return []forgeHealth.Check{
forgeHealth.NewBasicCheck(
forgeHealth.DefaultCheckConfig("database"),
func(ctx context.Context) error {
return s.db.PingContext(ctx)
},
func(ctx context.Context) error {
return s.db.PingContext(ctx)
},
),
}
}
type LoggingManager ¶
type LoggingManager struct {
// contains filtered or unexported fields
}
LoggingManager owns the application's zerolog configuration and hands out logger instances tagged with service and component context. Call Initialize before using Logger, WithService, or WithContext so the underlying logger has its output format (pretty console in development, JSON in production) and log level configured.
func NewLoggingManager ¶
func NewLoggingManager(config *config.BaseConfig) *LoggingManager
NewLoggingManager creates a LoggingManager bound to the given configuration. The returned manager's logger is not yet configured; call Initialize before using it.
func (*LoggingManager) Initialize ¶
func (lm *LoggingManager) Initialize() error
Initialize configures the manager's base logger from its BaseConfig: it sets the global zerolog level (falling back to Info if LogLevel doesn't parse), switches between a human-readable console writer in development and JSON output otherwise, and tags every log line with the service name and environment. The App calls this automatically during New, before observability and bundles are initialized.
func (*LoggingManager) Logger ¶
func (lm *LoggingManager) Logger() *zerolog.Logger
Logger returns a pointer to the manager's base zerolog.Logger, already tagged with service and environment fields by Initialize.
func (*LoggingManager) WithContext ¶
func (lm *LoggingManager) WithContext(fields map[string]any) zerolog.Logger
WithContext returns a copy of the base logger with an arbitrary set of key-value fields attached, one call to Interface per map entry, for callers that need structured fields beyond the fixed service/component pair provided by WithService.
func (*LoggingManager) WithService ¶
func (lm *LoggingManager) WithService(service, component string) zerolog.Logger
WithService returns a copy of the base logger with additional "service" and "component" fields attached, for use by a specific subsystem (e.g. the gRPC or HTTP server) so its log lines can be filtered independently.
type ObservabilityConfig ¶
type ObservabilityConfig struct {
ServiceName string
ServiceVersion string
Environment string
OTELEndpoint string
SampleRate float64
}
ObservabilityConfig contains the settings used to initialize OpenTelemetry tracing and metrics: the service's identity (name, version, environment), the OTLP collector endpoint to export to, and the trace sampling rate.
func NewObservabilityConfig ¶
func NewObservabilityConfig(baseConfig *config.BaseConfig, version string) *ObservabilityConfig
NewObservabilityConfig derives an ObservabilityConfig from the application's BaseConfig and the given service version, copying over the service name, environment, OTEL endpoint, and sample rate.
type ObservabilityManager ¶
type ObservabilityManager struct {
// contains filtered or unexported fields
}
ObservabilityManager owns the OpenTelemetry tracing and metrics providers for the life of the application. Call Initialize once during startup to wire up exporters from the manager's config, use Tracer and Meter to obtain instrumentation handles, and call Shutdown during graceful shutdown to flush pending telemetry and release exporter resources.
func NewObservabilityManager ¶
func NewObservabilityManager(config *ObservabilityConfig) *ObservabilityManager
NewObservabilityManager creates an ObservabilityManager for the given configuration. The returned manager is not yet initialized; call Initialize before relying on Tracer or Meter to return non-no-op instrumentation.
func (*ObservabilityManager) Initialize ¶
func (om *ObservabilityManager) Initialize(ctx context.Context) error
Initialize sets up OpenTelemetry tracing and metrics using the manager's configuration. It is idempotent: once initialization has succeeded, subsequent calls return nil without doing any work. The App calls this automatically during Start, before bundles are initialized, so components and bundles can safely assume tracing/metrics are ready by the time they run.
func (*ObservabilityManager) Meter ¶ added in v0.3.0
func (om *ObservabilityManager) Meter(name string) otelmetric.Meter
Meter returns an OpenTelemetry Meter for the given instrumentation name (conventionally the calling package or component name). Use it to create instruments — counters, histograms, gauges, etc. — for recording metrics. If Initialize has not configured a meter provider (no OTEL endpoint set), this falls back to the global otel.Meter, which produces no-op instruments rather than erroring.
func (*ObservabilityManager) Shutdown ¶
func (om *ObservabilityManager) Shutdown(ctx context.Context) error
Shutdown flushes and shuts down the trace and meter providers created by Initialize, returning a joined error if either fails to shut down cleanly. It is a no-op if Initialize was never called or did not complete successfully.
func (*ObservabilityManager) Tracer ¶ added in v0.3.0
func (om *ObservabilityManager) Tracer(name string) oteltrace.Tracer
Tracer returns an OpenTelemetry Tracer for the given instrumentation name (conventionally the calling package or component name). Use it to start spans around units of work you want traced, e.g. om.Tracer("myservice").Start(ctx, "operation-name"). If Initialize has not configured a trace provider (no OTEL endpoint set), this falls back to the global otel.Tracer, which produces no-op spans rather than erroring.
type OrchestrationHook ¶
type OrchestrationHook struct {
Name string
Func ShutdownHookFunc
}
OrchestrationHook represents a named shutdown hook with a cleanup function.
func ComponentShutdownHook ¶
func ComponentShutdownHook(name string, component Component) OrchestrationHook
ComponentShutdownHook creates a shutdown hook for a Component.
type Registrar ¶
type Registrar interface {
// RegisterGRPC registers gRPC service handlers with the provided server.
// This method is called during application startup before the gRPC server starts.
RegisterGRPC(server *grpc.Server) error
}
Registrar represents a service that can register gRPC handlers. Components that expose gRPC services should implement this interface to register their handlers with the framework's gRPC server.
Example:
func (s *UserService) RegisterGRPC(server *grpc.Server) error {
userpb.RegisterUserServiceServer(server, s)
return nil
}
type ShutdownHook ¶
ShutdownHook is called during application shutdown. Use shutdown hooks for custom cleanup logic that needs to run before components are stopped. Hooks are executed in reverse registration order.
type ShutdownHookFunc ¶
ShutdownHookFunc is a function that performs cleanup during shutdown.
type ShutdownOrchestrator ¶
type ShutdownOrchestrator struct {
// contains filtered or unexported fields
}
ShutdownOrchestrator manages the graceful shutdown of service components.
func NewShutdownOrchestrator ¶
func NewShutdownOrchestrator(timeout time.Duration) *ShutdownOrchestrator
NewShutdownOrchestrator creates a new shutdown orchestrator with the given timeout.
func (*ShutdownOrchestrator) RegisterHook ¶
func (so *ShutdownOrchestrator) RegisterHook(name string, hookFunc ShutdownHookFunc)
RegisterHook adds a shutdown hook to be executed during shutdown. Hooks are executed in the order they were registered.
func (*ShutdownOrchestrator) RegisterHookWithTimeout ¶
func (so *ShutdownOrchestrator) RegisterHookWithTimeout(name string, timeout time.Duration, hookFunc ShutdownHookFunc)
RegisterHookWithTimeout adds a shutdown hook with a specific timeout.