service

package
v1.0.0-beta.162 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 52 Imported by: 0

README

Service Package

Framework-level service infrastructure for SemStreams, providing service lifecycle management, HTTP server coordination, and configuration management.

Overview

The service package defines the core service architecture for SemStreams, providing explicit service registration with standardized lifecycle management, dependency injection, and HTTP endpoint coordination. This package follows clean architecture principles with dependency injection through Dependencies and configuration-driven service instantiation.

Services in SemStreams are self-contained units that are explicitly registered via the RegisterAll() function, receive structured dependencies, and can optionally expose HTTP endpoints through a shared server. The Manager coordinates all service lifecycle operations while maintaining clean separation of concerns.

The package supports both mandatory services (always running) and optional services (config-driven), with built-in health monitoring, graceful shutdown, and OpenAPI documentation aggregation.

Installation

import "github.com/c360/semstreams/service"

Core Concepts

Service Interface

Every service must implement the Service interface, providing lifecycle methods (Start/Stop) and health monitoring. Services handle their own configuration parsing and business logic.

Explicit Registration Pattern

Services export Register() functions that are called by RegisterAll() in register.go, enabling clear dependency graphs and testable service registration without global state modification.

Dependencies

All external dependencies (NATS client, metrics registry, logger, platform identity, config manager) are injected through Dependencies struct, following clean dependency injection patterns.

Manager

Central coordinator that manages service lifecycle, owns the shared HTTP server, and aggregates OpenAPI documentation from all services. Acts as both a framework component and a service itself.

ComponentManager Service

Special service that composes the constructor-captured boot component set and remains the sole lifecycle owner. It admits immutable declarations to Registry, seals composition, and exposes read-only component health, status, and configuration views. Later configuration writes do not mutate the running set.

Usage

Basic Example
// Exported Register function for explicit registration
func Register(registry *service.Registry) error {
    return registry.Register("my-service", NewMyService)
}

// Constructor following service pattern
func NewMyService(rawConfig json.RawMessage, deps *Dependencies) (Service, error) {
    cfg := &MyServiceConfig{
        Port: 8080, // service-specific default
    }
    
    // Parse raw JSON configuration
    if len(rawConfig) > 0 {
        if err := json.Unmarshal(rawConfig, cfg); err != nil {
            return nil, fmt.Errorf("invalid my-service config: %w", err)
        }
    }
    
    return &MyService{
        config: cfg,
        nats:   deps.NATSClient,
        logger: deps.Logger,
        platform: deps.Platform,
    }, nil
}

// Service implementation
type MyService struct {
    config *MyServiceConfig
    nats   *natsclient.Client
    logger *slog.Logger
    platform types.PlatformMeta
}

func (s *MyService) Start(ctx context.Context) error {
    s.logger.Info("Starting my-service", "org", s.platform.Org, "platform", s.platform.Platform)
    // Service-specific startup logic
    return nil
}

func (s *MyService) Stop(ctx context.Context) error {
    s.logger.Info("Stopping my-service")
    // Graceful shutdown logic
    return nil
}

func (s *MyService) IsHealthy() bool {
    return true // Service-specific health check
}

func (s *MyService) GetStatus() ServiceStatus {
    return ServiceStatus{
        Name:    "my-service",
        Healthy: s.IsHealthy(),
        Started: time.Now(), // Track actual start time
    }
}
Advanced Usage
// Service with HTTP endpoints
type MyService struct {
    // ... fields
}

// Implement HTTPHandler interface for HTTP endpoints
func (s *MyService) RegisterHTTPHandlers(prefix string, mux *http.ServeMux) {
    mux.HandleFunc(prefix+"/status", s.handleStatus)
    mux.HandleFunc(prefix+"/data", s.handleData)
}

func (s *MyService) OpenAPISpec() *OpenAPISpec {
    return &OpenAPISpec{
        Paths: map[string]PathItem{
            "/status": {
                Get: &Operation{
                    Summary:     "Get service status",
                    Description: "Returns current service status",
                    Responses: map[string]Response{
                        "200": {Description: "Service status"},
                    },
                },
            },
        },
    }
}

// Service configuration is restart-only. The containing services map owns
// identity and outer enabled; MyServiceConfig contains only inner settings.
ComponentManager HTTP APIs

ComponentManager exposes read-only endpoints for boot-composition observation:

// GET {prefix}/health - Aggregate component health
// GET {prefix}/list - List the boot-composed components
// GET {prefix}/types and {prefix}/types/{id} - Factory metadata and schemas
// GET {prefix}/status/{name} - Observe one component's status
// GET {prefix}/config/{name} - Observe its captured boot configuration

// Connectivity validation endpoints (uses flowgraph internally)
// GET {prefix}/flowgraph - Component connectivity graph
// GET {prefix}/validate - Connectivity analysis

There is no HTTP start, stop, or component-configuration write operation.

For component architecture and connectivity validation details, see component package and flowgraph.

Saved Flow Authoring Endpoints

FlowService treats flows as saved diagrams, not runtime lifecycle owners:

GET|POST /flowbuilder/flows
GET|PUT|DELETE /flowbuilder/flows/{id}
POST /flowbuilder/flows/{id}/validate
POST /flowbuilder/flows/{id}/publish-component-configs
GET /flowbuilder/flows/{id}/observations/{metrics,health,messages}

CRUD and validation do not publish configuration. Explicit publication validates and compiles the diagram, sorts component names, and performs upsert-only writes. It reports exact partial progress, leaves the current process unchanged, and requires a restart before published candidates can be composed. Diagram omissions never delete component configuration.

Flow deploy/start/stop/undeploy routes, the flow status WebSocket, associated runtime log stream, runtime ownership state, and lifecycle tools are absent.

API Reference

Types
Service

Primary interface that all services must implement.

type Service interface {
    Start(ctx context.Context) error    // Start service with context
    Stop(ctx context.Context) error     // Cancel Start lifetime and bound join/cleanup
    IsHealthy() bool                    // Health check
    GetStatus() ServiceStatus           // Service status for monitoring
}
Dependencies

Dependency injection structure for service construction.

type Dependencies struct {
    NATSClient      *natsclient.Client        // Required: NATS messaging client
    MetricsRegistry *metric.MetricsRegistry   // Optional: Prometheus metrics
    Logger          *slog.Logger              // Optional: structured logger (defaults to slog.Default())
    Platform        types.PlatformMeta        // Required: platform identity (org + platform)
    Manager   *config.Manager     // Optional: centralized configuration management
}
Manager

Central service coordinator and HTTP server owner.

type Manager struct {
    // Thread-safe service lifecycle management and HTTP server coordination
}
Functions
RegisterConstructor(name string, constructor Constructor)

Registers a service constructor with the ServiceRegistry. Called by RegisterAll() during service initialization.

(m *Manager) CreateService(name string, rawConfig json.RawMessage, deps *Dependencies) (Service, error)

Creates a service instance using the registered constructor with proper dependency injection.

(m *Manager) StartAll(ctx context.Context) error

Starts all created services in registration order with proper error handling.

(m *Manager) StopAll(ctx context.Context) error

Stops all services in reverse order, passing the exact caller-owned shutdown context to each service.

(cm *ComponentManager) GetComponentHealth() map[string]component.HealthStatus

Returns health values for the sealed boot composition.

(cm *ComponentManager) GetComponentStatus() map[string]ComponentStatus

Returns defensive status values without exposing live component handles.

Interfaces
HTTPHandler
type HTTPHandler interface {
    RegisterHTTPHandlers(prefix string, mux *http.ServeMux)
    OpenAPISpec() *OpenAPISpec
}

Optional interface for services that want to expose HTTP endpoints. Manager automatically registers handlers and aggregates OpenAPI documentation.

Service configuration is restart-only. KV updates change desired next-boot state; GET /services reports whether those changes require restart. Component post-boot reconfiguration is not a ComponentManager concern; published changes become eligible at the next boot.

Architecture

Design Decisions

Explicit Service Registration: Chose RegisterAll() orchestration over init() self-registration

  • Automatic discovery without configuration complexity
  • Clean dependency management through imports
  • Explicit control over service availability

Centralized HTTP Server: Manager owns single HTTP server shared by all services

  • Eliminates port conflicts and resource waste
  • Unified OpenAPI documentation and routing
  • Consistent URL patterns and middleware

Constructor Pattern: Standardized service constructor signature matching dependency injection

  • Services handle their own configuration parsing and validation
  • Enables flexible per-service configuration schemas
  • Clean separation between framework and service logic

Configuration-Driven Instantiation: Services created only if registered AND configured

  • Clear distinction between available (registered) and active (configured)
  • Supports optional services with graceful degradation
  • Environment-specific service composition

ComponentManager Integration: Special service for managing component lifecycle

  • Manages component startup/shutdown and health monitoring
  • Provides HTTP APIs for component introspection and control
  • Integrates with component package for connectivity validation
  • Enables boot-composition observation and debugging without runtime mutation

Startup observability: After composition seals, Manager binds the shared HTTP diagnostics and the configured built-in Prometheus listener before service startup without changing registration-order lifecycle. /readyz returns exact NOT READY until all fallible boot work succeeds, Manager commits the complete route set, and current service/component health is ready. The existing /services response includes additive startup counts; Prometheus exposes the same progress as semstreams_startup_units{owner,stage}. Stop clears commitment before child cleanup. Treat TCP reachability as liveness only, not readiness.

Integration Points
  • Dependencies: NATS client (required), MetricsRegistry (optional), Logger (optional), Manager (optional)
  • Used By: Main application for service orchestration, individual services for HTTP endpoints
  • Component Integration: ComponentManager service integrates with component package for lifecycle management
  • Data Flow: Configuration → Constructor → Service Instance → Manager → HTTP Endpoints

Configuration

Required Configuration
{
  "services": {
    "service-manager": {
      "enabled": true,
      "config": {
        "http_port": 8080,
        "swagger_ui": true
      }
    },
    "component-manager": {
      "enabled": true,
      "config": {}
    },
    "metrics": {
      "enabled": true,
      "config": {
        "port": 9090,
        "path": "/metrics"
      }
    }
  }
}
Optional Configuration
{
  "services": {
    "discovery": {
      "enabled": false,
      "config": {}
    },
    "message-logger": {
      "enabled": false,
      "config": {
        "max_entries": 1000
      }
    },
    "service-manager": {
      "enabled": true,
      "config": {
        "read_timeout": "10s",
        "write_timeout": "10s",
        "shutdown_timeout": "30s"
      }
    }
  }
}

Error Handling

Error Types

This package defines the following error patterns:

// Service registration errors
ErrServiceAlreadyExists = errors.New("service: constructor already registered")
ErrInvalidConstructor  = errors.New("service: invalid constructor function")

// Service lifecycle errors  
ErrServiceNotFound     = errors.New("service: service not found")
ErrServiceStartup      = errors.New("service: failed to start")
ErrServiceShutdown     = errors.New("service: failed to stop gracefully")

// HTTP server errors
ErrHTTPServerStartup   = errors.New("service: failed to start HTTP server")
ErrPortInUse          = errors.New("service: HTTP port already in use")
Error Detection
svc, err := manager.CreateService("my-service", config, deps)
if errors.Is(err, service.ErrServiceNotFound) {
    // Handle missing service constructor
}

err = manager.StartAll(ctx)
if errors.Is(err, service.ErrServiceStartup) {
    // Handle service startup failure
}

Testing

Test Utilities

This package provides comprehensive test utilities for service testing:

// ServiceSuite provides NATS testcontainer and common setup
type ServiceSuite struct {
    natsClient *natsclient.TestClient
    manager    *Manager
    deps       *Dependencies
}

// Use in service tests
func (s *MyServiceSuite) SetupTest() {
    s.ServiceSuite.SetupTest()
    
    // Register and create your service
    service.RegisterConstructor("my-service", NewMyService)
    svc, err := s.manager.CreateService("my-service", config, s.deps)
    s.Require().NoError(err)
}

// Test service lifecycle
func (s *MyServiceSuite) TestMyService_Lifecycle() {
    err := s.service.Start(context.Background())
    s.Assert().NoError(err)
    s.Assert().True(s.service.IsHealthy())
    
    err = s.service.Stop(5 * time.Second)
    s.Assert().NoError(err)
}
Testing Patterns
  • Use ServiceSuite for integration tests with real NATS via testcontainers
  • Test service behavior through Service interface methods
  • Verify HTTP endpoints using httptest.ResponseRecorder
  • Test configuration parsing with various JSON inputs
  • Validate graceful shutdown and resource cleanup

For component-specific testing (including connectivity validation), see component package.

Performance Considerations

  • Concurrency: All Manager operations are thread-safe using read-write mutex
  • Memory: Services maintain references until explicitly stopped and removed
  • HTTP Performance: Single shared server eliminates overhead of multiple HTTP listeners
  • Startup Time: Services start in parallel where possible, sequentially where dependencies exist
  • Component Lifecycle: ComponentManager caches connectivity analysis for efficient repeated access

Examples

Example 1: Simple Monitoring Service
package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "time"
    
    "github.com/c360/semstreams/service"
    "github.com/c360/semstreams/types"
)

// MonitoringService tracks system metrics
type MonitoringService struct {
    config   *MonitoringConfig
    platform types.PlatformMeta
    logger   *slog.Logger
    ticker   *time.Ticker
}

type MonitoringConfig struct {
    Interval time.Duration `json:"interval"`
}

func NewMonitoringService(rawConfig json.RawMessage, deps *service.Dependencies) (service.Service, error) {
    cfg := &MonitoringConfig{
        Interval: 30 * time.Second,
    }
    
    if len(rawConfig) > 0 {
        if err := json.Unmarshal(rawConfig, cfg); err != nil {
            return nil, err
        }
    }
    
    return &MonitoringService{
        config:   cfg,
        platform: deps.Platform,
        logger:   deps.Logger,
    }, nil
}

func (m *MonitoringService) Start(ctx context.Context) error {
    m.ticker = time.NewTicker(m.config.Interval)
    go m.monitoringLoop(ctx)
    
    m.logger.Info("Started monitoring service",
        "interval", m.config.Interval,
        "platform", m.platform.Platform)
    return nil
}

func (m *MonitoringService) Stop(ctx context.Context) error {
    if m.ticker != nil {
        m.ticker.Stop()
    }
    m.logger.Info("Stopped monitoring service")
    return nil
}

func (m *MonitoringService) IsHealthy() bool {
    return m.ticker != nil
}

func (m *MonitoringService) GetStatus() service.ServiceStatus {
    return service.ServiceStatus{
        Name:    "monitoring",
        Healthy: m.IsHealthy(),
        Details: map[string]any{
            "interval": m.config.Interval.String(),
            "platform": m.platform.Platform,
        },
    }
}

func (m *MonitoringService) monitoringLoop(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        case <-m.ticker.C:
            m.logger.Debug("Monitoring tick", "platform", m.platform.Platform)
            // Monitoring logic here
        }
    }
}

// HTTP endpoints
func (m *MonitoringService) RegisterHTTPHandlers(prefix string, mux *http.ServeMux) {
    mux.HandleFunc(prefix+"/status", m.handleStatus)
    mux.HandleFunc(prefix+"/metrics", m.handleMetrics)
}

func (m *MonitoringService) OpenAPISpec() *service.OpenAPISpec {
    return &service.OpenAPISpec{
        Paths: map[string]service.PathItem{
            "/status": {
                Get: &service.Operation{
                    Summary: "Get monitoring status",
                    Responses: map[string]service.Response{
                        "200": {Description: "Monitoring status"},
                    },
                },
            },
        },
    }
}

func (m *MonitoringService) handleStatus(w http.ResponseWriter, r *http.Request) {
    status := m.GetStatus()
    json.NewEncoder(w).Encode(status)
}

func (m *MonitoringService) handleMetrics(w http.ResponseWriter, r *http.Request) {
    metrics := map[string]any{
        "platform": m.platform.Platform,
        "uptime":   time.Since(time.Now()), // Would track actual uptime
    }
    json.NewEncoder(w).Encode(metrics)
}

// Explicit registration via exported function
func Register(registry *service.Registry) error {
    return registry.Register("monitoring", NewMonitoringService)
}

func main() {
    // Service is automatically available to Manager
    log.Println("Monitoring service registered and ready")
}
Example 2: Service Coordination and Management
package main

import (
    "context"
    "encoding/json"
    "log"
    "time"
    
    "github.com/c360/semstreams/service"
    "github.com/c360/semstreams/types"
    "github.com/c360/semstreams/natsclient"
    "github.com/c360/semstreams/metric"
)

func main() {
    // Create dependencies
    natsClient, _ := natsclient.NewClient("nats://localhost:4222")
    metricsRegistry := metric.NewMetricsRegistry()
    platform := types.PlatformMeta{
        Org:      "example",
        Platform: "demo-platform",
    }
    
    deps := &service.Dependencies{
        NATSClient:      natsClient,
        MetricsRegistry: metricsRegistry,
        Logger:          slog.Default(),
        Platform:        platform,
    }
    
    // Get the default Manager
    manager := service.DefaultManager
    
    // Configure HTTP server
    manager.SetHTTPConfig(8080, true, service.InfoSpec{
        Title:   "Demo Services",
        Version: "1.0.0",
    })
    
    // Services are registered via RegisterAll()
    // Create services from configuration
    serviceConfigs := types.ServiceConfigs{
        "monitoring": {Enabled: true, Config: json.RawMessage(`{"interval":"10s"}`)},
        "metrics":    {Enabled: true, Config: json.RawMessage(`{"port":9090}`)},
    }
    
    // Resolve and construct the complete pre-start composition.
    if err := manager.ConfigureFromServices(serviceConfigs, deps); err != nil {
        log.Fatalf("Failed to configure services: %v", err)
    }
    
    // Start all services
    ctx := context.Background()
    if err := manager.StartAll(ctx); err != nil {
        log.Fatalf("Failed to start services: %v", err)
    }
    
    log.Println("All services started")
    log.Println("HTTP server available at http://localhost:8080")
    log.Println("API documentation at http://localhost:8080/docs")
    
    // Check service health
    for name, svc := range manager.GetAllServices() {
        if svc.IsHealthy() {
            log.Printf("Service %s: healthy", name)
        } else {
            log.Printf("Service %s: unhealthy", name)
        }
    }
    
    // Simulate running for a while
    time.Sleep(30 * time.Second)
    
    // Graceful shutdown
    log.Println("Shutting down services...")
    if err := manager.StopAll(10 * time.Second); err != nil {
        log.Printf("Error during shutdown: %v", err)
    }
    
    log.Println("All services stopped")
}

Known Limitations

  • HTTP server configuration cannot be changed at runtime (requires restart)
  • Service dependencies must be acyclic (enforced through import structure)
  • OpenAPI spec aggregation assumes unique operation IDs across services
  • Graceful shutdown timeout applies to all services equally (no per-service timeouts)
  • pkg/component: ComponentManager service uses component Registry for lifecycle management
  • pkg/types: Provides PlatformMeta and other shared types
  • pkg/natsclient: NATS client dependency for service messaging
  • pkg/metric: Optional metrics collection for services
  • pkg/config: Configuration management and Manager integration

Documentation

Overview

Package service provides base functionality and common patterns for long-running services in the semstreams platform. It includes health monitoring, lifecycle management, and metric collection capabilities.

Package service provides service management and HTTP APIs for the SemStreams platform.

Package service provides service lifecycle management, HTTP server coordination, and component orchestration for the StreamKit platform.

The service package implements a sophisticated service architecture with clearly separated responsibilities across multiple service types:

Core Service Types

BaseService: Foundation for all services with standardized lifecycle management:

  • Lifecycle states: Stopped → Starting → Running → Stopping
  • Health monitoring with periodic checks
  • Metrics integration with CoreMetrics registry
  • Context-based cancellation and graceful shutdown
  • Dependency injection through Dependencies

Manager: Central orchestration of HTTP server and service lifecycle:

  • HTTP server management with graceful shutdown
  • Service registration and dependency injection
  • Two-phase HTTP initialization (system endpoints → service endpoints)
  • Health aggregation across all services
  • OpenAPI documentation aggregation

ComponentManager: boot-only component lifecycle ownership:

  • Captures configuration once during construction
  • Creates the enabled boot set and seals Registry declarations
  • Retains live handles as the sole lifecycle owner
  • Exposes read-only health, status, configuration, and flow graph views

FlowService: saved flow-diagram authoring API:

  • CRUD operations for flow definitions
  • Validation and compilation through Engine
  • Explicit sorted, upsert-only publication for the next boot
  • Best-effort observations keyed by diagram component names

Service Patterns

All services follow standardized patterns:

Constructor Pattern with Dependency Injection:

type MyService struct {
    *BaseService
    // service-specific fields
}

func NewMyService(deps Dependencies, config MyConfig) (*MyService, error) {
    base := NewBaseService("my-service", deps)
    svc := &MyService{BaseService: base}
    // Initialize service-specific fields
    return svc, nil
}

Lifecycle Implementation:

func (s *MyService) Initialize(ctx context.Context) error {
    // One-time initialization
    return s.BaseService.Initialize(ctx)
}

func (s *MyService) Start(ctx context.Context) error {
    // Start background operations
    return s.BaseService.Start(ctx)
}

func (s *MyService) Stop(ctx context.Context) error {
    // Graceful shutdown
    return s.BaseService.Stop(ctx)
}

HTTP Handler Integration:

func (s *MyService) RegisterHTTPHandlers(mux *http.ServeMux) {
    mux.HandleFunc("/api/v1/myservice/", s.handleRequest)
}

func (s *MyService) OpenAPISpec() map[string]any {
    return map[string]any{
        "paths": map[string]any{
            "/api/v1/myservice/": {
                "get": map[string]any{
                    "summary": "My service endpoint",
                    "responses": map[string]any{
                        "200": map[string]any{
                            "description": "Success",
                        },
                    },
                },
            },
        },
    }
}

Service Registration

Services are registered with Manager using constructor functions:

manager := service.NewServiceManager(deps)

// Register services
manager.RegisterConstructor("my-service", func(deps Dependencies) (Service, error) {
    return NewMyService(deps, config)
})

// Initialize and start all services
if err := manager.InitializeAll(ctx); err != nil {
    log.Fatal(err)
}
if err := manager.StartAll(ctx); err != nil {
    log.Fatal(err)
}

HTTP Server Management

Manager coordinates HTTP server lifecycle with startup diagnostics and atomic route promotion:

  1. Startup phase, after composition is sealed: - The shared listener binds before any service Start - Only /health, /healthz, /readyz, /services, /services/health, and read-only component diagnostics are served - Other routes return 503 with the exact body NOT READY - Manager binds the configured built-in Prometheus listener without changing service lifecycle order

  2. Commitment phase, after every fallible boot step succeeds: - Service, gateway, graph, and OpenAPI routes are built off-path - Manager starts its remaining runtime owners, stores the complete mux, and commits boot as the final non-failing transition

Requests therefore see either startup diagnostics or the complete route set, never a partially registered mux. TCP reachability is not readiness; callers use the /readyz status code. Its exact bodies remain READY and NOT READY.

Health Monitoring

Services implement health checks through BaseService:

// Override health check logic
func (s *MyService) healthCheck() error {
    if !s.isHealthy {
        return fmt.Errorf("service unhealthy: %v", s.lastError)
    }
    return nil
}

Health status is aggregated by Manager:

  • /health - Returns 200 if any service is healthy
  • /readyz - Returns 200 only after boot commitment, successful Starts, no Stop observation, and current health for every admitted unit

Metrics Integration

CoreMetrics and Manager-owned startup observation expose:

  • semstreams_service_status - Current service status (gauge)
  • semstreams_startup_units - Process-local admitted/invoked/completed/failed counts
  • semstreams_messages_received_total - Message counter
  • semstreams_messages_processed_total - Processing counter
  • semstreams_health_checks_total - Health check counter

Component Management

ComponentManager composes only the configuration snapshot captured by its constructor. It does not subscribe to later component or model-registry changes. Registry stores defensive declaration values and is sealed after boot admission; ComponentManager retains all concrete runtime handles.

Flow CRUD and validation are authoring-only. Explicit publish-component-configs writes sorted component candidates and reports exact partial progress. Those writes do not change the current process and require a later process boot.

Error Handling

Services follow StreamKit error handling patterns:

  • Configuration errors: Return during construction
  • Initialization errors: Return from Initialize()
  • Runtime errors: Log and update health status
  • Shutdown errors: Log but continue graceful shutdown

Use project error wrapping for context:

import "github.com/c360studio/semstreams/pkg/errs"

if err := validateConfig(cfg); err != nil {
    return errs.WrapInvalid(err, "my-service", "NewMyService", "validate config")
}

Graceful Shutdown

Manager coordinates graceful shutdown in reverse order:

  1. Stop accepting new HTTP requests
  2. Stop services in reverse registration order
  3. Shutdown HTTP server with timeout
  4. Close remaining connections

Example:

// Main application
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := manager.StopAll(ctx); err != nil {
    log.Printf("Graceful shutdown incomplete: %v", err)
}

Testing

The package provides ServiceSuite for integration testing with testcontainers:

func TestMyService(t *testing.T) {
    suite := service.NewServiceSuite(t)
    defer suite.Cleanup()

    // Suite provides NATS client, config manager, etc.
    svc, err := NewMyService(suite.Deps(), config)
    require.NoError(t, err)

    // Test service lifecycle
    err = svc.Initialize(suite.Context())
    require.NoError(t, err)
}

Security Considerations

The service HTTP APIs are designed for internal edge deployment:

  • No built-in authentication (add reverse proxy for production)
  • No rate limiting (implement at gateway level)
  • Path traversal protection on component endpoints
  • Input validation on all HTTP handlers

For production deployments, add external security layers:

  • Reverse proxy with authentication (nginx, Traefik)
  • Network policies to restrict access
  • TLS termination at gateway
  • Rate limiting at gateway level

Example: Complete Service Implementation

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/c360studio/semstreams/service"
    "github.com/c360studio/semstreams/config"
    "github.com/c360studio/semstreams/natsclient"
    "github.com/c360studio/semstreams/metric"
)

func main() {
    // Load configuration
    cfg, err := config.LoadMinimalConfig("config.json")
    if err != nil {
        log.Fatal(err)
    }

    // Initialize dependencies
    natsClient, err := natsclient.NewClient(cfg.NATS)
    if err != nil {
        log.Fatal(err)
    }
    defer natsClient.Close()

    metricsRegistry := metric.NewMetricsRegistry()
    configMgr := config.NewConfigManager(natsClient, cfg)

    deps := service.Dependencies{
        NATSClient:      natsClient,
        Manager:   configMgr,
        MetricsRegistry: metricsRegistry,
        Logger:          slog.Default(),
        Platform:        cfg.Platform,
    }

    // Create service manager
    manager := service.NewServiceManager(deps)

    // Register services
    manager.RegisterConstructor("flow-service", func(d Dependencies) (Service, error) {
        return service.NewFlowService(d, flowEngine, flowStore)
    })

    // Initialize and start
    ctx := context.Background()
    if err := manager.InitializeAll(ctx); err != nil {
        log.Fatal(err)
    }
    if err := manager.StartAll(ctx); err != nil {
        log.Fatal(err)
    }

    // Wait for signal
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig

    // Graceful shutdown
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := manager.StopAll(shutdownCtx); err != nil {
        log.Printf("Shutdown error: %v", err)
    }
}

For more details and examples, see the README.md in this directory.

Package service provides the Heartbeat service for emitting periodic system health logs.

Package service provides the LogForwarder service for log management.

Package service provides the MessageLogger service for observing message flow

Package service provides the MetricsForwarder service for forwarding metrics to NATS

Package service provides OpenAPI specification types for HTTP endpoint documentation

Package service provides the OpenAPI registry for service specifications

Package service provides OpenAPI specification types for HTTP endpoint documentation

Package service provides service registration

Package service provides service registration and management

Index

Constants

View Source
const (
	DefaultHTTPReadTimeout  = 30 * time.Second
	DefaultHTTPWriteTimeout = 120 * time.Second
)

HTTP server timeout defaults. Tuned for LLM-backed workloads — the prior 10s/10s defaults killed every long graph query with EOF. See commit message + feedback memory for the case study.

View Source
const StorageObservabilityServiceName = "storage-observability"

StorageObservabilityServiceName is the key this service is configured and registered under.

Variables

View Source
var ErrAlreadyStopped = errors.New("service already stopped")

ErrAlreadyStopped signals that Stop was invoked after exact owner completion was already observed. It is an idempotent, non-fatal outcome: Manager.StopAll treats it as successful and does not aggregate it as a shutdown error (gh#520). A Stop that observes completed teardown MAY return nil (the BaseService.Stop default) or this sentinel; both are success. StatusStopping alone is not completion evidence.

Functions

func BuildComponentTypeCatalog

func BuildComponentTypeCatalog(registry *component.Registry, logger *slog.Logger) []map[string]any

BuildComponentTypeCatalog returns the list of registered component factory types with their schemas. Both the GET /components/types HTTP handler and the list_components agent tool call this — keep the shape in one place.

func ConfigureRulePackMutations

func ConfigureRulePackMutations(manager *Manager) error

ConfigureRulePackMutations validates every enabled pack before injecting one canonical mutation client per contract-bearing processor.

func GetAllOpenAPISpecs

func GetAllOpenAPISpecs() map[string]*OpenAPISpec

GetAllOpenAPISpecs returns all registered OpenAPI specifications. Used by the openapi-generator tool to collect specs from all services.

func MaybeStartPProf

func MaybeStartPProf(debug bool, port int)

MaybeStartPProf starts the pprof HTTP server in a background goroutine when debug mode is enabled (debug && port > 0); otherwise it is a no-op.

ADR-058 rollout step 4: pprof is deliberately NOT a service.Service. An HTTP /debug/pprof mux has no process state to flush — every handler reads live runtime state per request — so it fails the Is-it-a-Service test on criterion 3 (needs a clean Stop/join): process exit cleans the listener with no correctness loss. Wrapping it in a StartAll-managed Service would also move the start from the composition root (before NATS) to StartAll (after NATS + all wiring), losing the ability to profile a wedged or slow boot — the canonical pprof use case. So, mirroring step 2's "shared helper, not a Service" conclusion, this is a plain helper called early and identically by both mains, killing the duplicated startPProfServer that otherwise drifts (the beta.18 class).

Fire-and-forget by design: a bind failure is a best-effort debug aid degrading, never a boot gate (the R1 posture). The pprof handlers are registered on http.DefaultServeMux by the CALLER's `import _ "net/http/pprof"` blank import — this helper serves that mux (nil handler). The blank import stays in the mains, not here, so importing package service does not silently arm pprof everywhere.

func RegisterAll

func RegisterAll(registry *Registry) error

RegisterAll registers all built-in services with the registry Future: Can be split into Registercore (), RegisterMonitoring(), etc.

func RegisterOpenAPISpec

func RegisterOpenAPISpec(name string, spec *OpenAPISpec)

RegisterOpenAPISpec registers an OpenAPI specification for a service. This should be called from init() functions in service files.

func ResolveServiceConfigs

func ResolveServiceConfigs(configs types.ServiceConfigs) (types.ServiceConfigs, error)

ResolveServiceConfigs returns the deterministic outer desired-service map. It owns map structure and activation defaults only; service constructors remain the sole interpreters of inner configuration.

func SchemaFromType

func SchemaFromType(t reflect.Type) map[string]any

SchemaFromType generates a JSON Schema from a reflect.Type. It handles primitives, structs, slices, maps, pointers, and time.Time.

func TypeNameFromReflect

func TypeNameFromReflect(t reflect.Type) string

TypeNameFromReflect extracts a clean type name from a reflect.Type. For example: "service.RuntimeHealthResponse" -> "RuntimeHealthResponse"

func WireGraphRuntime

func WireGraphRuntime(
	ctx context.Context,
	natsClient *natsclient.Client,
	logger *slog.Logger,
	contracts ...projection.Contract,
) (*projection.MutationClient, error)

WireGraphRuntime applies the existing storage-retention guard and constructs the built-in graph mutation client. It creates no ownership substrate or background coordination service.

Types

type BaseService

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

BaseService provides common functionality for all services

func NewBaseServiceWithOptions

func NewBaseServiceWithOptions(name string, cfg *config.Config, opts ...Option) *BaseService

NewBaseServiceWithOptions creates a new base service using functional options pattern

func (*BaseService) GetStatus

func (s *BaseService) GetStatus() Info

GetStatus returns the current service information

func (*BaseService) Health

func (s *BaseService) Health() health.Status

Health returns the standard health status for the service

func (*BaseService) IsHealthy

func (s *BaseService) IsHealthy() bool

IsHealthy returns whether the service is healthy

func (*BaseService) Name

func (s *BaseService) Name() string

Name returns the service name

func (*BaseService) OnHealthChange

func (s *BaseService) OnHealthChange(callback func(bool))

OnHealthChange sets a callback for health state changes

func (*BaseService) RegisterMetrics

func (s *BaseService) RegisterMetrics(_ metric.MetricsRegistrar) error

RegisterMetrics allows services to register their own domain-specific metrics

func (*BaseService) SetHealthCheck

func (s *BaseService) SetHealthCheck(fn HealthCheckFunc)

SetHealthCheck sets a custom health check function

func (*BaseService) Start

func (s *BaseService) Start(ctx context.Context) error

Start starts the service

func (*BaseService) Status

func (s *BaseService) Status() Status

Status returns the current service status

func (*BaseService) Stop

func (s *BaseService) Stop(ctx context.Context) error

Stop stops the service gracefully. The first call claims the one-shot cancellation authority and bounds the owner join with ctx. Later calls are nil/no-op; they neither wait again nor replay a prior result.

type ComponentGap

type ComponentGap struct {
	ComponentName string   `json:"component_name"`
	Issue         string   `json:"issue"`
	Description   string   `json:"description"`
	Suggestions   []string `json:"suggestions,omitempty"`
}

ComponentGap represents a connectivity gap in the component flow

type ComponentHealth

type ComponentHealth struct {
	Name          string              `json:"name"`
	Component     string              `json:"component"` // Component factory name (e.g., "udp", "graph-processor")
	Type          types.ComponentType `json:"type"`      // Component category (input/processor/output/storage/gateway)
	Status        string              `json:"status"`    // "running", "degraded", "error", "stopped"
	Healthy       bool                `json:"healthy"`
	Message       string              `json:"message"`
	StartTime     *time.Time          `json:"start_time"`     // ISO 8601 timestamp, null if not started
	LastActivity  *time.Time          `json:"last_activity"`  // ISO 8601 timestamp, null if no activity
	UptimeSeconds *float64            `json:"uptime_seconds"` // null if not started
	Details       any                 `json:"details"`        // Additional details for degraded/error states
}

ComponentHealth represents health and timing for a single component

type ComponentManager

type ComponentManager struct {
	*BaseService
	// contains filtered or unexported fields
}

ComponentManager handles lifecycle management of all components (inputs, processors, outputs) through the unified component system.

ComponentManager follows lifecycle:

Initialize() - Create components but don't start them
Start(ctx)   - Start initialized components with context
Stop()       - Stop components in reverse order

func (*ComponentManager) DetectObjectStoreGaps

func (cm *ComponentManager) DetectObjectStoreGaps() ([]ComponentGap, error)

DetectObjectStoreGaps identifies disconnected storage components

func (*ComponentManager) GetComponentHealth

func (cm *ComponentManager) GetComponentHealth() map[string]component.HealthStatus

GetComponentHealth returns current health status for all managed components. The projection holds a component borrow while invoking Health, so terminal Stop fences new observations and joins admitted callbacks before child Stop.

func (*ComponentManager) GetComponentStatus

func (cm *ComponentManager) GetComponentStatus() map[string]ComponentStatus

GetComponentStatus returns combined lifecycle state and health status for all components

func (*ComponentManager) GetFlowGraph

func (cm *ComponentManager) GetFlowGraph() (*flowgraph.FlowGraph, error)

GetFlowGraph returns the current FlowGraph, using cache if valid.

func (*ComponentManager) GetFlowPaths

func (cm *ComponentManager) GetFlowPaths() (map[string][]string, error)

GetFlowPaths returns data paths from input components to all reachable components

func (*ComponentManager) GetHealthyComponents

func (cm *ComponentManager) GetHealthyComponents() []string

GetHealthyComponents returns names of components that report healthy status

func (*ComponentManager) GetUnhealthyComponents

func (cm *ComponentManager) GetUnhealthyComponents() []string

GetUnhealthyComponents returns names of components that report unhealthy status

func (*ComponentManager) Initialize

func (cm *ComponentManager) Initialize() error

Initialize creates all configured components but does not start them This follows the unified Pattern A lifecycle where creation is separate from starting

func (*ComponentManager) IsInitialized

func (cm *ComponentManager) IsInitialized() bool

IsInitialized returns true if the component manager is initialized

func (*ComponentManager) IsStarted

func (cm *ComponentManager) IsStarted() bool

IsStarted returns true if the component manager is started

func (*ComponentManager) OpenAPISpec

func (cm *ComponentManager) OpenAPISpec() *OpenAPISpec

OpenAPISpec returns the OpenAPI specification for ComponentManager endpoints

func (*ComponentManager) RegisterHTTPHandlers

func (cm *ComponentManager) RegisterHTTPHandlers(prefix string, mux *http.ServeMux)

RegisterHTTPHandlers registers HTTP endpoints for the ComponentManager service

func (*ComponentManager) Start

func (cm *ComponentManager) Start(ctx context.Context) (startErr error)

Start starts the constructor-captured component set once. Cleanup authority is published before any fallible child acquisition and retained if bounded failed-Start rollback cannot finish.

func (*ComponentManager) Stop

func (cm *ComponentManager) Stop(ctx context.Context) error

Stop gracefully stops all components in reverse order of startup.

func (*ComponentManager) ValidateFlowConnectivity

func (cm *ComponentManager) ValidateFlowConnectivity() (*flowgraph.FlowAnalysisResult, error)

ValidateFlowConnectivity performs FlowGraph connectivity analysis with caching.

type ComponentManagerConfig

type ComponentManagerConfig struct {
	// EnabledComponents lists component names to enable.
	// If empty, all registered components are enabled.
	// Use this to selectively enable specific components in a deployment.
	EnabledComponents []string `json:"enabled_components" schema:"type:array,description:List of component names to enable (empty=all),category:basic"`
}

ComponentManagerConfig configures the ComponentManager service.

The ComponentManager composes one immutable boot component set.

func DefaultComponentManagerConfig

func DefaultComponentManagerConfig() ComponentManagerConfig

DefaultComponentManagerConfig returns the default configuration.

func (ComponentManagerConfig) Validate

func (c ComponentManagerConfig) Validate() error

Validate checks if the configuration is valid

type ComponentMetric

type ComponentMetric struct {
	Name        string              `json:"name"`
	Component   string              `json:"component"` // Component factory name (e.g., "udp", "graph-processor")
	Type        types.ComponentType `json:"type"`      // Component category (input/processor/output/storage/gateway)
	Status      string              `json:"status"`
	Throughput  *float64            `json:"throughput"`   // msgs/sec, null if unavailable
	ErrorRate   *float64            `json:"error_rate"`   // errors/sec, null if unavailable
	QueueDepth  *float64            `json:"queue_depth"`  // current queue depth, null if unavailable
	RawCounters *map[string]uint64  `json:"raw_counters"` // only present when Prometheus unavailable
}

ComponentMetric represents metrics for a single component

type ComponentPortDetail

type ComponentPortDetail struct {
	Name      string              `json:"name"`
	Direction component.Direction `json:"direction"`
	Subject   string              `json:"subject"`
	PortType  string              `json:"port_type"`
}

ComponentPortDetail represents detailed information about a single port

type ComponentPortInfo

type ComponentPortInfo struct {
	ComponentName string                `json:"component_name"`
	InputPorts    []ComponentPortDetail `json:"input_ports"`
	OutputPorts   []ComponentPortDetail `json:"output_ports"`
}

ComponentPortInfo represents port information extracted from a component

type ComponentPortReference

type ComponentPortReference struct {
	ComponentName string `json:"component_name"`
	PortName      string `json:"port_name"`
}

ComponentPortReference references a specific port on a component

type ComponentStatus

type ComponentStatus struct {
	Name      string                 `json:"name"`
	State     component.State        `json:"state"`
	Health    component.HealthStatus `json:"health"`
	DataFlow  component.FlowMetrics  `json:"data_flow"`
	LastError error                  `json:"last_error,omitempty"`
}

ComponentStatus combines lifecycle state with health and flow metrics

type ComponentsSpec

type ComponentsSpec struct {
	Schemas map[string]any `json:"schemas,omitempty"`
}

ComponentsSpec holds reusable OpenAPI component definitions

type CompositionSealedError

type CompositionSealedError struct {
	Operation string
	Name      string
}

CompositionSealedError reports an attempted composition write after StartAll fixed the process service identity set.

func (*CompositionSealedError) Error

func (e *CompositionSealedError) Error() string

type ConfigSchema

type ConfigSchema struct {
	component.ConfigSchema

	// ServiceSpecific can hold any service-specific schema extensions
	ServiceSpecific map[string]any `json:"service_specific,omitempty"`
}

ConfigSchema describes the configuration parameters for a service. We embed the component ConfigSchema for consistency across the system.

func NewConfigSchema

func NewConfigSchema(properties map[string]PropertySchema, required []string) ConfigSchema

NewConfigSchema creates a service ConfigSchema with extended property schemas

type Configurable

type Configurable interface {
	// ConfigSchema returns the configuration schema for this service
	ConfigSchema() ConfigSchema
}

Configurable is an optional interface for services that expose their configuration schema. This enables UI discovery and validation of service configurations. Services implementing this interface can describe their configuration parameters, including which fields can be changed at runtime without restart.

NOTE: This interface is reserved for future UI features. Currently only the Metrics service implements it. The Discovery service exists to expose this information via HTTP API.

type Constructor

type Constructor func(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

Constructor defines the standard constructor signature for all services. Every service must have a constructor that follows this pattern. The constructor receives raw JSON config and must handle its own parsing.

type Dependencies

type Dependencies struct {
	NATSClient        *natsclient.Client
	MetricsRegistry   *metric.MetricsRegistry
	Logger            *slog.Logger
	Platform          types.PlatformMeta           // Platform identity
	Manager           *config.Manager              // Centralized configuration management
	ComponentRegistry *component.Registry          // Component registry for ComponentManager
	ToolRegistry      component.ToolRegistryReader // Shared tool executor registry plumbed to component deps
	PayloadRegistry   *payloadregistry.Registry    // Shared payload registry plumbed to component deps
	LifecycleManager  *lifecycle.Manager           // Shared Lifecycle harness Manager (ADR-047), plumbed to component deps (rule processor + lifecycle-gateway). Nil when no app workflows are registered.
	ServiceManager    *Manager                     // Service manager for accessing other services
}

Dependencies provides the standard dependencies that all services receive. This replaces the old Dependencies struct and provides consistent injection. Services should use HTTP or NATS RPC for inter-service communication.

type DuplicateServiceError

type DuplicateServiceError struct {
	Name string
}

DuplicateServiceError reports two composition writers claiming one service map-key identity.

func (*DuplicateServiceError) Error

func (e *DuplicateServiceError) Error() string

type FlowConnection

type FlowConnection struct {
	Publisher  ComponentPortReference `json:"publisher"`
	Subscriber ComponentPortReference `json:"subscriber"`
	Subject    string                 `json:"subject"`
}

FlowConnection represents a connection between publisher and subscriber

type FlowCreateRequest

type FlowCreateRequest struct {
	ID          string                     `json:"id,omitempty"`
	Name        string                     `json:"name"`
	Description string                     `json:"description,omitempty"`
	Nodes       []flowstore.FlowNode       `json:"nodes"`
	Connections []flowstore.FlowConnection `json:"connections"`
	CreatedBy   string                     `json:"created_by,omitempty"`
}

FlowCreateRequest is the POST /flows request body. The server owns the version and every audit timestamp, so an author cannot send them: they are absent from this type and therefore from the generated schema. A legacy full-Flow body still decodes — the extra fields are simply ignored.

type FlowGap

type FlowGap struct {
	ComponentName string `json:"component_name"`
	PortName      string `json:"port_name"`
	Subject       string `json:"subject"`
	Direction     string `json:"direction"` // "input" or "output"
	Issue         string `json:"issue"`     // "no_publishers" or "no_subscribers"
}

FlowGap represents a disconnected port (no matching publisher/subscriber)

type FlowListResponse

type FlowListResponse struct {
	Flows []flowstore.Flow `json:"flows"`
}

FlowListResponse is the GET /flows response body. Its flows member is always present and is never null: an empty store serialises as [], so a client never has to tell "no saved flows" apart from "the field is missing". The elements are values rather than pointers because the schema generator renders a pointer element type as anyOf [..., null] (schema.go:12-20), and a saved Flow inside the list is never null.

type FlowService

type FlowService struct {
	*BaseService
	// contains filtered or unexported fields
}

FlowService provides saved flow-diagram CRUD, validation, compilation, and observations keyed by the component names declared in a diagram. A diagram is not a runtime lifecycle owner.

func (*FlowService) OpenAPISpec

func (fs *FlowService) OpenAPISpec() *OpenAPISpec

OpenAPISpec returns the saved-flow HTTP contract.

func (*FlowService) RegisterHTTPHandlers

func (fs *FlowService) RegisterHTTPHandlers(prefix string, mux *http.ServeMux)

RegisterHTTPHandlers registers diagram CRUD, validation, publication, and saved-diagram observation routes.

func (*FlowService) RegisterMetrics

func (fs *FlowService) RegisterMetrics(registrar metric.MetricsRegistrar) error

RegisterMetrics registers stream migration-override expiry reporting.

func (*FlowService) Start

func (fs *FlowService) Start(ctx context.Context) error

Start starts saved-diagram service work for the supplied process lifetime.

func (*FlowService) Stop

func (fs *FlowService) Stop(ctx context.Context) error

Stop joins saved-diagram service work.

type FlowServiceConfig

type FlowServiceConfig struct {
	PrometheusURL string `json:"prometheus_url,omitempty"`
	FallbackToRaw bool   `json:"fallback_to_raw,omitempty"`
}

FlowServiceConfig holds configuration for saved-diagram observations.

type FlowUpdateRequest

type FlowUpdateRequest struct {
	ID          string                     `json:"id"`
	Version     int64                      `json:"version"`
	Name        string                     `json:"name"`
	Description string                     `json:"description,omitempty"`
	Nodes       []flowstore.FlowNode       `json:"nodes"`
	Connections []flowstore.FlowConnection `json:"connections"`
	CreatedBy   string                     `json:"created_by,omitempty"`
}

FlowUpdateRequest is the PUT /flows/{id} request body. Version is the optimistic-concurrency precondition, not a stored value; the audit timestamps are the server's and are absent here for the same reason as on create.

type HTTPHandler

type HTTPHandler interface {
	RegisterHTTPHandlers(prefix string, mux *http.ServeMux)
	OpenAPISpec() *OpenAPISpec // Returns OpenAPI specification for this service
}

HTTPHandler is an optional interface for services that want to expose HTTP endpoints

type HTTPMiddleware

type HTTPMiddleware func(http.Handler) http.Handler

HTTPMiddleware wraps an http.Handler. Products supply middleware via Manager.UseHTTPMiddleware before Start; the Manager wraps the framework's HTTP mux with the chain at server boot.

Application order is outermost-first: the slice element at index 0 is the outermost wrapper — it sees the request first and the response last. The element at index len-1 is innermost, executing closest to the registered handler. This matches the convention established in ADR-030 and is the same direction the standard library's reverse-build composition pattern produces (see chainMiddleware below).

The framework ships zero default middleware. Common cross-cutting concerns — auth, request logging, panic recovery, rate limiting, CORS — are product policy and live in product-shell middleware. Products plugging in identity-aware middleware should pair with agenticdispatch.WithIdentity to populate the ctx that agenticdispatch.IdentityFromRequest consumes downstream (the beta.22 helper pair).

type HealthCheckFunc

type HealthCheckFunc func() error

HealthCheckFunc defines a custom health check function

type HeartbeatConfig

type HeartbeatConfig struct {
	// Interval between heartbeat logs (e.g., "30s", "1m")
	// Default: "30s"
	Interval string `json:"interval"`
}

HeartbeatConfig holds configuration for the Heartbeat service

func (HeartbeatConfig) Validate

func (c HeartbeatConfig) Validate() error

Validate checks if the configuration is valid

type HeartbeatService

type HeartbeatService struct {
	*BaseService
	// contains filtered or unexported fields
}

HeartbeatService emits periodic system heartbeat logs

func (*HeartbeatService) Start

func (hb *HeartbeatService) Start(ctx context.Context) error

Start begins the heartbeat service. Instances are single-use: once Stop has run its teardown the instance cannot be restarted — create a new one via the constructor (production disable→enable already does this via CreateService).

func (*HeartbeatService) Stop

func (hb *HeartbeatService) Stop(ctx context.Context) error

Stop gracefully stops the heartbeat service. Stop is idempotent per the Service contract (gh#520): a service that already reached a terminal state — e.g. via parent-context cancellation before the manager's StopAll visit — is a clean shutdown, and repeated calls are safe (gh#549). Teardown still runs on the already-stopped path so the ticker is released when cancellation wins the race.

type Info

type Info struct {
	Name               string        `json:"name"`
	Status             Status        `json:"status"`
	Uptime             time.Duration `json:"uptime"`
	StartTime          time.Time     `json:"start_time"`
	MessagesProcessed  int64         `json:"messages_processed"`
	LastActivity       time.Time     `json:"last_activity"`
	HealthChecks       int64         `json:"health_checks"`
	FailedHealthChecks int64         `json:"failed_health_checks"`
}

Info holds runtime information for a service

type InfoSpec

type InfoSpec struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	Version     string `json:"version"`
}

InfoSpec contains API metadata

type KVWatchConnectedEvent

type KVWatchConnectedEvent struct {
	Bucket  string `json:"bucket"`
	Pattern string `json:"pattern"`
	Message string `json:"message"`
}

KVWatchConnectedEvent represents the initial connection event

type KVWatchEvent

type KVWatchEvent struct {
	Bucket    string          `json:"bucket"`
	Key       string          `json:"key"`
	Operation string          `json:"operation"` // "create", "update", "delete"
	Value     json.RawMessage `json:"value,omitempty"`
	Revision  uint64          `json:"revision"`
	Timestamp time.Time       `json:"timestamp"`
}

KVWatchEvent represents a KV change event sent via SSE

type LogForwarder

type LogForwarder struct {
	*BaseService
	// contains filtered or unexported fields
}

LogForwarder is a service for log configuration management. With the new architecture, actual log forwarding to NATS is handled by NATSLogHandler in pkg/logging. This service validates configuration and provides a service endpoint.

func NewLogForwarder

func NewLogForwarder(config *LogForwarderConfig, opts ...Option) (*LogForwarder, error)

NewLogForwarder creates a new LogForwarder service.

func (*LogForwarder) Config

func (lf *LogForwarder) Config() LogForwarderConfig

Config returns the LogForwarder configuration. This can be used by other components to access the log configuration.

func (*LogForwarder) Start

func (lf *LogForwarder) Start(ctx context.Context) error

Start begins the LogForwarder service. Note: Log forwarding to NATS is handled by NATSLogHandler in main.go. This service provides configuration validation and service lifecycle management.

func (*LogForwarder) Stop

func (lf *LogForwarder) Stop(ctx context.Context) error

Stop gracefully stops the LogForwarder.

type LogForwarderConfig

type LogForwarderConfig struct {
	// MinLevel is the minimum log level to forward to NATS (DEBUG, INFO, WARN, ERROR).
	// Logs below this level are still written to stdout but not published to NATS.
	MinLevel string `json:"min_level"`

	// ExcludeSources is a list of source prefixes to exclude from NATS forwarding.
	// Logs from excluded sources still go to stdout but are not published to NATS.
	// Uses prefix matching with dotted notation: excluding "flow-service.websocket"
	// also excludes "flow-service.websocket.health" but NOT "flow-service".
	ExcludeSources []string `json:"exclude_sources"`
}

LogForwarderConfig holds configuration for the LogForwarder service. Note: The service is enabled/disabled via types.ServiceConfig.Enabled at the outer level.

Configuration is used by: - NATSLogHandler (in pkg/logging) for min_level and exclude_sources filtering - This service for configuration validation

func (LogForwarderConfig) Validate

func (c LogForwarderConfig) Validate() error

Validate checks if the configuration is valid.

type Manager

type Manager struct {
	*BaseService // Embed BaseService to implement Service interface
	// contains filtered or unexported fields
}

Manager manages service lifecycle using a provided registry. Services are explicitly registered and created from raw JSON configs.

func NewServiceManager

func NewServiceManager(registry *Registry) *Manager

NewServiceManager creates a new service manager

func (*Manager) ConfigureFromServices

func (m *Manager) ConfigureFromServices(services map[string]types.ServiceConfig, deps *Dependencies) error

ConfigureFromServices configures Manager directly from services config This replaces the old pattern where Manager was a service itself

func (*Manager) CreateService

func (m *Manager) CreateService(name string, rawConfig json.RawMessage, deps *Dependencies) (Service, error)

CreateService creates a service instance using the registered constructor

func (*Manager) GetAllServiceStatus

func (m *Manager) GetAllServiceStatus() map[string]any

GetAllServiceStatus returns the status of all services

func (*Manager) GetAllServices

func (m *Manager) GetAllServices() map[string]Service

GetAllServices returns all registered service instances

func (*Manager) GetHealthyServices

func (m *Manager) GetHealthyServices() []string

GetHealthyServices returns a list of healthy services

func (*Manager) GetService

func (m *Manager) GetService(name string) (Service, bool)

GetService returns a service instance by name

func (*Manager) GetServiceStatus

func (m *Manager) GetServiceStatus(name string) (any, error)

GetServiceStatus returns the status of a specific service

func (*Manager) GetUnhealthyServices

func (m *Manager) GetUnhealthyServices() []string

GetUnhealthyServices returns a list of unhealthy services

func (*Manager) HasConstructor

func (m *Manager) HasConstructor(name string) bool

HasConstructor checks if a constructor is registered

func (*Manager) ListConstructors

func (m *Manager) ListConstructors() []string

ListConstructors returns all registered constructor names

func (*Manager) ProjectionBinders

func (m *Manager) ProjectionBinders() []ProjectionBinder

ProjectionBinders returns enabled components that require projection clients.

func (*Manager) RegisterInstance

func (m *Manager) RegisterInstance(name string, svc Service) error

RegisterInstance admits a pre-built Service to the manager (composition-root wiring, as opposed to config-driven CreateService). Same map + order tracking CreateService uses, so StartAll/StopAll treat it identically.

func (*Manager) Start

func (m *Manager) Start(ctx context.Context) error

Start starts the Manager HTTP server if configured

func (*Manager) StartAll

func (m *Manager) StartAll(ctx context.Context) error

StartAll starts all registered service instances and the HTTP server

func (*Manager) StartHealthListener

func (m *Manager) StartHealthListener(ctx context.Context, port int) error

StartHealthListener binds a dedicated /health + /healthz listener on the given port. Intended for Docker / Kubernetes probes that want a stable port independent of the service-manager UI's HTTPPort (#100). Port 0 is a no-op (disabled — the default for the -health-port flag).

The listener serves the SAME handler functions as the main HTTP mux's /health and /healthz routes; it reads m.services / m.natsClient under the same m.mu locks. Bind failure is returned synchronously so the composition boundary can decide whether this convenience-only surface is required for boot.

The listener is one-shot: calling twice, including after completed Stop, returns an error rather than re-binding this Manager instance.

func (*Manager) Stop

func (m *Manager) Stop(ctx context.Context) error

Stop stops the Manager HTTP server

func (*Manager) StopAll

func (m *Manager) StopAll(ctx context.Context) error

StopAll stops all registered service instances in reverse order and the HTTP server

func (*Manager) StopHealthListener

func (m *Manager) StopHealthListener(ctx context.Context) error

StopHealthListener gracefully shuts down the dedicated health-port listener if one was started. No-op when the listener is not running.

func (*Manager) UseHTTPMiddleware

func (m *Manager) UseHTTPMiddleware(mws ...HTTPMiddleware)

UseHTTPMiddleware appends product-supplied middleware to the chain wrapped around every HTTP route the framework registers (component handlers, gateway-component handlers, system endpoints like /openapi.json and /health). Order is outermost-first: the first middleware passed in this call (or across calls) is the outermost wrapper.

This is the framework's only HTTP middleware seam. The framework ships zero default middleware; auth, request logging, panic recovery, rate limiting, and CORS are product policy. Products pairing identity-aware middleware with the beta.22 helpers should call agenticdispatch.WithIdentity from inside the middleware so agenticdispatch.IdentityFromRequest picks it up downstream.

Must be called before the HTTP server starts (i.e., before the owning service.Manager.Start*HTTP* path runs). Calls after the server is already up are ignored with a warning log — late registration would be silently dropped at the chain since the http.Server's Handler field is set at boot, and a warning is the closest we can get to "you didn't get what you asked for" without a panic. Multiple calls before boot are concatenated in call order, so a product may layer middleware progressively.

type ManagerConfig

type ManagerConfig struct {
	HTTPPort   int      `json:"http_port"`
	SwaggerUI  bool     `json:"swagger_ui"`
	ServerInfo InfoSpec `json:"server_info"`

	// HTTPReadTimeout caps how long the server waits for the request body.
	// Go duration string (e.g. "30s"). Empty falls back to
	// DefaultHTTPReadTimeout. Bump for paths that stream large request
	// bodies; the default is enough for typical JSON GraphQL queries.
	HTTPReadTimeout string `json:"http_read_timeout,omitempty"`

	// HTTPWriteTimeout caps how long the server takes to write the
	// response. Go duration string (e.g. "120s"). Empty falls back to
	// DefaultHTTPWriteTimeout. CRITICAL for LLM-backed paths (globalSearch
	// answer synthesis, classifier, etc.) — the prior hardcoded 10s
	// killed every long-running query mid-write with EOF on the client
	// side. Set to >= 2× the slowest expected handler time.
	HTTPWriteTimeout string `json:"http_write_timeout,omitempty"`
}

ManagerConfig holds configuration for the Manager HTTP server Simple struct - no UnmarshalJSON, no Enabled field

func (ManagerConfig) ResolvedHTTPReadTimeout

func (c ManagerConfig) ResolvedHTTPReadTimeout() time.Duration

ResolvedHTTPReadTimeout returns the configured read timeout or the default when unset/invalid.

func (ManagerConfig) ResolvedHTTPWriteTimeout

func (c ManagerConfig) ResolvedHTTPWriteTimeout() time.Duration

ResolvedHTTPWriteTimeout returns the configured write timeout or the default when unset/invalid.

func (ManagerConfig) Validate

func (c ManagerConfig) Validate() error

Validate checks if the configuration is valid

type MandatoryServiceDisabledError

type MandatoryServiceDisabledError struct {
	Name string
}

MandatoryServiceDisabledError reports desired state that cannot form a valid framework process composition.

func (*MandatoryServiceDisabledError) Error

type MessageLogEntry

type MessageLogEntry struct {
	Sequence    uint64          `json:"sequence"` // Monotonic sequence for index validity
	Timestamp   time.Time       `json:"timestamp"`
	Subject     string          `json:"subject"`
	MessageType string          `json:"message_type,omitempty"`
	MessageID   string          `json:"message_id,omitempty"`
	TraceID     string          `json:"trace_id,omitempty"` // W3C trace ID (32 hex chars)
	SpanID      string          `json:"span_id,omitempty"`  // W3C span ID (16 hex chars)
	Summary     string          `json:"summary"`
	RawData     json.RawMessage `json:"raw_data,omitempty"`
	Metadata    map[string]any  `json:"metadata,omitempty"`
}

MessageLogEntry represents a logged message

type MessageLogger

type MessageLogger struct {
	*BaseService
	// contains filtered or unexported fields
}

MessageLogger provides message observation and logging as a service

func NewMessageLogger

func NewMessageLogger(
	loggerConfig *MessageLoggerConfig,
	natsClient *natsclient.Client,
	opts ...Option,
) (*MessageLogger, error)

NewMessageLogger creates a new MessageLogger service

func (*MessageLogger) ConfigSchema

func (ml *MessageLogger) ConfigSchema() ConfigSchema

ConfigSchema returns the configuration schema for this service. This implements the Configurable interface for UI discovery.

func (*MessageLogger) GetEntriesByTrace

func (ml *MessageLogger) GetEntriesByTrace(traceID string) []MessageLogEntry

GetEntriesByTrace returns all log entries for a specific trace ID Entries are returned in chronological order (by sequence number)

func (*MessageLogger) GetLogEntries

func (ml *MessageLogger) GetLogEntries(limit int) []MessageLogEntry

GetLogEntries returns recent log entries with optional limit

func (*MessageLogger) GetMessages

func (ml *MessageLogger) GetMessages() []MessageLogEntry

GetMessages returns recent log entries

func (*MessageLogger) GetStatistics

func (ml *MessageLogger) GetStatistics() map[string]any

GetStatistics returns runtime statistics

func (*MessageLogger) OpenAPISpec

func (ml *MessageLogger) OpenAPISpec() *OpenAPISpec

OpenAPISpec returns the OpenAPI specification for MessageLogger endpoints

func (*MessageLogger) RegisterHTTPHandlers

func (ml *MessageLogger) RegisterHTTPHandlers(prefix string, mux *http.ServeMux)

RegisterHTTPHandlers registers HTTP endpoints for the MessageLogger service

func (*MessageLogger) SetDecoder

func (ml *MessageLogger) SetDecoder(d *message.Decoder)

SetDecoder installs the payload Decoder used for typed BaseMessage parsing in handleMessage. nil disables typed parsing — messages fall through to the "raw" type. Production wires this from deps.PayloadRegistry; tests can leave it nil to log raw envelopes.

func (*MessageLogger) Start

func (ml *MessageLogger) Start(ctx context.Context) error

Start begins message observation

func (*MessageLogger) Stop

func (ml *MessageLogger) Stop(ctx context.Context) error

Stop gracefully stops the MessageLogger

type MessageLoggerConfig

type MessageLoggerConfig struct {
	// Subjects to monitor
	// Use "*" to discover subjects from accepted Registry declarations
	// Example: ["*"] or ["*", "debug.>"] or ["raw.udp.messages", "processed.>"]
	MonitorSubjects []string `json:"monitor_subjects"`

	// Maximum entries to keep in memory for querying
	MaxEntries int `json:"max_entries"`

	// Whether to output to stdout
	OutputToStdout bool `json:"output_to_stdout"`

	// SampleRate controls message sampling (1 in N messages logged)
	// 0 or 1 = log all messages, 10 = log 10% of messages
	SampleRate int `json:"sample_rate"`
}

MessageLoggerConfig holds configuration for the MessageLogger service Simple struct - no UnmarshalJSON, no Enabled field

func DefaultMessageLoggerConfig

func DefaultMessageLoggerConfig() MessageLoggerConfig

DefaultMessageLoggerConfig returns sensible defaults

func (MessageLoggerConfig) Validate

func (c MessageLoggerConfig) Validate() error

Validate checks if the configuration is valid

type Metrics

type Metrics struct {
	*BaseService
	// contains filtered or unexported fields
}

Metrics is a service that provides Prometheus metrics endpoint

func (*Metrics) ConfigSchema

func (m *Metrics) ConfigSchema() ConfigSchema

ConfigSchema returns the configuration schema for the metrics service. This implements the Configurable interface for UI discovery.

func (*Metrics) Path

func (m *Metrics) Path() string

Path returns the metrics endpoint path

func (*Metrics) Port

func (m *Metrics) Port() int

Port returns the port the metrics server is listening on

func (*Metrics) Start

func (m *Metrics) Start(ctx context.Context) error

Start starts the metrics HTTP server

func (*Metrics) Stop

func (m *Metrics) Stop(ctx context.Context) error

Stop stops the metrics HTTP server

func (*Metrics) URL

func (m *Metrics) URL() string

URL returns the full URL for the metrics endpoint

type MetricsConfig

type MetricsConfig struct {
	Port int    `json:"port"`
	Path string `json:"path"`
}

MetricsConfig holds configuration for the metrics service Simple struct - no UnmarshalJSON, no Enabled field

func (MetricsConfig) Validate

func (c MetricsConfig) Validate() error

Validate checks if the configuration is valid

type MetricsForwarder

type MetricsForwarder struct {
	*BaseService
	// contains filtered or unexported fields
}

MetricsForwarder implements periodic metrics publishing to NATS

func (*MetricsForwarder) Start

func (mf *MetricsForwarder) Start(ctx context.Context) error

Start begins metrics forwarding. Instances are single-use: once Stop has run its teardown the instance cannot be restarted — create a new one via the constructor (production disable→enable already does this via CreateService).

func (*MetricsForwarder) Stop

func (mf *MetricsForwarder) Stop(ctx context.Context) error

Stop gracefully stops the MetricsForwarder. Stop is idempotent per the Service contract (gh#520): a service that already reached a terminal state — e.g. via parent-context cancellation before the manager's StopAll visit — is a clean shutdown, and repeated calls are safe (gh#549). Teardown still runs on the already-stopped path so the ticker is released when cancellation wins the race.

type MetricsForwarderConfig

type MetricsForwarderConfig struct {
	// Push interval for metrics publishing (e.g., "5s", "1m")
	PushInterval string `json:"push_interval"`

	// IncludeGoMetrics enables forwarding of go_* runtime metrics (goroutines, memory, GC)
	// Default: false (excluded to reduce noise)
	IncludeGoMetrics bool `json:"include_go_metrics"`

	// IncludeProcMetrics enables forwarding of process_* metrics (CPU, open FDs, memory)
	// Default: false (excluded to reduce noise)
	IncludeProcMetrics bool `json:"include_proc_metrics"`
}

MetricsForwarderConfig holds configuration for the MetricsForwarder service Note: The service is enabled/disabled via types.ServiceConfig.Enabled at the outer level. If the service is created, it will forward metrics.

func (MetricsForwarderConfig) Validate

func (c MetricsForwarderConfig) Validate() error

Validate checks if the configuration is valid

type MetricsGatherer

type MetricsGatherer interface {
	Gather() ([]*dto.MetricFamily, error)
}

MetricsGatherer defines the interface for gathering metrics. This allows for easier testing with mocks.

type MilestoneService

type MilestoneService struct {
	*BaseService
	// contains filtered or unexported fields
}

MilestoneService is the Phase-B service wrapper for the agent-run milestone subscriber (ADR-058 rollout step 3). It drives the subscriber's durable JetStream consumers (agent.complete.* / agent.failed.*) under the ServiceManager's ordered shutdown, replacing the hand-rolled NewMilestoneSubscriber + Start + defer-stop block that was duplicated in both mains.

Start returns subscriber setup errors so a configured milestone observer never reports running without its durable consumers. A genuine consumer-start failure is forwarded so StartAll aborts boot. The stream-absent case is already a graceful no-op inside the subscriber (gh#246) — it returns a no-op stop with no error — so resourceless deploys (no agentic components) still boot.

Shutdown semantics: Start receives the ServiceManager's lifecycle ctx (the SIGTERM-derived signal context), which the subscriber binds its consumers to. The old inline wiring passed an uncancellable context, so in-flight HandleEvent calls ran to completion at shutdown; now reads and handlers observe cancellation. Stop() additionally cancels consumption via the captured stop func, so delivery halts regardless of ctx.

func NewMilestoneService

func NewMilestoneService(subscriber milestoneStarter, client *natsclient.Client, cfg agentrun.StartConfig, logger *slog.Logger) *MilestoneService

NewMilestoneService builds the MilestoneService over a pre-built subscriber. The composition root constructs the subscriber (R2 — this wrapper never does) and passes it plus the live NATS client and StartConfig.

func (*MilestoneService) Start

func (s *MilestoneService) Start(ctx context.Context) error

Start starts the subscriber's durable consumers. A double-Start returns an error as a bug-class guard. A genuine consumer-start failure is FORWARDED — StartAll aborts boot — because the subscriber is a hard dependency (see the type doc for why this is deliberately not R1-degraded). The subscriber's stream-absent graceful-skip returns a non-nil no-op stop with no error, so that path stores the no-op and reports running (boot preserved).

func (*MilestoneService) Stop

func (s *MilestoneService) Stop(ctx context.Context) error

Stop cancels the subscriber's local consumption (durable offsets persist in NATS for restart recovery). Running Stop is terminal and completed repeats are nil no-ops. Only failed-Start cleanupPending may retry its retained opaque cleanup closure under a later manager Stop context. Stop before Start is a no-op.

type OpenAPIDocument

type OpenAPIDocument struct {
	OpenAPI    string              `json:"openapi"`
	Info       InfoSpec            `json:"info"`
	Servers    []ServerSpec        `json:"servers"`
	Paths      map[string]PathSpec `json:"paths"`
	Components *ComponentsSpec     `json:"components,omitempty"`
	Tags       []TagSpec           `json:"tags,omitempty"`
}

OpenAPIDocument represents the complete OpenAPI 3.0 specification

type OpenAPISpec

type OpenAPISpec struct {
	Paths            map[string]PathSpec `json:"paths"`
	Components       map[string]any      `json:"components,omitempty"`
	Tags             []TagSpec           `json:"tags,omitempty"`
	ResponseTypes    []reflect.Type      `json:"-"` // Response types to generate schemas for (not serialized)
	RequestBodyTypes []reflect.Type      `json:"-"` // Request body types to generate schemas for (not serialized)
}

OpenAPISpec represents a service's OpenAPI specification fragment

func GetOpenAPISpec

func GetOpenAPISpec(name string) (*OpenAPISpec, bool)

GetOpenAPISpec returns the OpenAPI specification for a specific service.

func NewOpenAPISpec

func NewOpenAPISpec() *OpenAPISpec

NewOpenAPISpec creates a new OpenAPI specification fragment for a service

func (*OpenAPISpec) AddPath

func (spec *OpenAPISpec) AddPath(path string, pathSpec PathSpec)

AddPath adds a path specification to the OpenAPI spec

func (*OpenAPISpec) AddTag

func (spec *OpenAPISpec) AddTag(name, description string)

AddTag adds a tag to the OpenAPI spec

func (*OpenAPISpec) MarshalJSON

func (spec *OpenAPISpec) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for OpenAPISpec

func (*OpenAPISpec) UnmarshalJSON

func (spec *OpenAPISpec) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for OpenAPISpec

type OperationSpec

type OperationSpec struct {
	Summary     string                  `json:"summary"`
	Description string                  `json:"description,omitempty"`
	Parameters  []ParameterSpec         `json:"parameters,omitempty"`
	RequestBody *RequestBodySpec        `json:"request_body,omitempty"`
	Responses   map[string]ResponseSpec `json:"responses"`
	Tags        []string                `json:"tags,omitempty"`
}

OperationSpec defines a single HTTP operation

type Option

type Option func(*BaseService)

Option is a functional option for configuring BaseService

func OnHealthChange

func OnHealthChange(fn func(bool)) Option

OnHealthChange sets a callback for health state changes

func WithHealthCheck

func WithHealthCheck(fn HealthCheckFunc) Option

WithHealthCheck sets a custom health check function

func WithHealthInterval

func WithHealthInterval(interval time.Duration) Option

WithHealthInterval sets the health check interval

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets a custom logger for the service

func WithMetrics

func WithMetrics(registry *metric.MetricsRegistry) Option

WithMetrics sets the metrics registry for the service

func WithNATS

func WithNATS(client *natsclient.Client) Option

WithNATS sets the NATS client for the service

type OverallHealth

type OverallHealth struct {
	Status        string `json:"status"` // "healthy", "degraded", "error"
	RunningCount  int    `json:"running_count"`
	DegradedCount int    `json:"degraded_count"`
	ErrorCount    int    `json:"error_count"`
}

OverallHealth provides aggregate health status and counts

type ParameterSpec

type ParameterSpec struct {
	Name        string `json:"name"`
	In          string `json:"in"` // "query", "path", "header"
	Description string `json:"description,omitempty"`
	Required    bool   `json:"required,omitempty"`
	Schema      Schema `json:"schema,omitempty"`
}

ParameterSpec defines an operation parameter

type PathSpec

type PathSpec struct {
	GET    *OperationSpec `json:"get,omitempty"`
	POST   *OperationSpec `json:"post,omitempty"`
	PUT    *OperationSpec `json:"put,omitempty"`
	PATCH  *OperationSpec `json:"patch,omitempty"`
	DELETE *OperationSpec `json:"delete,omitempty"`
}

PathSpec defines HTTP operations for a specific path

type PendingServiceChange

type PendingServiceChange struct {
	Name   string `json:"name"`
	Change string `json:"change"`
}

PendingServiceChange describes one structural desired-service change that a process restart is required to attempt to consume.

type ProjectionBinder

type ProjectionBinder interface {
	ProjectionBindings() (packID string, contracts []projection.Contract)
	PreflightProjectionMutations() error
	SetPredicateReconciler(projection.PredicateReconciler) error
}

ProjectionBinder is implemented by rule processors that use reconciled projection writes. Contracts are validated before any client is injected.

type PropertySchema

type PropertySchema struct {
	component.PropertySchema

	// Category groups related properties for UI organization
	Category string `json:"category,omitempty"`
}

PropertySchema extends component.PropertySchema with service-specific fields

type Registry

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

Registry manages service constructor registration

func NewServiceRegistry

func NewServiceRegistry() *Registry

NewServiceRegistry creates a new service registry

func (*Registry) Constructor

func (r *Registry) Constructor(name string) (Constructor, bool)

Constructor returns a constructor for the given service name

func (*Registry) Constructors

func (r *Registry) Constructors() map[string]Constructor

Constructors returns a copy of all constructors

func (*Registry) Register

func (r *Registry) Register(name string, constructor Constructor) error

Register registers a service constructor

func (*Registry) Services

func (r *Registry) Services() []string

Services returns all registered service names

type RequestBodySpec

type RequestBodySpec struct {
	Description string `json:"description,omitempty"`
	ContentType string `json:"content_type,omitempty"` // defaults to "application/json"
	SchemaRef   string `json:"schema_ref,omitempty"`   // e.g. "#/components/schemas/ReviewRequest"
	Required    bool   `json:"required,omitempty"`
}

RequestBodySpec defines an operation request body

type ResponseSpec

type ResponseSpec struct {
	Description string `json:"description"`
	ContentType string `json:"content_type,omitempty"`
	SchemaRef   string `json:"schema_ref,omitempty"` // $ref to schema, e.g., "#/components/schemas/RuntimeHealthResponse"
	IsArray     bool   `json:"is_array,omitempty"`   // If true, response is an array of SchemaRef items
}

ResponseSpec defines an operation response

type RuntimeHealthResponse

type RuntimeHealthResponse struct {
	Timestamp  time.Time         `json:"timestamp"`
	Overall    OverallHealth     `json:"overall"`
	Components []ComponentHealth `json:"components"`
}

RuntimeHealthResponse represents the JSON response for runtime health

type RuntimeMessage

type RuntimeMessage struct {
	Timestamp   string         `json:"timestamp"`
	Subject     string         `json:"subject"`
	MessageID   string         `json:"message_id"`
	Component   string         `json:"component"`
	Direction   string         `json:"direction"`
	Summary     string         `json:"summary"`
	Metadata    map[string]any `json:"metadata,omitempty"`
	MessageType string         `json:"message_type,omitempty"`
}

RuntimeMessage represents a formatted message entry for UI consumption

type RuntimeMessagesResponse

type RuntimeMessagesResponse struct {
	Timestamp string           `json:"timestamp"`
	Messages  []RuntimeMessage `json:"messages"`
	Total     int              `json:"total"`
	Limit     int              `json:"limit"`
	Note      string           `json:"note,omitempty"`
}

RuntimeMessagesResponse represents the response structure for runtime messages

type RuntimeMetricsResponse

type RuntimeMetricsResponse struct {
	Timestamp           time.Time         `json:"timestamp"`
	PrometheusAvailable bool              `json:"prometheus_available"`
	Components          []ComponentMetric `json:"components"`
}

RuntimeMetricsResponse represents the JSON response for runtime metrics

type Schema

type Schema struct {
	Type   string `json:"type"`
	Format string `json:"format,omitempty"`
}

Schema defines parameter or response schema

type ServerSpec

type ServerSpec struct {
	URL         string `json:"url"`
	Description string `json:"description"`
}

ServerSpec defines an API server

type Service

type Service interface {
	Name() string
	Start(ctx context.Context) error
	// Stop stops the service gracefully. After exact owner completion, another
	// invocation returns success (nil or ErrAlreadyStopped) without repeating
	// teardown. Parent cancellation can complete the owner before coordinated
	// shutdown reaches it; StatusStopping alone does not prove that completion
	// or authorize the manager to infer success (gh#520).
	Stop(ctx context.Context) error
	Status() Status
	IsHealthy() bool       // Keep for compatibility during migration
	GetStatus() Info       // Keep for compatibility during migration
	Health() health.Status // NEW: Standard health reporting
	RegisterMetrics(registrar metric.MetricsRegistrar) error
}

Service interface defines the contract for all services

func NewComponentManager

func NewComponentManager(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

NewComponentManager creates a new ComponentManager using the standard constructor pattern

func NewFlowServiceFromConfig

func NewFlowServiceFromConfig(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

NewFlowServiceFromConfig creates a saved-flow service.

func NewHeartbeatService

func NewHeartbeatService(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

NewHeartbeatService creates a new heartbeat service using the standard constructor pattern

func NewLogForwarderService

func NewLogForwarderService(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

NewLogForwarderService creates a new log forwarder service using the standard constructor pattern. With the new architecture, LogForwarder no longer intercepts slog - logs are published to NATS directly by the NATSLogHandler in pkg/logging. This service exists for configuration management and potential future features (e.g., log aggregation, filtering at the service level).

func NewMessageLoggerService

func NewMessageLoggerService(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

NewMessageLoggerService creates a new message logger service using the standard constructor pattern

func NewMetrics

func NewMetrics(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

NewMetrics creates a new metrics service using the standard constructor pattern

func NewMetricsForwarderService

func NewMetricsForwarderService(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

NewMetricsForwarderService creates a new metrics forwarder service using the standard constructor pattern

func NewStorageObservabilityService

func NewStorageObservabilityService(rawConfig json.RawMessage, deps *Dependencies) (Service, error)

NewStorageObservabilityService builds the service from its config block.

type Status

type Status int

Status represents the current status of a service

const (
	StatusStopped Status = iota
	StatusStarting
	StatusRunning
	StatusStopping
)

Possible service statuses

func (Status) String

func (s Status) String() string

String returns the string representation of Status

type StorageObservabilityConfig

type StorageObservabilityConfig struct {
	// Interval is how often the account is enumerated. Every process polling
	// account-wide multiplies cost by deployment size, so an operator running
	// many instances turns this down rather than losing the view entirely.
	// Default: 1m.
	Interval string `` /* 133-byte string literal not displayed */

	// Timeout bounds ONE collection, both account listings included. It must
	// fit inside Interval. Default: 15s.
	Timeout string `` /* 163-byte string literal not displayed */

	// PressureThresholds are the headroom and time-to-threshold bands pressure
	// is derived from. Report-only: nothing is rejected, throttled, or degraded
	// when a band is crossed.
	PressureThresholds natsclient.StoragePressureThresholds `` /* 168-byte string literal not displayed */
}

StorageObservabilityConfig is the operator surface for account storage observability.

Durations are strings ("1m", "30s") following the repository's service configuration convention. Every field is optional and takes its documented default; an unusable value is an ERROR at construction rather than a silent fallback, because a silently defaulted knob applies a number the operator did not choose and is indistinguishable from a working edit.

type StorageObservabilityService

type StorageObservabilityService struct {
	*BaseService
	// contains filtered or unexported fields
}

StorageObservabilityService collects the account storage inventory, publishes the report, and serves it as metrics and health status.

func (*StorageObservabilityService) Health

Health reports the storage picture WITHOUT letting it become a verdict.

The lifecycle status comes from BaseService untouched; pressure, capacity and over-commitment appear in the message. That separation is the report-only guarantee expressed where it would break first: readiness reads Status() and IsHealthy(), and /health aggregates Health(), so a pressure-derived verdict at any of those points would turn observability into admission control.

func (*StorageObservabilityService) OpenAPISpec

func (s *StorageObservabilityService) OpenAPISpec() *OpenAPISpec

OpenAPISpec documents the report route for the runtime `/openapi.json`.

It is deliberately NOT registered through RegisterOpenAPISpec: the generator writes each registered spec's paths at the document ROOT, unprefixed (cmd/openapi-generator/openapi_builder.go), so registering would publish a bare `/report` in specs/openapi.v3.yaml — a top-level path that says nothing about which service serves it. The runtime document, which applies the service prefix, gets the route either way.

func (*StorageObservabilityService) RegisterHTTPHandlers

func (s *StorageObservabilityService) RegisterHTTPHandlers(prefix string, mux *http.ServeMux)

RegisterHTTPHandlers mounts the operator report route.

func (*StorageObservabilityService) RegisterMetrics

func (s *StorageObservabilityService) RegisterMetrics(registrar metric.MetricsRegistrar) error

RegisterMetrics implements the Service interface. The constructor already registers through the injected registry, so this is the same idempotent registration against whatever registrar a caller supplies.

func (*StorageObservabilityService) Snapshot

Snapshot exposes the consumed report for surfaces built on top of this service. It is a read of the PUBLISHED report, never a recomputation.

func (*StorageObservabilityService) Start

Start begins collecting and consuming. Instances are single-use, matching the sibling services: once Stop has run its teardown, create a new one.

func (*StorageObservabilityService) Stop

Stop halts collection and consumption. Idempotent per the Service contract.

type StorageReportResponse

type StorageReportResponse struct {
	// ReportOnly is always true and is stated on the response's face. A reader
	// automating against this route must be able to see, without consulting a
	// document, that nothing was rejected, throttled, or degraded because of
	// what it says.
	ReportOnly bool `json:"report_only"`

	// Synced reports that the watch has delivered every current value at least
	// once. Before it, an empty Resources means "not read yet" rather than "the
	// account holds nothing" — two facts an operator acts on differently.
	Synced bool `json:"synced"`

	// UpdatedAt is when the last change was applied to the in-process view. A
	// POINTER because `omitempty` is a no-op on a time.Time: a value field would
	// publish a zero timestamp on an unread report and invite a reader to treat
	// it as a real one.
	UpdatedAt *time.Time `json:"updated_at,omitempty"`

	// Summary is a TALLY of what the rows say, carried through from the
	// consumer. It is never a re-evaluation.
	Summary StorageReportSummary `json:"summary"`

	// Account is the per-tier declared-versus-limit comparison. Absent — not
	// zero-valued — when no account row has been read, because an empty
	// comparison reads as "no tiers, nothing over-committed".
	Account *natsclient.AccountReport `json:"account,omitempty"`

	// Resources is every published row, sorted by resource name. Never
	// filtered: see the file comment.
	Resources []natsclient.ResourceReport `json:"resources"`
}

StorageReportResponse is the body of `GET /storage-observability/report`.

Resources are the PUBLISHED rows verbatim — the same natsclient.ResourceReport values the collector wrote and the metrics surface consumed — so the HTTP shape cannot drift away from the bucket's. Nothing here is recomputed.

type StorageReportSummary

type StorageReportSummary struct {
	// Resources is how many rows the report carries.
	Resources int `json:"resources"`

	// Pressure counts the EVALUATED rows by state.
	Pressure map[natsclient.PressureState]int `json:"pressure"`

	// NotEvaluated counts the rows carrying no pressure state at all. Its own
	// bucket rather than folded into normal: otherwise the resources nothing can
	// be said about would be the ones that disappear from the summary (task 4.7).
	//
	// The set CHANGED in task 5.9 and a consumer reading this field as "the
	// unbounded resources" now undercounts. A resource with no bound of its own is
	// evaluated against its storage tier's ceiling and counts under Pressure by
	// that state; what remains here is a resource whose capacity could not be
	// READ, plus an unbounded one whose tier offers no ceiling either. To count
	// the unbounded set, read the rows' capacity state — not this field.
	NotEvaluated int `json:"not_evaluated"`

	// WorstPressure is the worst EVALUATED state, omitted when nothing was
	// evaluated. Omission is NOT normal: an account whose every row declined to
	// evaluate has no pressure verdict, and publishing one would manufacture it.
	WorstPressure natsclient.PressureState `json:"worst_pressure,omitempty"`
}

StorageReportSummary is the account at a glance.

type TagSpec

type TagSpec struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

TagSpec defines an API tag for grouping operations

Jump to

Keyboard shortcuts

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