From 01b9156403e2add70e3b7131769afa94e872a0ee Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 08:31:21 -0600 Subject: [PATCH 1/8] style: gofmt entire tree Co-Authored-By: Claude Fable 5 --- bundles/configloader/bundle.go | 31 ++++++------ bundles/configloader/bundle_test.go | 72 ++++++++++++++------------- bundles/httpclient/bundle.go | 76 ++++++++++++++--------------- bundles/jwt/bundle.go | 13 ++--- bundles/jwt/bundle_test.go | 6 +-- bundles/postgresql/bundle.go | 3 +- bundles/prometheus/bundle.go | 40 +++++++-------- bundles/prometheus/bundle_test.go | 48 +++++++++--------- bundles/redis/bundle.go | 12 ++--- config/base.go | 15 +++--- config/base_test.go | 8 +-- doc.go | 2 +- errors/errors.go | 2 +- errors/errors_test.go | 4 +- examples/config-service/main.go | 40 +++++++-------- examples/httpclient-service/main.go | 72 +++++++++++++-------------- examples/jwt-service/main.go | 44 ++++++++--------- examples/postgresql-service/main.go | 14 +++--- examples/prometheus-service/main.go | 56 ++++++++++----------- examples/redis-service/main.go | 38 +++++++-------- examples/simple-service/main.go | 2 +- framework/app_test.go | 18 +++---- framework/http.go | 22 ++++----- framework/logging.go | 2 +- framework/observability.go | 12 ++--- framework/shutdown.go | 3 +- health/check.go | 2 +- health/registry.go | 2 +- health/registry_test.go | 8 +-- health/status.go | 2 +- testutil/testutil.go | 12 ++--- 31 files changed, 343 insertions(+), 338 deletions(-) diff --git a/bundles/configloader/bundle.go b/bundles/configloader/bundle.go index fd8a9f3..3bb74d0 100644 --- a/bundles/configloader/bundle.go +++ b/bundles/configloader/bundle.go @@ -51,13 +51,16 @@ // # Environment Variable Binding // // The loader automatically binds environment variables based on: +// // - Field names (converted to UPPER_SNAKE_CASE) +// // - `env` struct tags for custom names +// // - `envPrefix` configuration for namespacing // -// DatabaseURL string `env:"DATABASE_URL"` // Exact name -// APITimeout int `env:"API_TIMEOUT_SECONDS"` // Custom name -// Debug bool // Automatic: DEBUG or MYSERVICE_DEBUG (with prefix) +// DatabaseURL string `env:"DATABASE_URL"` // Exact name +// APITimeout int `env:"API_TIMEOUT_SECONDS"` // Custom name +// Debug bool // Automatic: DEBUG or MYSERVICE_DEBUG (with prefix) // // # Hot Reload // @@ -122,8 +125,8 @@ type Config struct { SecureLogging bool // Security configuration - MaxFileSize int64 // Maximum configuration file size (default: 1MB) - AllowedPaths []string // Allowed configuration file directories + MaxFileSize int64 // Maximum configuration file size (default: 1MB) + AllowedPaths []string // Allowed configuration file directories RequiredFileMode os.FileMode // Required file permissions (default: 0o644) } @@ -275,19 +278,19 @@ func (b *Bundle) initializeWatcher() error { // Loader provides configuration loading functionality. type Loader struct { - config Config - loadedFrom string + config Config + loadedFrom string changeCallbacks []func(interface{}) - mu sync.RWMutex + mu sync.RWMutex } // LoadResult contains information about the configuration loading process. type LoadResult struct { - LoadedFrom string `json:"loaded_from"` - Sources []string `json:"sources"` - EnvVarsUsed []string `json:"env_vars_used"` - DefaultsApplied []string `json:"defaults_applied"` - ValidationErrors []string `json:"validation_errors,omitempty"` + LoadedFrom string `json:"loaded_from"` + Sources []string `json:"sources"` + EnvVarsUsed []string `json:"env_vars_used"` + DefaultsApplied []string `json:"defaults_applied"` + ValidationErrors []string `json:"validation_errors,omitempty"` } // Load loads configuration into the provided struct from multiple sources. @@ -840,4 +843,4 @@ func MustLoadConfig[T any](configPaths ...string) (*T, *LoadResult) { panic(fmt.Sprintf("Configuration loading failed: %v", err)) } return cfg, result -} \ No newline at end of file +} diff --git a/bundles/configloader/bundle_test.go b/bundles/configloader/bundle_test.go index ba77cb6..64412f4 100644 --- a/bundles/configloader/bundle_test.go +++ b/bundles/configloader/bundle_test.go @@ -127,10 +127,10 @@ func TestLoaderLoad_DefaultsApplied(t *testing.T) { } l := loaderWithConfig(Config{ - ConfigPaths: []string{"./nonexistent.yaml"}, + ConfigPaths: []string{"./nonexistent.yaml"}, RequireConfigFile: false, - ValidateOnLoad: false, - MaxFileSize: 1024 * 1024, + ValidateOnLoad: false, + MaxFileSize: 1024 * 1024, }) var cfg TestCfg @@ -162,10 +162,10 @@ func TestLoaderLoad_EnvVarOverride(t *testing.T) { t.Setenv("TEST_SERVICE_URL_CONFIGLOADER", "http://production:9090") l := loaderWithConfig(Config{ - ConfigPaths: []string{"./nonexistent.yaml"}, + ConfigPaths: []string{"./nonexistent.yaml"}, RequireConfigFile: false, - ValidateOnLoad: false, - MaxFileSize: 1024 * 1024, + ValidateOnLoad: false, + MaxFileSize: 1024 * 1024, }) var cfg TestCfg @@ -217,11 +217,11 @@ func TestLoaderLoad_FromYAMLFile(t *testing.T) { } l := loaderWithConfig(Config{ - ConfigPaths: []string{cfgPath}, + ConfigPaths: []string{cfgPath}, RequireConfigFile: true, - ValidateOnLoad: false, - MaxFileSize: 1024 * 1024, - AllowedPaths: []string{dir}, + ValidateOnLoad: false, + MaxFileSize: 1024 * 1024, + AllowedPaths: []string{dir}, }) var cfg TestCfg @@ -257,11 +257,11 @@ func TestLoaderLoad_FromJSONFile(t *testing.T) { } l := loaderWithConfig(Config{ - ConfigPaths: []string{cfgPath}, + ConfigPaths: []string{cfgPath}, RequireConfigFile: true, - ValidateOnLoad: false, - MaxFileSize: 1024 * 1024, - AllowedPaths: []string{dir}, + ValidateOnLoad: false, + MaxFileSize: 1024 * 1024, + AllowedPaths: []string{dir}, }) var cfg TestCfg @@ -281,10 +281,10 @@ func TestLoaderLoad_FromJSONFile(t *testing.T) { // TestLoaderLoad_RequireConfigFile_NotFound tests error when required file missing. func TestLoaderLoad_RequireConfigFile_NotFound(t *testing.T) { l := loaderWithConfig(Config{ - ConfigPaths: []string{"./definitely_does_not_exist_xyz.yaml"}, + ConfigPaths: []string{"./definitely_does_not_exist_xyz.yaml"}, RequireConfigFile: true, - ValidateOnLoad: false, - MaxFileSize: 1024 * 1024, + ValidateOnLoad: false, + MaxFileSize: 1024 * 1024, }) var cfg struct{ Name string } @@ -305,14 +305,16 @@ func TestLoaderLoad_FileTooLarge(t *testing.T) { } l := loaderWithConfig(Config{ - ConfigPaths: []string{cfgPath}, + ConfigPaths: []string{cfgPath}, RequireConfigFile: false, - ValidateOnLoad: false, - MaxFileSize: 5, // very small - AllowedPaths: []string{dir}, + ValidateOnLoad: false, + MaxFileSize: 5, // very small + AllowedPaths: []string{dir}, }) - var cfg struct{ Key string `yaml:"key"` } + var cfg struct { + Key string `yaml:"key"` + } _, err := l.Load(&cfg) if err == nil { t.Error("expected error for oversized config file") @@ -448,12 +450,12 @@ func TestSetFieldValue(t *testing.T) { l := loaderWithConfig(DefaultConfig()) type TestCfg struct { - StrField string - IntField int - BoolField bool - FloatField float64 - UintField uint - SliceField []string + StrField string + IntField int + BoolField bool + FloatField float64 + UintField uint + SliceField []string } cfg := TestCfg{} @@ -545,10 +547,10 @@ func TestBundle_Close(t *testing.T) { // TestLoaderMustLoad_Panics tests that MustLoad panics on error. func TestLoaderMustLoad_Panics(t *testing.T) { l := loaderWithConfig(Config{ - ConfigPaths: []string{"./no_such_file.yaml"}, + ConfigPaths: []string{"./no_such_file.yaml"}, RequireConfigFile: true, - ValidateOnLoad: false, - MaxFileSize: 1024, + ValidateOnLoad: false, + MaxFileSize: 1024, }) defer func() { @@ -575,11 +577,11 @@ func TestLoaderReload(t *testing.T) { } l := loaderWithConfig(Config{ - ConfigPaths: []string{cfgPath}, + ConfigPaths: []string{cfgPath}, RequireConfigFile: false, - ValidateOnLoad: false, - MaxFileSize: 1024 * 1024, - AllowedPaths: []string{dir}, + ValidateOnLoad: false, + MaxFileSize: 1024 * 1024, + AllowedPaths: []string{dir}, }) var cfg TestCfg diff --git a/bundles/httpclient/bundle.go b/bundles/httpclient/bundle.go index 80394d5..ec93d27 100644 --- a/bundles/httpclient/bundle.go +++ b/bundles/httpclient/bundle.go @@ -134,10 +134,10 @@ type Config struct { Timeout time.Duration // Transport configuration - MaxIdleConns int // Maximum idle connections (default: 100) - MaxIdleConnsPerHost int // Maximum idle connections per host (default: 10) - IdleConnTimeout time.Duration // Idle connection timeout (default: 90 seconds) - TLSHandshakeTimeout time.Duration // TLS handshake timeout (default: 10 seconds) + MaxIdleConns int // Maximum idle connections (default: 100) + MaxIdleConnsPerHost int // Maximum idle connections per host (default: 10) + IdleConnTimeout time.Duration // Idle connection timeout (default: 90 seconds) + TLSHandshakeTimeout time.Duration // TLS handshake timeout (default: 10 seconds) ExpectContinueTimeout time.Duration // Expect 100-continue timeout (default: 1 second) // TLS configuration @@ -157,11 +157,11 @@ type Config struct { CircuitBreakerConfig CircuitBreakerConfig // Logging and observability - EnableRequestLogging bool // Enable request/response logging - EnableMetrics bool // Enable request metrics collection - LogRequestBody bool // Log request bodies (be careful with sensitive data) - LogResponseBody bool // Log response bodies (be careful with sensitive data) - MaxLogBodySize int // Maximum body size to log (default: 1024 bytes) + EnableRequestLogging bool // Enable request/response logging + EnableMetrics bool // Enable request metrics collection + LogRequestBody bool // Log request bodies (be careful with sensitive data) + LogResponseBody bool // Log response bodies (be careful with sensitive data) + MaxLogBodySize int // Maximum body size to log (default: 1024 bytes) // User agent UserAgent string // User agent string (default: "Forge-HTTP-Client/1.0") @@ -169,44 +169,44 @@ type Config struct { // RetryConfig contains retry policy configuration. type RetryConfig struct { - MaxRetries int // Maximum number of retries (default: 3) - InitialInterval time.Duration // Initial retry interval (default: 100ms) - MaxInterval time.Duration // Maximum retry interval (default: 5s) - Multiplier float64 // Backoff multiplier (default: 2.0) - RandomizationFactor float64 // Randomization factor (default: 0.1) + MaxRetries int // Maximum number of retries (default: 3) + InitialInterval time.Duration // Initial retry interval (default: 100ms) + MaxInterval time.Duration // Maximum retry interval (default: 5s) + Multiplier float64 // Backoff multiplier (default: 2.0) + RandomizationFactor float64 // Randomization factor (default: 0.1) } // CircuitBreakerConfig contains circuit breaker configuration. type CircuitBreakerConfig struct { - Name string // Circuit breaker name for metrics - MaxRequests uint32 // Max requests in half-open state (default: 3) - Interval time.Duration // Interval to clear failure counts (default: 60s) - Timeout time.Duration // Timeout in open state (default: 30s) - ReadyToTrip func(counts gobreaker.Counts) bool // Custom trip function + Name string // Circuit breaker name for metrics + MaxRequests uint32 // Max requests in half-open state (default: 3) + Interval time.Duration // Interval to clear failure counts (default: 60s) + Timeout time.Duration // Timeout in open state (default: 30s) + ReadyToTrip func(counts gobreaker.Counts) bool // Custom trip function } // DefaultConfig returns a Config with sensible defaults. func DefaultConfig() Config { return Config{ - Timeout: 30 * time.Second, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - APIKeyHeader: "X-API-Key", - EnableRequestLogging: true, - EnableMetrics: true, - LogRequestBody: false, - LogResponseBody: false, - MaxLogBodySize: 1024, - UserAgent: "Forge-HTTP-Client/1.0", + Timeout: 30 * time.Second, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + APIKeyHeader: "X-API-Key", + EnableRequestLogging: true, + EnableMetrics: true, + LogRequestBody: false, + LogResponseBody: false, + MaxLogBodySize: 1024, + UserAgent: "Forge-HTTP-Client/1.0", RetryConfig: RetryConfig{ - MaxRetries: 3, - InitialInterval: 100 * time.Millisecond, - MaxInterval: 5 * time.Second, - Multiplier: 2.0, - RandomizationFactor: 0.1, + MaxRetries: 3, + InitialInterval: 100 * time.Millisecond, + MaxInterval: 5 * time.Second, + Multiplier: 2.0, + RandomizationFactor: 0.1, }, CircuitBreakerConfig: CircuitBreakerConfig{ MaxRequests: 3, @@ -883,4 +883,4 @@ func (c *Client) GetCircuitBreakerState() gobreaker.State { // GetCircuitBreakerCounts returns the current circuit breaker counts. func (c *Client) GetCircuitBreakerCounts() gobreaker.Counts { return c.circuitBreaker.Counts() -} \ No newline at end of file +} diff --git a/bundles/jwt/bundle.go b/bundles/jwt/bundle.go index a34c6c8..ab1b5dc 100644 --- a/bundles/jwt/bundle.go +++ b/bundles/jwt/bundle.go @@ -121,9 +121,9 @@ type Config struct { // DefaultConfig returns a Config with sensible secure defaults. func DefaultConfig() Config { return Config{ - TokenDuration: 1 * time.Hour, // Shorter duration for better security - ClockSkew: 1 * time.Minute, // Reduced clock skew tolerance - RequireHTTPS: true, // Secure by default + TokenDuration: 1 * time.Hour, // Shorter duration for better security + ClockSkew: 1 * time.Minute, // Reduced clock skew tolerance + RequireHTTPS: true, // Secure by default SkipPaths: []string{"/health"}, // Only health checks skip auth by default } } @@ -171,9 +171,9 @@ type ServiceClaims struct { // Bundle provides JWT authentication for Forge applications. type Bundle struct { - config Config - parser *jwt.Parser - tokenCache *tokenCache + config Config + parser *jwt.Parser + tokenCache *tokenCache } // NewBundle creates a new JWT authentication bundle. @@ -663,6 +663,7 @@ func validateServiceIdentifier(identifier string) error { return nil } + // Stop implements the Bundle interface for graceful shutdown. // JWT bundle has no persistent resources requiring cleanup. func (b *Bundle) Stop(ctx context.Context) error { diff --git a/bundles/jwt/bundle_test.go b/bundles/jwt/bundle_test.go index 80ab4a8..093d5ac 100644 --- a/bundles/jwt/bundle_test.go +++ b/bundles/jwt/bundle_test.go @@ -395,7 +395,7 @@ func TestBundle_ClaimsFromContext(t *testing.T) { // responseRecorder is a minimal ResponseWriter for JWT tests. type responseRecorder struct { - Code int + Code int header http.Header } @@ -403,9 +403,9 @@ func newResponseRecorder() *responseRecorder { return &responseRecorder{Code: 200, header: make(http.Header)} } -func (r *responseRecorder) Header() http.Header { return r.header } +func (r *responseRecorder) Header() http.Header { return r.header } func (r *responseRecorder) Write(b []byte) (int, error) { return len(b), nil } -func (r *responseRecorder) WriteHeader(code int) { r.Code = code } +func (r *responseRecorder) WriteHeader(code int) { r.Code = code } // TestHasPermission tests permission checking. func TestHasPermission(t *testing.T) { diff --git a/bundles/postgresql/bundle.go b/bundles/postgresql/bundle.go index a45558e..6a85907 100644 --- a/bundles/postgresql/bundle.go +++ b/bundles/postgresql/bundle.go @@ -219,7 +219,6 @@ func (b *Bundle) HealthChecks() []forgeHealth.Check { } } - // PostgreSQLHealthCheck implements health checking for PostgreSQL connections. type PostgreSQLHealthCheck struct { db *sql.DB @@ -281,4 +280,4 @@ func (c *PostgreSQLHealthCheck) Readiness(ctx context.Context) error { } return nil -} \ No newline at end of file +} diff --git a/bundles/prometheus/bundle.go b/bundles/prometheus/bundle.go index a6e24df..6d402d0 100644 --- a/bundles/prometheus/bundle.go +++ b/bundles/prometheus/bundle.go @@ -167,8 +167,8 @@ func (c *Config) Validate() error { } // Prevent sensitive data in labels if strings.Contains(strings.ToLower(key), "password") || - strings.Contains(strings.ToLower(key), "secret") || - strings.Contains(strings.ToLower(key), "token") { + strings.Contains(strings.ToLower(key), "secret") || + strings.Contains(strings.ToLower(key), "token") { return fmt.Errorf("service label %s appears to contain sensitive data", key) } } @@ -205,12 +205,12 @@ type Bundle struct { logger zerolog.Logger // Application metrics - httpRequestsTotal *prometheus.CounterVec - httpRequestDuration *prometheus.HistogramVec - grpcRequestsTotal *prometheus.CounterVec - grpcRequestDuration *prometheus.HistogramVec - healthCheckDuration *prometheus.HistogramVec - healthCheckTotal *prometheus.CounterVec + httpRequestsTotal *prometheus.CounterVec + httpRequestDuration *prometheus.HistogramVec + grpcRequestsTotal *prometheus.CounterVec + grpcRequestDuration *prometheus.HistogramVec + healthCheckDuration *prometheus.HistogramVec + healthCheckTotal *prometheus.CounterVec // Bundle integration metrics dbConnectionsActive prometheus.Gauge @@ -675,7 +675,7 @@ func isValidLabelName(name string) bool { for i := 1; i < len(name); i++ { char := name[i] if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || - (char >= '0' && char <= '9') || char == '_') { + (char >= '0' && char <= '9') || char == '_') { return false } } @@ -686,18 +686,18 @@ func isValidLabelName(name string) bool { // SecurityConfig contains security configuration for the metrics endpoint. type SecurityConfig struct { MaxRequestsInFlight int // Maximum concurrent requests (default: 3) - Timeout time.Duration // Request timeout (default: 10s) - EnableBasicAuth bool // Enable basic authentication - Username string // Basic auth username - Password string // Basic auth password + Timeout time.Duration // Request timeout (default: 10s) + EnableBasicAuth bool // Enable basic authentication + Username string // Basic auth username + Password string // Basic auth password } // DefaultSecurityConfig returns secure defaults for metrics endpoint. func DefaultSecurityConfig() SecurityConfig { return SecurityConfig{ MaxRequestsInFlight: 3, - Timeout: 10 * time.Second, - EnableBasicAuth: false, + Timeout: 10 * time.Second, + EnableBasicAuth: false, } } @@ -712,7 +712,7 @@ func (b *Bundle) GetSecureMetricsHandler(secConfig SecurityConfig) http.Handler b.gatherer, promhttp.HandlerOpts{ EnableOpenMetrics: true, - Timeout: secConfig.Timeout, + Timeout: secConfig.Timeout, MaxRequestsInFlight: secConfig.MaxRequestsInFlight, }, ) @@ -802,8 +802,8 @@ func (c *PrometheusHealthCheck) Readiness(ctx context.Context) error { if family.Name != nil { name := *family.Name if strings.HasPrefix(name, c.bundle.config.Namespace) || - strings.HasPrefix(name, "go_") || - strings.HasPrefix(name, "process_") { + strings.HasPrefix(name, "go_") || + strings.HasPrefix(name, "process_") { hasNamespaceMetrics = true break } @@ -833,7 +833,7 @@ func isValidMetricName(name string) bool { for i := 1; i < len(name); i++ { char := name[i] if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || - (char >= '0' && char <= '9') || char == '_' || char == ':') { + (char >= '0' && char <= '9') || char == '_' || char == ':') { return false } } @@ -877,4 +877,4 @@ func (b *Bundle) Stop(ctx context.Context) error { // Metrics remain in Prometheus registry until process exit // No cleanup needed return nil -} \ No newline at end of file +} diff --git a/bundles/prometheus/bundle_test.go b/bundles/prometheus/bundle_test.go index 5922d6c..140a707 100644 --- a/bundles/prometheus/bundle_test.go +++ b/bundles/prometheus/bundle_test.go @@ -187,10 +187,10 @@ func TestDefaultConfig(t *testing.T) { func TestRecordHTTPRequest(t *testing.T) { cfg := Config{ - Namespace: "test", + Namespace: "test", EnableHTTPMetrics: true, - HistogramBuckets: []float64{0.01, 0.1, 1.0}, - ServiceLabels: map[string]string{}, + HistogramBuckets: []float64{0.01, 0.1, 1.0}, + ServiceLabels: map[string]string{}, } b := newInitializedBundle(t, cfg) @@ -202,10 +202,10 @@ func TestRecordHTTPRequest(t *testing.T) { func TestRecordHTTPRequest_MetricsDisabled(t *testing.T) { cfg := Config{ - Namespace: "test", + Namespace: "test", EnableHTTPMetrics: false, - HistogramBuckets: []float64{0.01, 0.1, 1.0}, - ServiceLabels: map[string]string{}, + HistogramBuckets: []float64{0.01, 0.1, 1.0}, + ServiceLabels: map[string]string{}, } b := newInitializedBundle(t, cfg) @@ -217,10 +217,10 @@ func TestRecordHTTPRequest_MetricsDisabled(t *testing.T) { func TestRecordGRPCRequest(t *testing.T) { cfg := Config{ - Namespace: "test", + Namespace: "test", EnableGRPCMetrics: true, - HistogramBuckets: []float64{0.01, 0.1, 1.0}, - ServiceLabels: map[string]string{}, + HistogramBuckets: []float64{0.01, 0.1, 1.0}, + ServiceLabels: map[string]string{}, } b := newInitializedBundle(t, cfg) @@ -230,10 +230,10 @@ func TestRecordGRPCRequest(t *testing.T) { func TestRecordGRPCRequest_MetricsDisabled(t *testing.T) { cfg := Config{ - Namespace: "test", + Namespace: "test", EnableGRPCMetrics: false, - HistogramBuckets: []float64{0.01, 0.1, 1.0}, - ServiceLabels: map[string]string{}, + HistogramBuckets: []float64{0.01, 0.1, 1.0}, + ServiceLabels: map[string]string{}, } b := newInitializedBundle(t, cfg) b.RecordGRPCRequest("/svc/Method", "OK", time.Millisecond) @@ -481,10 +481,10 @@ func TestCreateCustomCounter_TooManyLabels(t *testing.T) { func TestGetMetricsHandler(t *testing.T) { cfg := Config{ - Namespace: "test", + Namespace: "test", EnableHTTPMetrics: true, - HistogramBuckets: []float64{0.01, 0.1}, - ServiceLabels: map[string]string{}, + HistogramBuckets: []float64{0.01, 0.1}, + ServiceLabels: map[string]string{}, } b := newInitializedBundle(t, cfg) @@ -512,10 +512,10 @@ func TestGetSecureMetricsHandler_BasicAuth(t *testing.T) { secConfig := SecurityConfig{ MaxRequestsInFlight: 3, - Timeout: 10 * time.Second, - EnableBasicAuth: true, - Username: "admin", - Password: "secret", + Timeout: 10 * time.Second, + EnableBasicAuth: true, + Username: "admin", + Password: "secret", } handler := b.GetSecureMetricsHandler(secConfig) @@ -617,14 +617,14 @@ func TestPrometheusHealthCheck_Liveness(t *testing.T) { func TestPrometheusHealthCheck_Readiness(t *testing.T) { cfg := Config{ - Namespace: "test", + Namespace: "test", EnableDefaultMetrics: false, // avoid registering go/process collectors - EnableHTTPMetrics: true, - EnableGRPCMetrics: false, + EnableHTTPMetrics: true, + EnableGRPCMetrics: false, EnableHealthMetrics: false, EnableBundleMetrics: false, - HistogramBuckets: []float64{0.01, 0.1}, - ServiceLabels: map[string]string{}, + HistogramBuckets: []float64{0.01, 0.1}, + ServiceLabels: map[string]string{}, } b := newInitializedBundle(t, cfg) diff --git a/bundles/redis/bundle.go b/bundles/redis/bundle.go index da47d6b..a974afa 100644 --- a/bundles/redis/bundle.go +++ b/bundles/redis/bundle.go @@ -98,11 +98,11 @@ type Config struct { RedisURL string // Connection pool configuration - PoolSize int // Maximum number of socket connections (default: 10) - MinIdleConns int // Minimum number of idle connections (default: 2) - MaxIdleTime time.Duration // Maximum amount of time a connection may be idle (default: 30 minutes) - MaxConnAge time.Duration // Maximum amount of time a connection may be reused (default: 1 hour) - PoolTimeout time.Duration // Amount of time client waits for connection (default: 4 seconds) + PoolSize int // Maximum number of socket connections (default: 10) + MinIdleConns int // Minimum number of idle connections (default: 2) + MaxIdleTime time.Duration // Maximum amount of time a connection may be idle (default: 30 minutes) + MaxConnAge time.Duration // Maximum amount of time a connection may be reused (default: 1 hour) + PoolTimeout time.Duration // Amount of time client waits for connection (default: 4 seconds) // Timeouts DialTimeout time.Duration // Timeout for establishing new connections (default: 5 seconds) @@ -674,4 +674,4 @@ func (r *RateLimiter) Allow(ctx context.Context, key string, limit int, window t } return result.(int64) == 1, nil -} \ No newline at end of file +} diff --git a/config/base.go b/config/base.go index 99481d9..c35ee83 100644 --- a/config/base.go +++ b/config/base.go @@ -66,12 +66,13 @@ import ( // All fields support environment variable override using the env struct tag. // // Example environment variables: -// SERVICE_NAME=user-service -// APP_ENV=production -// GRPC_ADDR=:8080 -// HTTP_ADDR=:8081 -// LOG_LEVEL=info -// DATABASE_URL=postgres://localhost:5432/mydb +// +// SERVICE_NAME=user-service +// APP_ENV=production +// GRPC_ADDR=:8080 +// HTTP_ADDR=:8081 +// LOG_LEVEL=info +// DATABASE_URL=postgres://localhost:5432/mydb type BaseConfig struct { // ServiceName is the name of the service (e.g., "user-service", "auth-service") ServiceName string `yaml:"service_name" env:"SERVICE_NAME"` @@ -292,4 +293,4 @@ func (c *BaseConfig) ShouldEnableReflection() bool { // Validator is an interface for configuration structs that can validate themselves. type Validator interface { Validate() error -} \ No newline at end of file +} diff --git a/config/base_test.go b/config/base_test.go index b1fde18..f9f5e4d 100644 --- a/config/base_test.go +++ b/config/base_test.go @@ -178,10 +178,10 @@ func TestBaseConfig_IsProduction(t *testing.T) { // TestBaseConfig_ShouldEnableReflection tests reflection enablement logic func TestBaseConfig_ShouldEnableReflection(t *testing.T) { tests := []struct { - name string - env string - explicitEnable bool - expectedEnabled bool + name string + env string + explicitEnable bool + expectedEnabled bool }{ {"development implicit", "development", false, true}, {"production implicit", "production", false, false}, diff --git a/doc.go b/doc.go index 5382c65..f071729 100644 --- a/doc.go +++ b/doc.go @@ -97,4 +97,4 @@ // # More Information // // Visit https://github.com/datariot/forge for documentation, examples, and contribution guidelines. -package forge \ No newline at end of file +package forge diff --git a/errors/errors.go b/errors/errors.go index 645e70c..59131a9 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -300,4 +300,4 @@ func IsRepositoryError(err error) bool { } return false -} \ No newline at end of file +} diff --git a/errors/errors_test.go b/errors/errors_test.go index 482baa2..18747fd 100644 --- a/errors/errors_test.go +++ b/errors/errors_test.go @@ -186,8 +186,8 @@ func TestIsValidationError(t *testing.T) { // TestIsConfigurationError tests configuration error classification func TestIsConfigurationError(t *testing.T) { tests := []struct { - name string - err error + name string + err error isConfigErr bool }{ {"nil error", nil, false}, diff --git a/examples/config-service/main.go b/examples/config-service/main.go index 9955b79..dce1243 100644 --- a/examples/config-service/main.go +++ b/examples/config-service/main.go @@ -15,27 +15,27 @@ // // # Run the service // -// # With config file -// echo 'service_name: "config-demo" -// database_url: "postgres://localhost:5432/demo" -// api_key: "secret-key-123" -// debug: true' > config.yaml -// go run main.go +// # With config file +// echo 'service_name: "config-demo" +// database_url: "postgres://localhost:5432/demo" +// api_key: "secret-key-123" +// debug: true' > config.yaml +// go run main.go // -// # With environment variables (overrides file) -// DATABASE_URL="postgres://prod:5432/proddb" \ -// API_KEY="prod-secret-456" \ -// DEBUG="false" \ -// go run main.go +// # With environment variables (overrides file) +// DATABASE_URL="postgres://prod:5432/proddb" \ +// API_KEY="prod-secret-456" \ +// DEBUG="false" \ +// go run main.go // -// # Hot reload testing -// # Edit config.yaml while service is running to see hot reload +// # Hot reload testing +// # Edit config.yaml while service is running to see hot reload // // # Test configuration endpoints // -// curl http://localhost:8081/api/config/info -// curl http://localhost:8081/api/config/reload -// curl http://localhost:8081/api/config/sources +// curl http://localhost:8081/api/config/info +// curl http://localhost:8081/api/config/reload +// curl http://localhost:8081/api/config/sources package main import ( @@ -237,9 +237,9 @@ func (s *ConfigService) handleConfigReload(w http.ResponseWriter, r *http.Reques s.config = &newConfig response := map[string]interface{}{ - "reloaded": true, - "timestamp": time.Now().UTC(), - "load_info": result, + "reloaded": true, + "timestamp": time.Now().UTC(), + "load_info": result, } w.Header().Set("Content-Type", "application/json") @@ -384,4 +384,4 @@ func main() { if err := app.Run(context.Background()); err != nil { log.Fatalf("Application failed: %v", err) } -} \ No newline at end of file +} diff --git a/examples/httpclient-service/main.go b/examples/httpclient-service/main.go index e9426f2..7611c9c 100644 --- a/examples/httpclient-service/main.go +++ b/examples/httpclient-service/main.go @@ -15,22 +15,22 @@ // // # Run the service // -// go run main.go +// go run main.go // // # Test the service // -// # Test basic HTTP calls -// curl http://localhost:8081/api/test/get -// curl -X POST http://localhost:8081/api/test/post +// # Test basic HTTP calls +// curl http://localhost:8081/api/test/get +// curl -X POST http://localhost:8081/api/test/post // -// # Test circuit breaker -// curl http://localhost:8081/api/test/unreliable +// # Test circuit breaker +// curl http://localhost:8081/api/test/unreliable // -// # Test with authentication -// curl -H "Authorization: Bearer " http://localhost:8081/api/test/auth +// # Test with authentication +// curl -H "Authorization: Bearer " http://localhost:8081/api/test/auth // -// # Check circuit breaker status -// curl http://localhost:8081/api/circuit-breaker/status +// # Check circuit breaker status +// curl http://localhost:8081/api/circuit-breaker/status package main import ( @@ -92,8 +92,8 @@ func (c *ServiceConfig) Validate() error { // HTTPClientService demonstrates HTTP client functionality. type HTTPClientService struct { - config *ServiceConfig - httpBundle *httpclient.Bundle + config *ServiceConfig + httpBundle *httpclient.Bundle clientTimeout time.Duration } @@ -170,9 +170,9 @@ func (s *HTTPClientService) handleTestGet(w http.ResponseWriter, r *http.Request // Return the response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "method": "GET", - "target": s.config.TargetServiceURL + "/get", + "success": true, + "method": "GET", + "target": s.config.TargetServiceURL + "/get", "response": response, }) } @@ -199,10 +199,10 @@ func (s *HTTPClientService) handleTestPost(w http.ResponseWriter, r *http.Reques // Return the response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "method": "POST", - "target": s.config.TargetServiceURL + "/post", - "sent": user, + "success": true, + "method": "POST", + "target": s.config.TargetServiceURL + "/post", + "sent": user, "response": response, }) } @@ -290,11 +290,11 @@ func (s *HTTPClientService) handleCircuitBreakerStatus(w http.ResponseWriter, r status := map[string]interface{}{ "state": state.String(), "counts": map[string]interface{}{ - "requests": counts.Requests, - "total_successes": counts.TotalSuccesses, - "total_failures": counts.TotalFailures, + "requests": counts.Requests, + "total_successes": counts.TotalSuccesses, + "total_failures": counts.TotalFailures, "consecutive_successes": counts.ConsecutiveSuccesses, - "consecutive_failures": counts.ConsecutiveFailures, + "consecutive_failures": counts.ConsecutiveFailures, }, "timestamp": time.Now().UTC(), } @@ -308,12 +308,12 @@ func (s *HTTPClientService) handleClientStats(w http.ResponseWriter, r *http.Req // This would show connection pool stats, request metrics, etc. // For now, return basic information stats := map[string]interface{}{ - "service": s.config.ServiceName, - "target_url": s.config.TargetServiceURL, - "timeout": s.clientTimeout.String(), - "max_retries": s.config.MaxRetries, - "auth_enabled": s.config.EnableAuth, - "timestamp": time.Now().UTC(), + "service": s.config.ServiceName, + "target_url": s.config.TargetServiceURL, + "timeout": s.clientTimeout.String(), + "max_retries": s.config.MaxRetries, + "auth_enabled": s.config.EnableAuth, + "timestamp": time.Now().UTC(), } w.Header().Set("Content-Type", "application/json") @@ -369,11 +369,11 @@ func main() { BaseURL: cfg.TargetServiceURL, Timeout: timeout, RetryConfig: httpclient.RetryConfig{ - MaxRetries: cfg.MaxRetries, - InitialInterval: 100 * time.Millisecond, - MaxInterval: 5 * time.Second, - Multiplier: 2.0, - RandomizationFactor: 0.1, + MaxRetries: cfg.MaxRetries, + InitialInterval: 100 * time.Millisecond, + MaxInterval: 5 * time.Second, + Multiplier: 2.0, + RandomizationFactor: 0.1, }, CircuitBreakerConfig: httpclient.CircuitBreakerConfig{ Name: cfg.ServiceName + "-circuit-breaker", @@ -387,7 +387,7 @@ func main() { LogRequestBody: true, LogResponseBody: true, MaxLogBodySize: 1024, - UserAgent: fmt.Sprintf("%s/1.0", cfg.ServiceName), + UserAgent: fmt.Sprintf("%s/1.0", cfg.ServiceName), } httpBundle := httpclient.NewBundle(httpConfig) @@ -426,4 +426,4 @@ func main() { if err := app.Run(context.Background()); err != nil { log.Fatalf("Application failed: %v", err) } -} \ No newline at end of file +} diff --git a/examples/jwt-service/main.go b/examples/jwt-service/main.go index e05b773..a2ab92c 100644 --- a/examples/jwt-service/main.go +++ b/examples/jwt-service/main.go @@ -14,20 +14,20 @@ // // # Run the service // -// JWT_SECRET="your-very-secure-secret-key-here-32-bytes-minimum" \ -// JWT_ISSUER="auth-service" \ -// JWT_AUDIENCE="forge-services" \ -// go run main.go +// JWT_SECRET="your-very-secure-secret-key-here-32-bytes-minimum" \ +// JWT_ISSUER="auth-service" \ +// JWT_AUDIENCE="forge-services" \ +// go run main.go // // # Test Authentication // -// # Generate a token (in a real setup, your auth service would do this) -// curl -X POST http://localhost:8081/auth/token \ -// -H "Content-Type: application/json" \ -// -d '{"service_id": "test-client", "permissions": ["read:users"]}' +// # Generate a token (in a real setup, your auth service would do this) +// curl -X POST http://localhost:8081/auth/token \ +// -H "Content-Type: application/json" \ +// -d '{"service_id": "test-client", "permissions": ["read:users"]}' // -// # Use token to access protected endpoint -// curl -H "Authorization: Bearer " http://localhost:8081/api/protected +// # Use token to access protected endpoint +// curl -H "Authorization: Bearer " http://localhost:8081/api/protected package main import ( @@ -161,12 +161,12 @@ func (s *AuthenticatedService) handleProtected(w http.ResponseWriter, r *http.Re } response := map[string]interface{}{ - "message": "This is a protected endpoint", - "service": s.config.ServiceName, + "message": "This is a protected endpoint", + "service": s.config.ServiceName, "authenticated_service": claims.ServiceName, - "service_id": claims.ServiceID, - "permissions": claims.Permissions, - "time": time.Now().UTC(), + "service_id": claims.ServiceID, + "permissions": claims.Permissions, + "time": time.Now().UTC(), } w.Header().Set("Content-Type", "application/json") @@ -182,13 +182,13 @@ func (s *AuthenticatedService) handleAdmin(w http.ResponseWriter, r *http.Reques } response := map[string]interface{}{ - "message": "This is an admin endpoint", - "service": s.config.ServiceName, + "message": "This is an admin endpoint", + "service": s.config.ServiceName, "authenticated_service": claims.ServiceName, - "service_id": claims.ServiceID, - "permissions": claims.Permissions, - "admin_access": jwt.HasPermission(r.Context(), "admin"), - "time": time.Now().UTC(), + "service_id": claims.ServiceID, + "permissions": claims.Permissions, + "admin_access": jwt.HasPermission(r.Context(), "admin"), + "time": time.Now().UTC(), } w.Header().Set("Content-Type", "application/json") @@ -294,4 +294,4 @@ func main() { if err := app.Run(context.Background()); err != nil { log.Fatalf("Application failed: %v", err) } -} \ No newline at end of file +} diff --git a/examples/postgresql-service/main.go b/examples/postgresql-service/main.go index f61bd7c..c494bec 100644 --- a/examples/postgresql-service/main.go +++ b/examples/postgresql-service/main.go @@ -15,13 +15,13 @@ // // # Run the service // -// go run main.go +// go run main.go // // # Environment Configuration // -// DATABASE_URL=postgres://user:pass@localhost:5432/forge_example?sslmode=disable -// SERVICE_NAME=postgresql-service -// APP_ENV=development +// DATABASE_URL=postgres://user:pass@localhost:5432/forge_example?sslmode=disable +// SERVICE_NAME=postgresql-service +// APP_ENV=development package main import ( @@ -42,8 +42,8 @@ type ServiceConfig struct { config.BaseConfig `yaml:",inline"` // PostgreSQL configuration - DatabaseURL string `yaml:"database_url" env:"DATABASE_URL"` - MaxConnections int `yaml:"max_connections" env:"MAX_CONNECTIONS"` + DatabaseURL string `yaml:"database_url" env:"DATABASE_URL"` + MaxConnections int `yaml:"max_connections" env:"MAX_CONNECTIONS"` } // DefaultServiceConfig returns configuration with defaults. @@ -257,4 +257,4 @@ func main() { if err := app.Run(context.Background()); err != nil { log.Fatalf("Application failed: %v", err) } -} \ No newline at end of file +} diff --git a/examples/prometheus-service/main.go b/examples/prometheus-service/main.go index 32c02e6..a74860b 100644 --- a/examples/prometheus-service/main.go +++ b/examples/prometheus-service/main.go @@ -9,26 +9,26 @@ // // # Run the service // -// go run main.go +// go run main.go // // # View metrics // -// # Prometheus metrics endpoint -// curl http://localhost:8081/metrics +// # Prometheus metrics endpoint +// curl http://localhost:8081/metrics // -// # Test endpoints to generate metrics -// curl http://localhost:8081/api/users -// curl -X POST http://localhost:8081/api/users -d '{"name":"John","email":"john@example.com"}' -// curl http://localhost:8081/api/simulate/error -// curl http://localhost:8081/api/simulate/slow +// # Test endpoints to generate metrics +// curl http://localhost:8081/api/users +// curl -X POST http://localhost:8081/api/users -d '{"name":"John","email":"john@example.com"}' +// curl http://localhost:8081/api/simulate/error +// curl http://localhost:8081/api/simulate/slow // -// # Health checks (generates health metrics) -// curl http://localhost:8081/health +// # Health checks (generates health metrics) +// curl http://localhost:8081/health // // # Grafana Dashboard // -// Import the dashboard from bundles/prometheus/grafana-dashboard.json -// Update the namespace variable to match your service name +// Import the dashboard from bundles/prometheus/grafana-dashboard.json +// Update the namespace variable to match your service name package main import ( @@ -82,7 +82,7 @@ func (c *ServiceConfig) Validate() error { // MetricsService demonstrates Prometheus metrics functionality. type MetricsService struct { - config *ServiceConfig + config *ServiceConfig prometheusBundle *forgePrometheus.Bundle // Custom metrics @@ -201,9 +201,9 @@ func (s *MetricsService) setupHTTPEndpoints(mux *http.ServeMux) { // User represents a user for demonstration. type User struct { - ID string `json:"id"` - Name string `json:"name"` - Email string `json:"email"` + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` CreatedAt time.Time `json:"created_at"` } @@ -285,8 +285,8 @@ func (s *MetricsService) handleCreateUser(w http.ResponseWriter, r *http.Request w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "user": user, - "created": true, + "user": user, + "created": true, "timestamp": time.Now().UTC(), }) } @@ -327,9 +327,9 @@ func (s *MetricsService) handleSimulateSlow(w http.ResponseWriter, r *http.Reque w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "message": "Slow operation completed", - "delay": delay.String(), - "duration": duration.String(), + "message": "Slow operation completed", + "delay": delay.String(), + "duration": duration.String(), "timestamp": time.Now().UTC(), }) } @@ -337,9 +337,9 @@ func (s *MetricsService) handleSimulateSlow(w http.ResponseWriter, r *http.Reque // handleMetricsInfo provides information about available metrics. func (s *MetricsService) handleMetricsInfo(w http.ResponseWriter, r *http.Request) { info := map[string]interface{}{ - "service": s.config.ServiceName, - "namespace": s.config.MetricsNamespace, - "subsystem": s.config.MetricsSubsystem, + "service": s.config.ServiceName, + "namespace": s.config.MetricsNamespace, + "subsystem": s.config.MetricsSubsystem, "metrics_endpoint": "/metrics", "available_metrics": []string{ "http_requests_total", @@ -360,7 +360,7 @@ func (s *MetricsService) handleMetricsInfo(w http.ResponseWriter, r *http.Reques "cache_hit_ratio", }, "grafana_dashboard": "/bundles/prometheus/grafana-dashboard.json", - "timestamp": time.Now().UTC(), + "timestamp": time.Now().UTC(), } w.Header().Set("Content-Type", "application/json") @@ -393,8 +393,8 @@ func (s *MetricsService) handleUpdateMetrics(w http.ResponseWriter, r *http.Requ w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "updated": true, - "message": "Metrics updated with random values", + "updated": true, + "message": "Metrics updated with random values", "timestamp": time.Now().UTC(), }) } @@ -532,4 +532,4 @@ func main() { if err := app.Run(context.Background()); err != nil { log.Fatalf("Application failed: %v", err) } -} \ No newline at end of file +} diff --git a/examples/redis-service/main.go b/examples/redis-service/main.go index 085fa6c..8d793ad 100644 --- a/examples/redis-service/main.go +++ b/examples/redis-service/main.go @@ -15,24 +15,24 @@ // // # Run the service // -// REDIS_URL="redis://localhost:6379/0" go run main.go +// REDIS_URL="redis://localhost:6379/0" go run main.go // // # Test the service // -// # Test caching -// curl -X POST http://localhost:8081/api/cache/user/123 \ -// -H "Content-Type: application/json" \ -// -d '{"name": "John Doe", "email": "john@example.com"}' +// # Test caching +// curl -X POST http://localhost:8081/api/cache/user/123 \ +// -H "Content-Type: application/json" \ +// -d '{"name": "John Doe", "email": "john@example.com"}' // -// curl http://localhost:8081/api/cache/user/123 +// curl http://localhost:8081/api/cache/user/123 // -// # Test pub/sub -// curl -X POST http://localhost:8081/api/events/user.created \ -// -H "Content-Type: application/json" \ -// -d '{"user_id": "123", "event": "user_created"}' +// # Test pub/sub +// curl -X POST http://localhost:8081/api/events/user.created \ +// -H "Content-Type: application/json" \ +// -d '{"user_id": "123", "event": "user_created"}' // -// # Test rate limiting -// for i in {1..10}; do curl http://localhost:8081/api/limited; done +// # Test rate limiting +// for i in {1..10}; do curl http://localhost:8081/api/limited; done package main import ( @@ -367,12 +367,12 @@ func (s *CacheService) handleRedisStats(w http.ResponseWriter, r *http.Request) response := map[string]interface{}{ "redis_info": strings.Split(info, "\r\n"), "pool_stats": map[string]interface{}{ - "hits": stats.Hits, - "misses": stats.Misses, - "timeouts": stats.Timeouts, - "total_conns": stats.TotalConns, - "idle_conns": stats.IdleConns, - "stale_conns": stats.StaleConns, + "hits": stats.Hits, + "misses": stats.Misses, + "timeouts": stats.Timeouts, + "total_conns": stats.TotalConns, + "idle_conns": stats.IdleConns, + "stale_conns": stats.StaleConns, }, "timestamp": time.Now().UTC(), } @@ -521,4 +521,4 @@ func main() { if err := app.Run(context.Background()); err != nil { log.Fatalf("Application failed: %v", err) } -} \ No newline at end of file +} diff --git a/examples/simple-service/main.go b/examples/simple-service/main.go index e5a922a..875eaed 100644 --- a/examples/simple-service/main.go +++ b/examples/simple-service/main.go @@ -118,4 +118,4 @@ func main() { if err := app.Run(context.Background()); err != nil { log.Fatalf("Application failed: %v", err) } -} \ No newline at end of file +} diff --git a/framework/app_test.go b/framework/app_test.go index 8e33144..c09503c 100644 --- a/framework/app_test.go +++ b/framework/app_test.go @@ -16,10 +16,10 @@ import ( // TestComponent is a test implementation of the Component interface type TestComponent struct { - started bool - stopped bool + started bool + stopped bool startError error - stopError error + stopError error } func (c *TestComponent) Start(ctx context.Context) error { @@ -40,11 +40,11 @@ func (c *TestComponent) HealthChecks() []forgeHealth.Check { // TestBundle is a test implementation of the Bundle interface type TestBundle struct { - name string - initError error + name string + initError error initialized bool - stopped bool - stopError error + stopped bool + stopError error } func (b *TestBundle) Name() string { @@ -226,7 +226,7 @@ func TestApp_Start_BundleInitializationError(t *testing.T) { cfg.ServiceName = "test-service" bundle := &TestBundle{ - name: "failing-bundle", + name: "failing-bundle", initError: fmt.Errorf("initialization failed"), } @@ -730,4 +730,4 @@ func TestApp_WithStreamInterceptor_Valid(t *testing.T) { if len(app.streamInterceptors) != 1 { t.Errorf("expected 1 stream interceptor, got %d", len(app.streamInterceptors)) } -} \ No newline at end of file +} diff --git a/framework/http.go b/framework/http.go index e52c6e5..a7f5d1d 100644 --- a/framework/http.go +++ b/framework/http.go @@ -34,9 +34,9 @@ type HTTPServerConfig struct { CORSCredentials bool `yaml:"cors_credentials" env:"CORS_CREDENTIALS"` // Endpoint configuration - EnableMetrics bool `yaml:"enable_metrics" env:"ENABLE_METRICS"` - MetricsPath string `yaml:"metrics_path" env:"METRICS_PATH"` - HealthPathPrefix string `yaml:"health_path_prefix" env:"HEALTH_PATH_PREFIX"` + EnableMetrics bool `yaml:"enable_metrics" env:"ENABLE_METRICS"` + MetricsPath string `yaml:"metrics_path" env:"METRICS_PATH"` + HealthPathPrefix string `yaml:"health_path_prefix" env:"HEALTH_PATH_PREFIX"` // Request logging EnableRequestLogging bool `yaml:"enable_request_logging" env:"ENABLE_REQUEST_LOGGING"` @@ -106,11 +106,11 @@ func (c *HTTPServerConfig) Validate() error { // HTTPServerBuilder builds an enhanced HTTP server with configurable endpoints. type HTTPServerBuilder struct { - config HTTPServerConfig - mux *http.ServeMux - app *App - logger zerolog.Logger - registry prometheus.Registerer + config HTTPServerConfig + mux *http.ServeMux + app *App + logger zerolog.Logger + registry prometheus.Registerer } // NewHTTPServerBuilder creates a new HTTP server builder. @@ -201,8 +201,8 @@ func (b *HTTPServerBuilder) registerMetricsEndpoint() { prometheus.DefaultGatherer, promhttp.HandlerOpts{ EnableOpenMetrics: true, - Timeout: 30 * time.Second, - ErrorLog: &promLogAdapter{logger: b.logger}, + Timeout: 30 * time.Second, + ErrorLog: &promLogAdapter{logger: b.logger}, }, ) @@ -409,4 +409,4 @@ type promLogAdapter struct { // Println implements the log.Logger interface for Prometheus error logging. func (p *promLogAdapter) Println(v ...interface{}) { p.logger.Error().Interface("prometheus_error", v).Msg("Prometheus metrics error") -} \ No newline at end of file +} diff --git a/framework/logging.go b/framework/logging.go index d9d7f69..b05bfca 100644 --- a/framework/logging.go +++ b/framework/logging.go @@ -120,4 +120,4 @@ func (h *healthLoggerAdapter) addFields(event *zerolog.Event, fields []interface } event.Interface(key, fields[i+1]) } -} \ No newline at end of file +} diff --git a/framework/observability.go b/framework/observability.go index e6b7b01..5562f5b 100644 --- a/framework/observability.go +++ b/framework/observability.go @@ -6,16 +6,16 @@ import ( "strings" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" otelprometheus "go.opentelemetry.io/otel/exporters/prometheus" otelmetric "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" - oteltrace "go.opentelemetry.io/otel/trace" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + oteltrace "go.opentelemetry.io/otel/trace" "github.com/datariot/forge/config" ) @@ -42,10 +42,10 @@ func NewObservabilityConfig(baseConfig *config.BaseConfig, version string) *Obse // ObservabilityManager manages OpenTelemetry tracing and metrics. type ObservabilityManager struct { - config *ObservabilityConfig - traceProvider *trace.TracerProvider - meterProvider *sdkmetric.MeterProvider - initialized bool + config *ObservabilityConfig + traceProvider *trace.TracerProvider + meterProvider *sdkmetric.MeterProvider + initialized bool } // NewObservabilityManager creates a new observability manager. diff --git a/framework/shutdown.go b/framework/shutdown.go index 6d9692a..1020d38 100644 --- a/framework/shutdown.go +++ b/framework/shutdown.go @@ -114,7 +114,6 @@ func (so *ShutdownOrchestrator) executeHook(ctx context.Context, hook Orchestrat } } - // ComponentShutdownHook creates a shutdown hook for a Component. func ComponentShutdownHook(name string, component Component) OrchestrationHook { return OrchestrationHook{ @@ -123,4 +122,4 @@ func ComponentShutdownHook(name string, component Component) OrchestrationHook { return component.Stop(ctx) }, } -} \ No newline at end of file +} diff --git a/health/check.go b/health/check.go index 9f3e1d8..d82d9e4 100644 --- a/health/check.go +++ b/health/check.go @@ -291,4 +291,4 @@ func (c *AlwaysUnhealthyCheck) Liveness(ctx context.Context) error { // Readiness always returns an error (unhealthy). func (c *AlwaysUnhealthyCheck) Readiness(ctx context.Context) error { return c.err -} \ No newline at end of file +} diff --git a/health/registry.go b/health/registry.go index e547908..ec85620 100644 --- a/health/registry.go +++ b/health/registry.go @@ -275,4 +275,4 @@ func (r *Registry) GetCheckConfig(name string) (CheckConfig, bool) { config, exists := r.configs[name] return config, exists -} \ No newline at end of file +} diff --git a/health/registry_test.go b/health/registry_test.go index aba29ab..d88aeec 100644 --- a/health/registry_test.go +++ b/health/registry_test.go @@ -118,9 +118,9 @@ func TestRegistry_CheckLiveness_FailingCheck(t *testing.T) { registry := NewRegistry(nil) failingCheck := &testCheck{ - name: "failing-check", - livenessErr: errors.New("check failed"), - readinessErr: errors.New("check failed"), + name: "failing-check", + livenessErr: errors.New("check failed"), + readinessErr: errors.New("check failed"), } config := DefaultCheckConfig("failing-check") config.Required = true @@ -233,7 +233,7 @@ func TestRegistry_ConcurrentChecks(t *testing.T) { // Add multiple checks with delays for i := 0; i < 5; i++ { check := &testCheck{ - name: "slow-check-" + string(rune('A'+i)), + name: "slow-check-" + string(rune('A'+i)), delay: 100 * time.Millisecond, } registry.Register(check, DefaultCheckConfig(check.name)) diff --git a/health/status.go b/health/status.go index 838fd13..58f5285 100644 --- a/health/status.go +++ b/health/status.go @@ -198,4 +198,4 @@ func (s StatusResponse) HTTPStatus() int { default: return 500 } -} \ No newline at end of file +} diff --git a/testutil/testutil.go b/testutil/testutil.go index d4973da..ff13439 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -145,11 +145,11 @@ func (c *TestComponent) HealthChecks() []forgeHealth.Check { // TestBundle provides a mock Bundle implementation for testing. type TestBundle struct { - BundleName string - InitCalled bool - InitError error - StopCalled bool - StopError error + BundleName string + InitCalled bool + InitError error + StopCalled bool + StopError error } // NewTestBundle creates a new test bundle. @@ -321,4 +321,4 @@ func SetupTestRedis(t *testing.T) redis.UniversalClient { // This would connect to a test Redis instance // Implementation depends on testing infrastructure return nil -} \ No newline at end of file +} From 29cc7cdda56a10e63453a73dcfb02aebf2202346 Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 08:33:07 -0600 Subject: [PATCH 2/8] fix: go mod tidy all example modules so they build against current framework Co-Authored-By: Claude Fable 5 --- examples/config-service/go.mod | 6 ++- examples/config-service/go.sum | 12 +++++- examples/httpclient-service/go.mod | 6 ++- examples/httpclient-service/go.sum | 12 +++++- examples/jwt-service/go.mod | 6 ++- examples/jwt-service/go.sum | 12 +++++- examples/postgresql-service/go.mod | 9 ++-- examples/postgresql-service/go.sum | 66 ++++++++++-------------------- examples/prometheus-service/go.mod | 6 ++- examples/prometheus-service/go.sum | 12 +++++- examples/redis-service/go.mod | 6 ++- examples/redis-service/go.sum | 12 +++++- examples/simple-service/go.mod | 6 ++- examples/simple-service/go.sum | 12 +++++- 14 files changed, 117 insertions(+), 66 deletions(-) diff --git a/examples/config-service/go.mod b/examples/config-service/go.mod index ef8c144..450766e 100644 --- a/examples/config-service/go.mod +++ b/examples/config-service/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -21,12 +22,15 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rs/zerolog v1.34.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect diff --git a/examples/config-service/go.sum b/examples/config-service/go.sum index 39b9f63..2d87ce3 100644 --- a/examples/config-service/go.sum +++ b/examples/config-service/go.sum @@ -21,6 +21,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -47,8 +49,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= @@ -60,10 +64,14 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= diff --git a/examples/httpclient-service/go.mod b/examples/httpclient-service/go.mod index 176f466..fe8f77b 100644 --- a/examples/httpclient-service/go.mod +++ b/examples/httpclient-service/go.mod @@ -16,6 +16,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -23,12 +24,15 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rs/zerolog v1.34.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect diff --git a/examples/httpclient-service/go.sum b/examples/httpclient-service/go.sum index a93a4d1..04c466a 100644 --- a/examples/httpclient-service/go.sum +++ b/examples/httpclient-service/go.sum @@ -22,6 +22,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -48,8 +50,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= @@ -65,10 +69,14 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= diff --git a/examples/jwt-service/go.mod b/examples/jwt-service/go.mod index 6120d6d..414219c 100644 --- a/examples/jwt-service/go.mod +++ b/examples/jwt-service/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -21,12 +22,15 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rs/zerolog v1.34.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect diff --git a/examples/jwt-service/go.sum b/examples/jwt-service/go.sum index bc52d2f..3849009 100644 --- a/examples/jwt-service/go.sum +++ b/examples/jwt-service/go.sum @@ -21,6 +21,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -47,8 +49,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= @@ -60,10 +64,14 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= diff --git a/examples/postgresql-service/go.mod b/examples/postgresql-service/go.mod index e40ead2..afd87ff 100644 --- a/examples/postgresql-service/go.mod +++ b/examples/postgresql-service/go.mod @@ -12,11 +12,9 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang-migrate/migrate/v4 v4.19.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -24,12 +22,15 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rs/zerolog v1.34.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect diff --git a/examples/postgresql-service/go.sum b/examples/postgresql-service/go.sum index e611f9b..162e999 100644 --- a/examples/postgresql-service/go.sum +++ b/examples/postgresql-service/go.sum @@ -1,55 +1,36 @@ -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= -github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= -github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -57,19 +38,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -79,8 +49,12 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= @@ -88,14 +62,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= @@ -130,5 +106,7 @@ google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/prometheus-service/go.mod b/examples/prometheus-service/go.mod index 136a3b9..669cdce 100644 --- a/examples/prometheus-service/go.mod +++ b/examples/prometheus-service/go.mod @@ -16,18 +16,22 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rs/zerolog v1.34.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect diff --git a/examples/prometheus-service/go.sum b/examples/prometheus-service/go.sum index 6a6fca6..42e8e23 100644 --- a/examples/prometheus-service/go.sum +++ b/examples/prometheus-service/go.sum @@ -19,6 +19,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -45,8 +47,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= @@ -58,10 +62,14 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= diff --git a/examples/redis-service/go.mod b/examples/redis-service/go.mod index a7e095e..abcff88 100644 --- a/examples/redis-service/go.mod +++ b/examples/redis-service/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -21,13 +22,16 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/redis/go-redis/v9 v9.14.0 // indirect github.com/rs/zerolog v1.34.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect diff --git a/examples/redis-service/go.sum b/examples/redis-service/go.sum index 3821069..c512da0 100644 --- a/examples/redis-service/go.sum +++ b/examples/redis-service/go.sum @@ -25,6 +25,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -51,8 +53,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -66,10 +70,14 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= diff --git a/examples/simple-service/go.mod b/examples/simple-service/go.mod index 5c24289..4ff8200 100644 --- a/examples/simple-service/go.mod +++ b/examples/simple-service/go.mod @@ -13,6 +13,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -20,12 +21,15 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rs/zerolog v1.34.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect diff --git a/examples/simple-service/go.sum b/examples/simple-service/go.sum index 6a6fca6..42e8e23 100644 --- a/examples/simple-service/go.sum +++ b/examples/simple-service/go.sum @@ -19,6 +19,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -45,8 +47,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= @@ -58,10 +62,14 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= From 28c45d5d8822ec21ed9f19e8ec62d3a261b215a0 Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 08:37:19 -0600 Subject: [PATCH 3/8] fix: framework and config review findings - shutdown: stop executing hooks after timeout (break-in-select bug), aggregate with errors.Join - observability: return all shutdown errors via errors.Join instead of first only - app: apply configured gRPC max msg sizes and connection timeout; bind HTTP listener synchronously so port conflicts fail Start - http: Build returns error on invalid config; WithHTTPServerConfig option makes HTTPServerConfig reachable; drop unimplemented LogRequest/ResponseBody fields; precompute CORS header joins - config: default OTELEndpoint to empty so default services don't dial a dead collector Co-Authored-By: Claude Fable 5 --- config/base.go | 2 +- framework/app.go | 70 ++++++++++++++++++++++++++++---------- framework/http.go | 32 +++++++---------- framework/observability.go | 5 +-- framework/shutdown.go | 33 ++++++++++++------ 5 files changed, 91 insertions(+), 51 deletions(-) diff --git a/config/base.go b/config/base.go index c35ee83..fb56eaa 100644 --- a/config/base.go +++ b/config/base.go @@ -151,7 +151,7 @@ func DefaultBaseConfig() BaseConfig { LogLevel: "info", ShutdownTimeout: 30 * time.Second, ReadinessInitialDelay: 0, - OTELEndpoint: "http://localhost:4317", + OTELEndpoint: "", OTELSampleRate: 1.0, GRPCMaxRecvMsgSize: 4 * 1024 * 1024, // 4MB GRPCMaxSendMsgSize: 4 * 1024 * 1024, // 4MB diff --git a/framework/app.go b/framework/app.go index 42ffdc3..8994120 100644 --- a/framework/app.go +++ b/framework/app.go @@ -52,6 +52,7 @@ package framework import ( "context" + "errors" "fmt" "net" "net/http" @@ -232,9 +233,10 @@ type App struct { grpcHealthServer *health.Server // Servers - grpcServer *grpc.Server - httpServer *http.Server - grpcListener net.Listener + grpcServer *grpc.Server + httpServer *http.Server + grpcListener net.Listener + httpServerConfig *HTTPServerConfig // Components and bundles components []Component @@ -408,6 +410,18 @@ func WithShutdownHook(hook ShutdownHook) AppOption { } } +// WithHTTPServerConfig sets a custom HTTP server configuration, overriding the +// framework's default endpoint, CORS, and request logging settings. +func WithHTTPServerConfig(cfg HTTPServerConfig) AppOption { + return func(app *App) error { + if err := cfg.Validate(); err != nil { + return fmt.Errorf("invalid HTTP server config: %w", err) + } + app.httpServerConfig = &cfg + return nil + } +} + // WithUnaryInterceptor adds a gRPC unary server interceptor. func WithUnaryInterceptor(interceptor grpc.UnaryServerInterceptor) AppOption { return func(app *App) error { @@ -708,6 +722,17 @@ func (a *App) startGRPCServer(ctx context.Context) error { opts = append(opts, grpc.ChainStreamInterceptor(a.streamInterceptors...)) } + // Apply configured message size and connection timeout limits + if a.config.GRPCMaxRecvMsgSize > 0 { + opts = append(opts, grpc.MaxRecvMsgSize(a.config.GRPCMaxRecvMsgSize)) + } + if a.config.GRPCMaxSendMsgSize > 0 { + opts = append(opts, grpc.MaxSendMsgSize(a.config.GRPCMaxSendMsgSize)) + } + if a.config.GRPCTimeout > 0 { + opts = append(opts, grpc.ConnectionTimeout(a.config.GRPCTimeout)) + } + // Create gRPC server with interceptors a.grpcServer = grpc.NewServer(opts...) @@ -749,19 +774,17 @@ func (a *App) startGRPCServer(ctx context.Context) error { // startHTTPServer creates and starts the enhanced HTTP server. func (a *App) startHTTPServer(ctx context.Context) error { - // Create enhanced HTTP server configuration - httpConfig := HTTPServerConfig{ - EnableCORS: a.config.EnableCORS, - CORSOrigins: a.config.CORSOrigins, - CORSMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - CORSHeaders: []string{"Content-Type", "Authorization"}, - CORSCredentials: false, - EnableMetrics: a.config.EnableMetrics, - MetricsPath: "/metrics", - HealthPathPrefix: "/health", - EnableRequestLogging: a.config.EnableRequestLogging, - LogRequestBody: false, - LogResponseBody: false, + // Use the user-supplied HTTP server configuration if provided, otherwise + // fall back to defaults driven by the base config's toggles. + var httpConfig HTTPServerConfig + if a.httpServerConfig != nil { + httpConfig = *a.httpServerConfig + } else { + httpConfig = DefaultHTTPServerConfig() + httpConfig.EnableCORS = a.config.EnableCORS + httpConfig.CORSOrigins = a.config.CORSOrigins + httpConfig.EnableMetrics = a.config.EnableMetrics + httpConfig.EnableRequestLogging = a.config.EnableRequestLogging } // Build enhanced HTTP server @@ -774,14 +797,25 @@ func (a *App) startHTTPServer(ctx context.Context) error { } } - a.httpServer = builder.Build() + httpServer, err := builder.Build() + if err != nil { + return fmt.Errorf("failed to build HTTP server: %w", err) + } + a.httpServer = httpServer + + // Create listener synchronously so bind failures (e.g. port already in use) + // are reported to the caller instead of only being logged from the goroutine. + listener, err := net.Listen("tcp", a.config.HTTPAddr) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", a.config.HTTPAddr, err) + } // Start server in goroutine go func() { logger := a.logging.WithService(a.config.ServiceName, "http") logger.Info().Str("addr", a.config.HTTPAddr).Msg("Starting HTTP server") - if err := a.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := a.httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error().Err(err).Msg("HTTP server error") } }() diff --git a/framework/http.go b/framework/http.go index a7f5d1d..afb1b2a 100644 --- a/framework/http.go +++ b/framework/http.go @@ -40,8 +40,6 @@ type HTTPServerConfig struct { // Request logging EnableRequestLogging bool `yaml:"enable_request_logging" env:"ENABLE_REQUEST_LOGGING"` - LogRequestBody bool `yaml:"log_request_body" env:"LOG_REQUEST_BODY"` - LogResponseBody bool `yaml:"log_response_body" env:"LOG_RESPONSE_BODY"` } // DefaultHTTPServerConfig returns HTTP server configuration with sensible defaults. @@ -56,8 +54,6 @@ func DefaultHTTPServerConfig() HTTPServerConfig { MetricsPath: "/metrics", HealthPathPrefix: "/health", EnableRequestLogging: true, - LogRequestBody: false, - LogResponseBody: false, } } @@ -130,11 +126,10 @@ func (b *HTTPServerBuilder) RegisterCustomRoutes(registerFunc func(*http.ServeMu } // Build constructs the HTTP server with all configured endpoints and middleware. -func (b *HTTPServerBuilder) Build() *http.Server { +func (b *HTTPServerBuilder) Build() (*http.Server, error) { // Validate configuration before building if err := b.config.Validate(); err != nil { - b.logger.Error().Err(err).Msg("HTTP server configuration validation failed") - // Log error but continue with safe defaults rather than panicking + return nil, fmt.Errorf("HTTP server configuration validation failed: %w", err) } // Apply middleware stack @@ -146,7 +141,7 @@ func (b *HTTPServerBuilder) Build() *http.Server { ReadTimeout: b.app.config.HTTPReadTimeout, WriteTimeout: b.app.config.HTTPWriteTimeout, IdleTimeout: b.app.config.HTTPIdleTimeout, - } + }, nil } // buildHandler creates the complete HTTP handler with all endpoints and middleware. @@ -325,19 +320,16 @@ func (b *HTTPServerBuilder) logHTTPRequest(r *http.Request, wrapper *responseWri // corsMiddleware adds CORS headers based on configuration. func (b *HTTPServerBuilder) corsMiddleware(next http.Handler) http.Handler { + // Wildcard+credentials safety is enforced by config Validate() at startup, + // so it is not re-checked on every request. Header values that don't vary + // per-request are joined once here rather than on every call. + hasWildcard := len(b.config.CORSOrigins) == 1 && b.config.CORSOrigins[0] == "*" + allowMethods := strings.Join(b.config.CORSMethods, ", ") + allowHeaders := strings.Join(b.config.CORSHeaders, ", ") + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") - // SECURITY: Validate CORS configuration safety - hasWildcard := len(b.config.CORSOrigins) == 1 && b.config.CORSOrigins[0] == "*" - if hasWildcard && b.config.CORSCredentials { - b.logger.Error(). - Str("origin", origin). - Msg("SECURITY ERROR: Cannot use wildcard CORS with credentials - potential security vulnerability") - http.Error(w, "CORS configuration error", http.StatusInternalServerError) - return - } - // Set CORS headers safely if b.isAllowedOrigin(origin) { if hasWildcard && !b.config.CORSCredentials { @@ -349,8 +341,8 @@ func (b *HTTPServerBuilder) corsMiddleware(next http.Handler) http.Handler { } } - w.Header().Set("Access-Control-Allow-Methods", strings.Join(b.config.CORSMethods, ", ")) - w.Header().Set("Access-Control-Allow-Headers", strings.Join(b.config.CORSHeaders, ", ")) + w.Header().Set("Access-Control-Allow-Methods", allowMethods) + w.Header().Set("Access-Control-Allow-Headers", allowHeaders) if b.config.CORSCredentials { w.Header().Set("Access-Control-Allow-Credentials", "true") diff --git a/framework/observability.go b/framework/observability.go index 5562f5b..aeba689 100644 --- a/framework/observability.go +++ b/framework/observability.go @@ -2,6 +2,7 @@ package framework import ( "context" + "errors" "fmt" "strings" @@ -227,9 +228,9 @@ func (om *ObservabilityManager) Shutdown(ctx context.Context) error { } } - // Return first error if any occurred + // Return all errors if any occurred if len(shutdownErrors) > 0 { - return shutdownErrors[0] + return errors.Join(shutdownErrors...) } return nil diff --git a/framework/shutdown.go b/framework/shutdown.go index 1020d38..00bbf30 100644 --- a/framework/shutdown.go +++ b/framework/shutdown.go @@ -2,6 +2,7 @@ package framework import ( "context" + "errors" "fmt" "sync" "time" @@ -68,27 +69,39 @@ func (so *ShutdownOrchestrator) Shutdown(ctx context.Context) error { timeoutCtx, cancel := context.WithTimeout(ctx, so.timeout) defer cancel() - var errors []error + var errs []error + var skipped []string + timedOut := false // Execute hooks sequentially to maintain order for i := len(hooks) - 1; i >= 0; i-- { // Reverse order for proper cleanup hook := hooks[i] - select { - case <-timeoutCtx.Done(): - errors = append(errors, fmt.Errorf("shutdown timeout exceeded before executing hook '%s'", hook.Name)) - break - default: - // Continue with hook execution + if !timedOut { + select { + case <-timeoutCtx.Done(): + timedOut = true + default: + // Continue with hook execution + } + } + + if timedOut { + skipped = append(skipped, hook.Name) + continue } if err := so.executeHook(timeoutCtx, hook); err != nil { - errors = append(errors, fmt.Errorf("hook '%s' failed: %w", hook.Name, err)) + errs = append(errs, fmt.Errorf("hook '%s' failed: %w", hook.Name, err)) } } - if len(errors) > 0 { - return fmt.Errorf("shutdown completed with %d errors: %v", len(errors), errors) + if len(skipped) > 0 { + errs = append(errs, fmt.Errorf("shutdown timeout exceeded before executing hooks: %v", skipped)) + } + + if len(errs) > 0 { + return errors.Join(errs...) } return nil From 0ebb5fe5c5c023393b1bc8ce0bf7e8b2df1cb5ee Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 08:42:56 -0600 Subject: [PATCH 4/8] fix: bundles error handling and redis readiness review findings - httpclient: preserve root cause in retry exhaustion (%w: %w), surface context cancellation distinctly, errors.Is for gobreaker sentinel, errors.As for net.Error, errors.New for constant sentinels - redis: exported ErrCacheMiss sentinel with errors.Is(redis.Nil) detection; readiness check reduced from 5 round-trips (incl. writes) to a single PING - jwt/configloader/prometheus: constant-string fmt.Errorf -> errors.New sweep Co-Authored-By: Claude Fable 5 --- bundles/configloader/bundle.go | 17 ++--- bundles/httpclient/bundle.go | 35 ++++++---- bundles/httpclient/bundle_test.go | 102 ++++++++++++++++++++++++++++++ bundles/jwt/bundle.go | 33 +++++----- bundles/prometheus/bundle.go | 16 ++--- bundles/redis/bundle.go | 53 ++++------------ bundles/redis/bundle_test.go | 81 ++++++++++++++++++++++++ 7 files changed, 251 insertions(+), 86 deletions(-) diff --git a/bundles/configloader/bundle.go b/bundles/configloader/bundle.go index 3bb74d0..11ca332 100644 --- a/bundles/configloader/bundle.go +++ b/bundles/configloader/bundle.go @@ -83,6 +83,7 @@ package configloader import ( "context" "encoding/json" + stderrors "errors" "fmt" "io" "os" @@ -152,13 +153,13 @@ func DefaultConfig() Config { // Validate validates the configuration loader settings. func (c *Config) Validate() error { if len(c.ConfigPaths) == 0 { - return fmt.Errorf("at least one config path must be specified") + return stderrors.New("at least one config path must be specified") } // Validate config paths for _, path := range c.ConfigPaths { if path == "" { - return fmt.Errorf("config path cannot be empty") + return stderrors.New("config path cannot be empty") } if !filepath.IsAbs(path) && !strings.HasPrefix(path, "./") { return fmt.Errorf("config path must be absolute or relative (starting with ./): %s", path) @@ -296,12 +297,12 @@ type LoadResult struct { // Load loads configuration into the provided struct from multiple sources. func (l *Loader) Load(dest interface{}) (*LoadResult, error) { if dest == nil { - return nil, fmt.Errorf("destination cannot be nil") + return nil, stderrors.New("destination cannot be nil") } destValue := reflect.ValueOf(dest) if destValue.Kind() != reflect.Ptr || destValue.Elem().Kind() != reflect.Struct { - return nil, fmt.Errorf("destination must be a pointer to a struct") + return nil, stderrors.New("destination must be a pointer to a struct") } result := &LoadResult{ @@ -570,7 +571,7 @@ func (l *Loader) setFieldValue(field reflect.Value, value string) error { field.Set(reflect.ValueOf(values)) } } else { - return fmt.Errorf("unsupported slice type for field") + return stderrors.New("unsupported slice type for field") } default: return fmt.Errorf("unsupported field type: %s", field.Kind()) @@ -688,7 +689,7 @@ func (l *Loader) OnConfigChange(callback func(interface{})) { // StartWatching starts watching configuration files for changes. func (b *Bundle) StartWatching(ctx context.Context, dest interface{}) error { if b.watcher == nil { - return fmt.Errorf("file watcher not initialized") + return stderrors.New("file watcher not initialized") } go func() { @@ -780,14 +781,14 @@ func (l *Loader) Reload(dest interface{}) (*LoadResult, error) { // Paths containing ".." traversal sequences are rejected regardless of resolution outcome. func (l *Loader) validateFilePath(filename string) error { if filename == "" { - return fmt.Errorf("configuration file path cannot be empty") + return stderrors.New("configuration file path cannot be empty") } // Reject inputs that contain path traversal sequences before any resolution. // filepath.Clean / filepath.Abs would silently resolve "../../../etc/passwd" to // a valid absolute path, making it look legitimate. We block such inputs explicitly. if strings.Contains(filepath.Clean(filename), "..") { - return fmt.Errorf("path traversal not allowed in configuration file path") + return stderrors.New("path traversal not allowed in configuration file path") } // Resolve relative paths to absolute diff --git a/bundles/httpclient/bundle.go b/bundles/httpclient/bundle.go index ec93d27..8cb8410 100644 --- a/bundles/httpclient/bundle.go +++ b/bundles/httpclient/bundle.go @@ -69,6 +69,7 @@ import ( "context" "crypto/tls" "encoding/json" + stderrors "errors" "fmt" "io" "net" @@ -112,7 +113,7 @@ func NewStaticCredentialProvider(apiKey, jwtToken string) *StaticCredentialProvi // GetAPIKey returns the static API key. func (p *StaticCredentialProvider) GetAPIKey(ctx context.Context) (string, error) { if p.apiKey == "" { - return "", fmt.Errorf("no API key configured") + return "", stderrors.New("no API key configured") } return p.apiKey, nil } @@ -120,7 +121,7 @@ func (p *StaticCredentialProvider) GetAPIKey(ctx context.Context) (string, error // GetJWTToken returns the static JWT token. func (p *StaticCredentialProvider) GetJWTToken(ctx context.Context) (string, error) { if p.jwtToken == "" { - return "", fmt.Errorf("no JWT token configured") + return "", stderrors.New("no JWT token configured") } return p.jwtToken, nil } @@ -264,7 +265,7 @@ func (c *Config) Validate() error { // Validate circuit breaker configuration if c.CircuitBreakerConfig.MaxRequests == 0 { - return fmt.Errorf("circuit breaker max_requests must be positive") + return stderrors.New("circuit breaker max_requests must be positive") } if c.CircuitBreakerConfig.Interval <= 0 { return fmt.Errorf("circuit breaker interval must be positive, got %v", c.CircuitBreakerConfig.Interval) @@ -473,8 +474,8 @@ func (e *HTTPError) IsRetryableError() bool { // Common errors var ( - ErrCircuitBreakerOpen = fmt.Errorf("circuit breaker is open") - ErrMaxRetriesExceeded = fmt.Errorf("maximum retries exceeded") + ErrCircuitBreakerOpen = stderrors.New("circuit breaker is open") + ErrMaxRetriesExceeded = stderrors.New("maximum retries exceeded") ) // Get performs a GET request and unmarshals the response into dest. @@ -512,7 +513,7 @@ func (c *Client) request(ctx context.Context, method, path string, body interfac if err != nil { // Check if circuit breaker is open - if err == gobreaker.ErrOpenState { + if stderrors.Is(err, gobreaker.ErrOpenState) { return ErrCircuitBreakerOpen } return err @@ -531,9 +532,14 @@ func (c *Client) executeWithRetry(ctx context.Context, method, url string, body b := backoff.WithContext(c.backoffFactory(), ctx) // Retry with backoff - err := backoff.Retry(operation, b) - if err != nil { - return ErrMaxRetriesExceeded + lastErr := backoff.Retry(operation, b) + if lastErr != nil { + // Distinguish context cancellation/deadline from genuine retry exhaustion + // so callers can detect cancellation instead of misreading it as exhaustion. + if stderrors.Is(lastErr, context.Canceled) || stderrors.Is(lastErr, context.DeadlineExceeded) { + return fmt.Errorf("request context ended: %w", lastErr) + } + return fmt.Errorf("%w: %w", ErrMaxRetriesExceeded, lastErr) } return nil @@ -591,7 +597,8 @@ func (c *Client) executeRequest(ctx context.Context, method, url string, body in } // Check if error is retryable - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + var netErr net.Error + if stderrors.As(err, &netErr) && netErr.Timeout() { return err // Retryable timeout error } return backoff.Permanent(err) // Non-retryable error @@ -778,13 +785,13 @@ func (c *Client) addAuthHeaders(ctx context.Context, req *http.Request) error { // validateJWTToken performs basic JWT token validation. func (c *Client) validateJWTToken(token string) error { if token == "" { - return fmt.Errorf("token cannot be empty") + return stderrors.New("token cannot be empty") } // Basic JWT format validation (header.payload.signature) parts := strings.Split(token, ".") if len(parts) != 3 { - return fmt.Errorf("invalid JWT token format") + return stderrors.New("invalid JWT token format") } // Additional validation would be performed by JWT bundle @@ -843,7 +850,7 @@ func (c *Client) RawRequest(ctx context.Context, method, url string, headers map }) if err != nil { - if err == gobreaker.ErrOpenState { + if stderrors.Is(err, gobreaker.ErrOpenState) { return nil, ErrCircuitBreakerOpen } return nil, err @@ -855,7 +862,7 @@ func (c *Client) RawRequest(ctx context.Context, method, url string, headers map // HealthCheck performs health checks for the HTTP client. func (c *Client) HealthCheck(ctx context.Context, healthURL string) error { if healthURL == "" { - return fmt.Errorf("health check URL not configured") + return stderrors.New("health check URL not configured") } // Perform a simple GET request to health endpoint diff --git a/bundles/httpclient/bundle_test.go b/bundles/httpclient/bundle_test.go index 590fe56..e198d55 100644 --- a/bundles/httpclient/bundle_test.go +++ b/bundles/httpclient/bundle_test.go @@ -2,6 +2,7 @@ package httpclient import ( "context" + "errors" "net/http" "net/http/httptest" "testing" @@ -586,3 +587,104 @@ func TestClient_Put_Success(t *testing.T) { t.Error("expected Updated=true") } } + +// --- executeWithRetry error handling --- + +func TestExecuteWithRetry_MaxRetriesExceeded_PreservesCause(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + })) + defer server.Close() + + cfg := DefaultConfig() + cfg.BaseURL = server.URL + cfg.RetryConfig.MaxRetries = 2 + cfg.RetryConfig.InitialInterval = 5 * time.Millisecond + cfg.RetryConfig.MaxInterval = 20 * time.Millisecond + cfg.CircuitBreakerConfig.Name = "test-retry-exhausted" + + b := NewBundle(cfg) + if err := b.Initialize(nil); err != nil { + t.Fatalf("failed to initialize bundle: %v", err) + } + + err := b.Client().Get(context.Background(), "/api/test", nil) + if err == nil { + t.Fatal("expected error after exhausting retries") + } + if !errors.Is(err, ErrMaxRetriesExceeded) { + t.Errorf("expected errors.Is(err, ErrMaxRetriesExceeded), got %v", err) + } + + var httpErr *HTTPError + if !errors.As(err, &httpErr) { + t.Fatalf("expected underlying HTTPError to be preserved, got %v", err) + } + if httpErr.StatusCode != http.StatusServiceUnavailable { + t.Errorf("expected status 503, got %d", httpErr.StatusCode) + } +} + +func TestExecuteWithRetry_ContextDeadlineExceeded(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(200 * time.Millisecond) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + cfg := DefaultConfig() + cfg.BaseURL = server.URL + cfg.RetryConfig.MaxRetries = 3 + cfg.RetryConfig.InitialInterval = 5 * time.Millisecond + cfg.RetryConfig.MaxInterval = 20 * time.Millisecond + cfg.CircuitBreakerConfig.Name = "test-ctx-deadline" + + b := NewBundle(cfg) + if err := b.Initialize(nil); err != nil { + t.Fatalf("failed to initialize bundle: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + + err := b.Client().Get(ctx, "/api/slow", nil) + if err == nil { + t.Fatal("expected error for context deadline exceeded") + } + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("expected errors.Is(err, context.DeadlineExceeded), got %v", err) + } + if errors.Is(err, ErrMaxRetriesExceeded) { + t.Error("context deadline exceeded should not be reported as max retries exceeded") + } +} + +func TestRequest_CircuitBreakerOpen(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + cfg := DefaultConfig() + cfg.BaseURL = server.URL + cfg.RetryConfig.MaxRetries = 0 + cfg.CircuitBreakerConfig.Name = "test-cb-open" + cfg.CircuitBreakerConfig.MaxRequests = 1 + cfg.CircuitBreakerConfig.Interval = time.Minute + cfg.CircuitBreakerConfig.Timeout = time.Minute + + b := NewBundle(cfg) + if err := b.Initialize(nil); err != nil { + t.Fatalf("failed to initialize bundle: %v", err) + } + + // Default ReadyToTrip opens the breaker after 5 consecutive failures. + for i := 0; i < 5; i++ { + _ = b.Client().Get(context.Background(), "/api/test", nil) + } + + err := b.Client().Get(context.Background(), "/api/test", nil) + if !errors.Is(err, ErrCircuitBreakerOpen) { + t.Errorf("expected errors.Is(err, ErrCircuitBreakerOpen), got %v", err) + } +} diff --git a/bundles/jwt/bundle.go b/bundles/jwt/bundle.go index ab1b5dc..25e0b2d 100644 --- a/bundles/jwt/bundle.go +++ b/bundles/jwt/bundle.go @@ -69,6 +69,7 @@ package jwt import ( "context" + stderrors "errors" "fmt" "net/http" "path/filepath" @@ -131,31 +132,31 @@ func DefaultConfig() Config { // Validate validates the JWT configuration. func (c *Config) Validate() error { if len(c.SecretKey) == 0 { - return fmt.Errorf("jwt secret key is required") + return stderrors.New("jwt secret key is required") } if len(c.SecretKey) < 32 { - return fmt.Errorf("jwt secret key must be at least 32 bytes for security") + return stderrors.New("jwt secret key must be at least 32 bytes for security") } if c.Issuer == "" { - return fmt.Errorf("jwt issuer is required") + return stderrors.New("jwt issuer is required") } if c.Audience == "" { - return fmt.Errorf("jwt audience is required") + return stderrors.New("jwt audience is required") } if c.ServiceName == "" { - return fmt.Errorf("service name is required for JWT authentication") + return stderrors.New("service name is required for JWT authentication") } if c.TokenDuration <= 0 { - return fmt.Errorf("token duration must be positive") + return stderrors.New("token duration must be positive") } if c.ClockSkew < 0 { - return fmt.Errorf("clock skew must be non-negative") + return stderrors.New("clock skew must be non-negative") } return nil @@ -398,17 +399,17 @@ func (b *Bundle) HTTPMiddleware(next http.Handler) http.Handler { func (b *Bundle) extractTokenFromMetadata(ctx context.Context) (string, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { - return "", fmt.Errorf("no metadata found") + return "", stderrors.New("no metadata found") } authHeaders := md.Get("authorization") if len(authHeaders) == 0 { - return "", fmt.Errorf("no authorization header found") + return "", stderrors.New("no authorization header found") } authHeader := authHeaders[0] if !strings.HasPrefix(authHeader, "Bearer ") { - return "", fmt.Errorf("invalid authorization header format") + return "", stderrors.New("invalid authorization header format") } return strings.TrimPrefix(authHeader, "Bearer "), nil @@ -424,11 +425,11 @@ func (b *Bundle) addTokenToMetadata(ctx context.Context, token string) context.C func (b *Bundle) extractTokenFromHTTP(r *http.Request) (string, error) { authHeader := r.Header.Get("Authorization") if authHeader == "" { - return "", fmt.Errorf("no authorization header found") + return "", stderrors.New("no authorization header found") } if !strings.HasPrefix(authHeader, "Bearer ") { - return "", fmt.Errorf("invalid authorization header format") + return "", stderrors.New("invalid authorization header format") } return strings.TrimPrefix(authHeader, "Bearer "), nil @@ -643,22 +644,22 @@ func (tc *tokenCache) cleanup() { // validateServiceIdentifier validates service ID and name formats to prevent injection attacks. func validateServiceIdentifier(identifier string) error { if identifier == "" { - return fmt.Errorf("identifier cannot be empty") + return stderrors.New("identifier cannot be empty") } // Service identifiers should only contain alphanumeric characters, hyphens, and underscores // This prevents injection attacks and ensures safe usage in logs and metrics if !validServiceIdentifierRe.MatchString(identifier) { - return fmt.Errorf("identifier contains invalid characters, only alphanumeric, hyphens, and underscores allowed") + return stderrors.New("identifier contains invalid characters, only alphanumeric, hyphens, and underscores allowed") } // Reasonable length limits if len(identifier) > 64 { - return fmt.Errorf("identifier too long, maximum 64 characters") + return stderrors.New("identifier too long, maximum 64 characters") } if len(identifier) < 2 { - return fmt.Errorf("identifier too short, minimum 2 characters") + return stderrors.New("identifier too short, minimum 2 characters") } return nil diff --git a/bundles/prometheus/bundle.go b/bundles/prometheus/bundle.go index 6d402d0..16fc76f 100644 --- a/bundles/prometheus/bundle.go +++ b/bundles/prometheus/bundle.go @@ -141,16 +141,16 @@ func DefaultConfig() Config { // Validate validates the Prometheus configuration. func (c *Config) Validate() error { if c.Namespace == "" { - return fmt.Errorf("namespace is required for Prometheus metrics") + return stderrors.New("namespace is required for Prometheus metrics") } // Validate metric naming (Prometheus has strict naming rules) if !isValidMetricName(c.Namespace) { - return fmt.Errorf("invalid namespace format: must match [a-zA-Z_:][a-zA-Z0-9_:]*") + return stderrors.New("invalid namespace format: must match [a-zA-Z_:][a-zA-Z0-9_:]*") } if c.Subsystem != "" && !isValidMetricName(c.Subsystem) { - return fmt.Errorf("invalid subsystem format: must match [a-zA-Z_:][a-zA-Z0-9_:]*") + return stderrors.New("invalid subsystem format: must match [a-zA-Z_:][a-zA-Z0-9_:]*") } // Validate service labels for security and cardinality @@ -175,7 +175,7 @@ func (c *Config) Validate() error { // Validate histogram buckets if len(c.HistogramBuckets) == 0 { - return fmt.Errorf("histogram buckets cannot be empty") + return stderrors.New("histogram buckets cannot be empty") } if len(c.HistogramBuckets) > 50 { @@ -185,13 +185,13 @@ func (c *Config) Validate() error { // Ensure buckets are in ascending order and reasonable for i := 1; i < len(c.HistogramBuckets); i++ { if c.HistogramBuckets[i] <= c.HistogramBuckets[i-1] { - return fmt.Errorf("histogram buckets must be in ascending order") + return stderrors.New("histogram buckets must be in ascending order") } } // Validate bucket ranges are reasonable if c.HistogramBuckets[0] <= 0 { - return fmt.Errorf("histogram buckets must be positive") + return stderrors.New("histogram buckets must be positive") } return nil @@ -641,7 +641,7 @@ func (b *Bundle) validateMetricDefinition(name, help string, labelNames []string } if help == "" { - return fmt.Errorf("metric help text is required") + return stderrors.New("metric help text is required") } // Prevent high cardinality @@ -793,7 +793,7 @@ func (c *PrometheusHealthCheck) Readiness(ctx context.Context) error { // Ensure we have at least some metrics registered if len(metricFamilies) == 0 { - return fmt.Errorf("no metrics registered in Prometheus registry") + return stderrors.New("no metrics registered in Prometheus registry") } // Check for expected namespace metrics diff --git a/bundles/redis/bundle.go b/bundles/redis/bundle.go index a974afa..bedd4fd 100644 --- a/bundles/redis/bundle.go +++ b/bundles/redis/bundle.go @@ -76,6 +76,7 @@ import ( "crypto/tls" "encoding/hex" "encoding/json" + stderrors "errors" "fmt" "net/url" "os" @@ -138,7 +139,7 @@ func DefaultConfig() Config { // Validate validates the Redis configuration. func (c *Config) Validate() error { if c.RedisURL == "" { - return fmt.Errorf("redis_url is required") + return stderrors.New("redis_url is required") } // Parse and validate Redis URL @@ -149,28 +150,28 @@ func (c *Config) Validate() error { // Validate scheme if parsedURL.Scheme != "redis" && parsedURL.Scheme != "rediss" { - return fmt.Errorf("redis_url must use 'redis://' or 'rediss://' scheme") + return stderrors.New("redis_url must use 'redis://' or 'rediss://' scheme") } // Security: Enforce authentication for remote connections if parsedURL.Hostname() != "localhost" && parsedURL.Hostname() != "127.0.0.1" && parsedURL.Hostname() != "" { if parsedURL.User == nil { - return fmt.Errorf("authentication required for remote Redis connections") + return stderrors.New("authentication required for remote Redis connections") } if password, ok := parsedURL.User.Password(); !ok || password == "" { - return fmt.Errorf("password required for remote Redis connections") + return stderrors.New("password required for remote Redis connections") } } // Security: Enforce TLS for remote connections if parsedURL.Scheme == "redis" && parsedURL.Hostname() != "localhost" && parsedURL.Hostname() != "127.0.0.1" && parsedURL.Hostname() != "" { - return fmt.Errorf("TLS required for remote Redis connections, use rediss:// scheme") + return stderrors.New("TLS required for remote Redis connections, use rediss:// scheme") } // Validate TLS configuration if using rediss:// if parsedURL.Scheme == "rediss" && c.TLSConfig != nil { if c.TLSConfig.InsecureSkipVerify { - return fmt.Errorf("TLS certificate verification cannot be disabled for security") + return stderrors.New("TLS certificate verification cannot be disabled for security") } } @@ -392,7 +393,7 @@ func (c *RedisHealthCheck) Liveness(ctx context.Context) error { return c.client.Ping(ctx).Err() } -// Readiness performs a more comprehensive check including memory and performance. +// Readiness performs a basic connectivity check. func (c *RedisHealthCheck) Readiness(ctx context.Context) error { timeout := c.timeout if timeout <= 0 { @@ -406,44 +407,16 @@ func (c *RedisHealthCheck) Readiness(ctx context.Context) error { defer cancel() } - // First check basic connectivity if err := c.client.Ping(ctx).Err(); err != nil { return fmt.Errorf("Redis ping failed: %w", err) } - // Check Redis memory usage and basic operations - info, err := c.client.Info(ctx, "memory").Result() - if err != nil { - return fmt.Errorf("failed to get Redis memory info: %w", err) - } - - if info == "" { - return fmt.Errorf("Redis memory info returned empty") - } - - // Test basic set/get operation - testKey := "forge:health:test" - testValue := "ok" - - if err := c.client.Set(ctx, testKey, testValue, 30*time.Second).Err(); err != nil { - return fmt.Errorf("failed to set test key in Redis: %w", err) - } - - result, err := c.client.Get(ctx, testKey).Result() - if err != nil { - return fmt.Errorf("failed to get test key from Redis: %w", err) - } - - if result != testValue { - return fmt.Errorf("Redis test operation failed: expected %q, got %q", testValue, result) - } - - // Clean up test key - c.client.Del(ctx, testKey) - return nil } +// ErrCacheMiss indicates the requested key does not exist in the cache. +var ErrCacheMiss = stderrors.New("cache miss") + // CacheService provides high-level caching operations. type CacheService struct { client redis.UniversalClient @@ -468,8 +441,8 @@ func (c *CacheService) Set(ctx context.Context, key string, value interface{}, t func (c *CacheService) Get(ctx context.Context, key string, dest interface{}) error { data, err := c.client.Get(ctx, key).Result() if err != nil { - if err == redis.Nil { - return fmt.Errorf("cache key not found: %s", key) + if stderrors.Is(err, redis.Nil) { + return fmt.Errorf("cache key %q: %w", key, ErrCacheMiss) } return fmt.Errorf("failed to get cache value: %w", err) } diff --git a/bundles/redis/bundle_test.go b/bundles/redis/bundle_test.go index a94a137..75c2239 100644 --- a/bundles/redis/bundle_test.go +++ b/bundles/redis/bundle_test.go @@ -2,12 +2,35 @@ package redis import ( "context" + stderrors "errors" + "fmt" "testing" "time" + goredis "github.com/redis/go-redis/v9" + "github.com/datariot/forge/errors" ) +// newLiveTestClient returns a Redis client connected to a local test instance, +// skipping the test if no Redis server is reachable. +func newLiveTestClient(t *testing.T) goredis.UniversalClient { + t.Helper() + + client := goredis.NewClient(&goredis.Options{Addr: "localhost:6379"}) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + if err := client.Ping(ctx).Err(); err != nil { + client.Close() + t.Skipf("skipping: no Redis available at localhost:6379: %v", err) + } + + t.Cleanup(func() { client.Close() }) + return client +} + // TestDefaultConfig tests default configuration values func TestDefaultConfig(t *testing.T) { cfg := DefaultConfig() @@ -328,3 +351,61 @@ func TestConfig_Validate_EdgeCases(t *testing.T) { }) } } + +// --- CacheService.Get cache miss handling --- + +func TestCacheService_Get_CacheMiss(t *testing.T) { + client := newLiveTestClient(t) + cache := NewCacheService(client) + + key := fmt.Sprintf("forge:test:cachemiss:%d", time.Now().UnixNano()) + ctx := context.Background() + + // Ensure the key does not exist. + client.Del(ctx, key) + + var dest string + err := cache.Get(ctx, key, &dest) + if err == nil { + t.Fatal("expected error for missing cache key") + } + if !stderrors.Is(err, ErrCacheMiss) { + t.Errorf("expected errors.Is(err, ErrCacheMiss), got %v", err) + } +} + +func TestCacheService_Get_Success(t *testing.T) { + client := newLiveTestClient(t) + cache := NewCacheService(client) + + key := fmt.Sprintf("forge:test:cachehit:%d", time.Now().UnixNano()) + ctx := context.Background() + defer client.Del(ctx, key) + + if err := cache.Set(ctx, key, "hello", time.Minute); err != nil { + t.Fatalf("unexpected error setting cache value: %v", err) + } + + var dest string + if err := cache.Get(ctx, key, &dest); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest != "hello" { + t.Errorf("expected 'hello', got %q", dest) + } +} + +// --- RedisHealthCheck.Readiness --- + +func TestRedisHealthCheck_Readiness_Success(t *testing.T) { + client := newLiveTestClient(t) + + check := &RedisHealthCheck{ + client: client, + timeout: 2 * time.Second, + } + + if err := check.Readiness(context.Background()); err != nil { + t.Errorf("expected Readiness to succeed, got: %v", err) + } +} From e41704eff96529162e4bb2b9285e3c71a1b60467 Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 08:56:45 -0600 Subject: [PATCH 5/8] feat: background health check runner with cached probe results - registry: Start/Stop lifecycle runs each check on its configured Interval/InitialDelay, caching results; probe endpoints serve the cached snapshot so a hanging dependency can never block kubelet probes - registry: live-computation fallback preserved when runner not started (library/test use); /health built from one snapshot read instead of two serialized live rounds; typed probe dispatch replaces stringly liveness/readiness switch - app: registry runner started after bundle registration, stopped on shutdown; gRPC health service now re-synced from cached results every 5s instead of a one-shot startup snapshot Co-Authored-By: Claude Fable 5 --- framework/app.go | 63 ++++++- health/registry.go | 392 ++++++++++++++++++++++++++++++++++++---- health/registry_test.go | 289 +++++++++++++++++++++++++++++ 3 files changed, 700 insertions(+), 44 deletions(-) diff --git a/framework/app.go b/framework/app.go index 8994120..b5539b7 100644 --- a/framework/app.go +++ b/framework/app.go @@ -227,10 +227,11 @@ type App struct { startAt time.Time // Core managers - logging *LoggingManager - observability *ObservabilityManager - healthRegistry *forgeHealth.Registry - grpcHealthServer *health.Server + logging *LoggingManager + observability *ObservabilityManager + healthRegistry *forgeHealth.Registry + grpcHealthServer *health.Server + grpcHealthSyncStop chan struct{} // Servers grpcServer *grpc.Server @@ -553,6 +554,20 @@ func (a *App) Start(ctx context.Context) error { logger.Debug().Int("hook_index", i).Msg("Startup hook executed successfully") } + // Start the health registry's background check runner now that bundles + // and startup hooks have registered their checks, but before servers + // start accepting traffic. From this point on, /health probes are + // served from cached results instead of running checks live. + // + // This intentionally uses a background context rather than the Start + // context above: the latter is only meant to bound startup itself (as + // with startGRPCServer/startHTTPServer), not the runner's lifetime. + // The runner is stopped explicitly in Stop. + if err := a.healthRegistry.Start(context.Background()); err != nil { + return fmt.Errorf("failed to start health registry: %w", err) + } + logger.Info().Msg("Health registry background runner started") + // Create and start gRPC server only if registrars are configured if len(a.registrars) > 0 { if err := a.startGRPCServer(ctx); err != nil { @@ -588,6 +603,13 @@ func (a *App) Start(ctx context.Context) error { logger.Info().Msg("Service marked as ready") } + // Keep the gRPC health service in sync with cached background check + // results for the process lifetime, not just the startup snapshot. + if a.grpcHealthServer != nil { + a.grpcHealthSyncStop = make(chan struct{}) + go a.syncGRPCHealthLoop(a.grpcHealthSyncStop) + } + a.running = true logger.Info().Msg("Service started successfully") @@ -613,6 +635,18 @@ func (a *App) Stop(ctx context.Context) error { a.healthRegistry.SetReady(false) a.updateGRPCHealthStatus() + if a.grpcHealthSyncStop != nil { + close(a.grpcHealthSyncStop) + a.grpcHealthSyncStop = nil + } + + // Stop the health registry's background check runner. This waits for + // in-flight check goroutines to notice cancellation and exit, bounded + // by ctx, so shutdown doesn't hang on a wedged dependency check. + if err := a.healthRegistry.Stop(ctx); err != nil { + logger.Warn().Err(err).Msg("health registry runner did not stop cleanly") + } + // Create shutdown orchestrator with proper timeout orchestrator := NewShutdownOrchestrator(a.config.ShutdownTimeout) @@ -897,3 +931,24 @@ func (a *App) updateGRPCHealthStatus() { } } } + +// grpcHealthSyncInterval is how often the gRPC health service is refreshed +// from the health registry's cached check results. +const grpcHealthSyncInterval = 5 * time.Second + +// syncGRPCHealthLoop periodically mirrors health registry status into the +// gRPC health service until the stop channel is closed. Reads are served +// from the registry's cache, so each tick is a lock-and-copy, not live checks. +func (a *App) syncGRPCHealthLoop(stop <-chan struct{}) { + ticker := time.NewTicker(grpcHealthSyncInterval) + defer ticker.Stop() + + for { + select { + case <-stop: + return + case <-ticker.C: + a.updateGRPCHealthStatus() + } + } +} diff --git a/health/registry.go b/health/registry.go index ec85620..8dc5051 100644 --- a/health/registry.go +++ b/health/registry.go @@ -3,18 +3,72 @@ package health import ( "context" "fmt" + "maps" "sync" "time" ) +// defaultCheckTimeout is used for a single check invocation when the +// registered CheckConfig does not specify a Timeout. +const defaultCheckTimeout = 5 * time.Second + +// defaultCheckInterval is used for the background runner when the +// registered CheckConfig does not specify an Interval. +const defaultCheckInterval = 30 * time.Second + +// probeKind selects which side of a Check (liveness or readiness) to invoke. +// It carries a method-value extractor rather than a string tag so that +// dispatch has no "unknown kind" branch to fall through to - only the two +// package-level values below exist, and each knows how to run itself. +// Callers always pass one of *probeLiveness / *probeReadiness, which makes +// pointer identity (used in snapshotStatus) a valid way to tell them apart. +type probeKind struct { + label string + fn func(Check) func(context.Context) error +} + +// probeLiveness dispatches to Check.Liveness. +var probeLiveness = &probeKind{ + label: "liveness", + fn: func(c Check) func(context.Context) error { return c.Liveness }, +} + +// probeReadiness dispatches to Check.Readiness. +var probeReadiness = &probeKind{ + label: "readiness", + fn: func(c Check) func(context.Context) error { return c.Readiness }, +} + +// checkCache holds the most recent background results for a single +// registered check. A zero-value checkCache (ran == false) means the +// background runner has not completed a round for this check yet. +type checkCache struct { + liveness CheckResult + readiness CheckResult + ran bool +} + // Registry manages a collection of health checks and provides methods // to check the overall health status of a service. +// +// By default, Registry computes results live on every call (the original +// behavior, and what unit tests and direct library use still get). Calling +// Start runs checks in the background on their configured Interval and +// serves cached results instead, so a probe read never blocks on a slow or +// hanging dependency check. type Registry struct { mu sync.RWMutex checks map[string]Check configs map[string]CheckConfig ready bool logger Logger + + // Background runner state, guarded by mu. + started bool + runnerCtx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + cache map[string]*checkCache } // Logger interface for health check logging. @@ -44,10 +98,15 @@ func NewRegistry(logger Logger) *Registry { configs: make(map[string]CheckConfig), ready: false, logger: logger, + cache: make(map[string]*checkCache), } } // Register registers a health check with the registry. +// +// If the background runner is already started (see Start), the new check +// is picked up immediately and scheduled on its own goroutine so that +// checks registered after startup still get background execution. func (r *Registry) Register(check Check, config CheckConfig) error { if check == nil { return fmt.Errorf("check cannot be nil") @@ -59,16 +118,29 @@ func (r *Registry) Register(check Check, config CheckConfig) error { } r.mu.Lock() - defer r.mu.Unlock() if _, exists := r.checks[name]; exists { + r.mu.Unlock() return fmt.Errorf("check with name %q already registered", name) } r.checks[name] = check r.configs[name] = config + r.cache[name] = &checkCache{} + + started := r.started + runnerCtx := r.runnerCtx + if started { + r.wg.Add(1) + } + r.mu.Unlock() r.logger.Info("registered health check", "name", name, "required", config.Required) + + if started { + go r.runCheck(runnerCtx, name, check, config) + } + return nil } @@ -80,12 +152,17 @@ func (r *Registry) MustRegister(check Check, config CheckConfig) { } // Unregister removes a health check from the registry. +// +// If the background runner is active, the check's goroutine notices the +// removal on its next tick (or initial round) and exits on its own; there +// is nothing further to clean up here. func (r *Registry) Unregister(name string) { r.mu.Lock() defer r.mu.Unlock() delete(r.checks, name) delete(r.configs, name) + delete(r.cache, name) r.logger.Info("unregistered health check", "name", name) } @@ -106,57 +183,240 @@ func (r *Registry) IsReady() bool { return r.ready } -// CheckLiveness performs liveness checks on all registered health checks. -func (r *Registry) CheckLiveness(ctx context.Context) HealthStatus { - // Add overall timeout for all liveness checks - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) +// Start launches the background check runner: one goroutine per registered +// check that waits its CheckConfig.InitialDelay, runs an immediate round, +// and then re-runs on CheckConfig.Interval until Stop is called or ctx is +// done. Once started, CheckLiveness/CheckReadiness/CheckHealth serve the +// cached results from these rounds instead of computing live, so a probe +// read is never blocked by a slow or hanging dependency. +// +// Start is not safe to call twice without an intervening Stop. +func (r *Registry) Start(ctx context.Context) error { + r.mu.Lock() + if r.started { + r.mu.Unlock() + return fmt.Errorf("health registry runner already started") + } + + runnerCtx, cancel := context.WithCancel(ctx) + r.runnerCtx = runnerCtx + r.cancel = cancel + r.started = true + + checks := maps.Clone(r.checks) + configs := maps.Clone(r.configs) + r.mu.Unlock() + + for name, check := range checks { + r.wg.Add(1) + go r.runCheck(runnerCtx, name, check, configs[name]) + } + + return nil +} + +// Stop cancels all background runner goroutines and waits for them to +// exit, or for ctx to be done, whichever comes first. It is safe to call +// even if Start was never called. After Stop returns (with a nil error), +// no runner goroutines remain. +func (r *Registry) Stop(ctx context.Context) error { + r.mu.Lock() + if !r.started { + r.mu.Unlock() + return nil + } + + cancel := r.cancel + r.started = false + r.cancel = nil + r.runnerCtx = nil + r.mu.Unlock() + + cancel() + + done := make(chan struct{}) + go func() { + r.wg.Wait() + close(done) + }() + + select { + case <-done: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// runCheck is the background goroutine body for a single check: wait for +// InitialDelay, run an immediate round, then run one round per Interval +// until ctx is cancelled or the check is unregistered. +func (r *Registry) runCheck(ctx context.Context, name string, check Check, config CheckConfig) { + defer r.wg.Done() + + if config.InitialDelay > 0 { + select { + case <-time.After(config.InitialDelay): + case <-ctx.Done(): + return + } + } + + r.runRound(ctx, name, check, config) + + interval := config.Interval + if interval <= 0 { + interval = defaultCheckInterval + } + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if !r.stillRegistered(name) { + return + } + r.runRound(ctx, name, check, config) + } + } +} + +// stillRegistered reports whether name is still a registered check. +func (r *Registry) stillRegistered(name string) bool { + r.mu.RLock() + defer r.mu.RUnlock() + _, ok := r.checks[name] + return ok +} + +// runRound executes one liveness+readiness round for a single check and +// stores the results in the cache under mu. +func (r *Registry) runRound(ctx context.Context, name string, check Check, config CheckConfig) { + timeout := config.Timeout + if timeout <= 0 { + timeout = defaultCheckTimeout + } + + liveness := r.executeOne(ctx, timeout, name, check, config, probeLiveness) + readiness := r.executeOne(ctx, timeout, name, check, config, probeReadiness) + + r.mu.Lock() + entry, ok := r.cache[name] + if !ok { + entry = &checkCache{} + r.cache[name] = entry + } + entry.liveness = liveness + entry.readiness = readiness + entry.ran = true + r.mu.Unlock() +} + +// executeOne runs a single probe against a single check with a per-run +// timeout derived from parent, and returns the resulting CheckResult. +func (r *Registry) executeOne(parent context.Context, timeout time.Duration, name string, check Check, config CheckConfig, kind *probeKind) CheckResult { + ctx, cancel := context.WithTimeout(parent, timeout) defer cancel() + start := time.Now() + err := kind.fn(check)(ctx) + duration := time.Since(start) + + result := NewCheckResult(name, config.Required, duration, err) + + if err != nil { + r.logger.Warn("health check failed", "name", name, "type", kind.label, "error", err, "duration", duration) + } else { + r.logger.Debug("health check passed", "name", name, "type", kind.label, "duration", duration) + } + + return result +} + +// CheckLiveness performs liveness checks on all registered health checks. +// +// If the background runner is active, this serves the latest cached +// results instead of computing them live. +func (r *Registry) CheckLiveness(ctx context.Context) HealthStatus { r.mu.RLock() - checks := make(map[string]Check, len(r.checks)) - configs := make(map[string]CheckConfig, len(r.configs)) - for name, check := range r.checks { - checks[name] = check - configs[name] = r.configs[name] + if r.started { + status := r.snapshotStatus(probeLiveness) + r.mu.RUnlock() + return status } + checks := maps.Clone(r.checks) + configs := maps.Clone(r.configs) r.mu.RUnlock() - return r.performChecks(ctx, checks, configs, "liveness") + // Add overall timeout for all liveness checks + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + return r.performChecks(ctx, checks, configs, probeLiveness) } // CheckReadiness performs readiness checks on all registered health checks. +// +// If the background runner is active, this serves the latest cached +// results instead of computing them live. func (r *Registry) CheckReadiness(ctx context.Context) HealthStatus { - // Add overall timeout for all readiness checks - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - r.mu.RLock() - checks := make(map[string]Check, len(r.checks)) - configs := make(map[string]CheckConfig, len(r.configs)) - ready := r.ready - for name, check := range r.checks { - checks[name] = check - configs[name] = r.configs[name] + if r.started { + status := r.snapshotStatus(probeReadiness) + ready := r.ready + r.mu.RUnlock() + return applyReadyGate(status, ready) } + checks := maps.Clone(r.checks) + configs := maps.Clone(r.configs) + ready := r.ready r.mu.RUnlock() - status := r.performChecks(ctx, checks, configs, "readiness") + // Add overall timeout for all readiness checks + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + status := r.performChecks(ctx, checks, configs, probeReadiness) + return applyReadyGate(status, ready) +} - // If the service hasn't been marked as ready, it's not ready regardless of checks +// applyReadyGate forces status to unhealthy when the service hasn't been +// explicitly marked ready, regardless of individual check results. +func applyReadyGate(status HealthStatus, ready bool) HealthStatus { if !ready { status.Status = StatusUnhealthy status.Message = "Service not marked as ready" } - return status } // CheckHealth performs both liveness and readiness checks and returns combined results. +// +// When the background runner is active, both sides are built from a single +// cached snapshot (one lock acquisition) rather than two serialized live +// rounds of every check. func (r *Registry) CheckHealth(ctx context.Context) HealthStatus { + r.mu.RLock() + if r.started { + liveness := r.snapshotStatus(probeLiveness) + readiness := applyReadyGate(r.snapshotStatus(probeReadiness), r.ready) + r.mu.RUnlock() + return combineHealth(liveness, readiness) + } + r.mu.RUnlock() + liveness := r.CheckLiveness(ctx) readiness := r.CheckReadiness(ctx) + return combineHealth(liveness, readiness) +} - // Combine results - service is healthy if both liveness and readiness pass +// combineHealth merges a liveness and a readiness HealthStatus into the +// combined status returned by CheckHealth. +func combineHealth(liveness, readiness HealthStatus) HealthStatus { status := HealthStatus{ Status: StatusHealthy, Message: "All checks passed", @@ -184,8 +444,70 @@ func (r *Registry) CheckHealth(ctx context.Context) HealthStatus { return status } -// performChecks executes health checks and returns aggregated results. -func (r *Registry) performChecks(ctx context.Context, checks map[string]Check, configs map[string]CheckConfig, checkType string) HealthStatus { +// snapshotStatus builds a HealthStatus from the cached background results +// for the given probe kind. Callers must hold at least r.mu.RLock. +// +// A check that the background runner hasn't completed a round for yet is +// reported as pending (StatusUnknown), which counts as failing for a +// required check - this is what keeps a pod from being reported ready +// before its first real check has run. +func (r *Registry) snapshotStatus(kind *probeKind) HealthStatus { + if len(r.checks) == 0 { + return HealthStatus{ + Status: StatusHealthy, + Message: "No health checks registered", + Timestamp: time.Now().UTC(), + Details: make(map[string]CheckResult), + } + } + + results := make(map[string]CheckResult, len(r.checks)) + overallHealthy := true + + for name, config := range r.configs { + entry, ok := r.cache[name] + + var result CheckResult + switch { + case !ok || !entry.ran: + result = CheckResult{ + Name: name, + Status: StatusUnknown, + Message: "pending first check", + Timestamp: time.Now().UTC(), + Required: config.Required, + } + case kind == probeLiveness: + result = entry.liveness + default: + result = entry.readiness + } + + results[name] = result + if result.Required && !result.IsHealthy() { + overallHealthy = false + } + } + + status := StatusHealthy + message := "All checks passed" + if !overallHealthy { + status = StatusUnhealthy + message = "One or more required checks failed" + } + + return HealthStatus{ + Status: status, + Message: message, + Timestamp: time.Now().UTC(), + Details: results, + } +} + +// performChecks executes health checks live and returns aggregated results. +// This is the fallback path used when the background runner has not been +// started (unit tests, direct library use without Start). +func (r *Registry) performChecks(ctx context.Context, checks map[string]Check, configs map[string]CheckConfig, kind *probeKind) HealthStatus { if len(checks) == 0 { return HealthStatus{ Status: StatusHealthy, @@ -208,17 +530,7 @@ func (r *Registry) performChecks(ctx context.Context, checks map[string]Check, c defer wg.Done() start := time.Now() - var err error - - switch checkType { - case "liveness": - err = check.Liveness(ctx) - case "readiness": - err = check.Readiness(ctx) - default: - err = fmt.Errorf("unknown check type: %s", checkType) - } - + err := kind.fn(check)(ctx) duration := time.Since(start) result := NewCheckResult(name, config.Required, duration, err) @@ -230,9 +542,9 @@ func (r *Registry) performChecks(ctx context.Context, checks map[string]Check, c mu.Unlock() if err != nil { - r.logger.Warn("health check failed", "name", name, "type", checkType, "error", err, "duration", duration) + r.logger.Warn("health check failed", "name", name, "type", kind.label, "error", err, "duration", duration) } else { - r.logger.Debug("health check passed", "name", name, "type", checkType, "duration", duration) + r.logger.Debug("health check passed", "name", name, "type", kind.label, "duration", duration) } }(name, check, configs[name]) } diff --git a/health/registry_test.go b/health/registry_test.go index d88aeec..bda6d05 100644 --- a/health/registry_test.go +++ b/health/registry_test.go @@ -3,6 +3,7 @@ package health import ( "context" "errors" + "sync" "testing" "time" ) @@ -435,3 +436,291 @@ func (c *testCheck) Readiness(ctx context.Context) error { } return c.readinessErr } + +// blockingCheck blocks Liveness/Readiness on ctx.Done() (never returning on +// its own), letting a test drive it via context cancellation only. onCall, +// if set, is invoked (once, safely from concurrent probes) the first time +// either method is entered. +type blockingCheck struct { + name string + + once sync.Once + onCall func() +} + +func (c *blockingCheck) Name() string { return c.name } + +func (c *blockingCheck) block(ctx context.Context) error { + if c.onCall != nil { + c.once.Do(c.onCall) + } + <-ctx.Done() + return ctx.Err() +} + +func (c *blockingCheck) Liveness(ctx context.Context) error { return c.block(ctx) } +func (c *blockingCheck) Readiness(ctx context.Context) error { return c.block(ctx) } + +// gatedCheck succeeds immediately for its first roundSize invocations (one +// full round's worth of Liveness+Readiness calls), then blocks on ctx.Done() +// for every invocation after that. It's used to simulate a dependency that +// is healthy on the first background round and then hangs on the next one. +type gatedCheck struct { + name string + roundSize int + + mu sync.Mutex + calls int + + firstRoundDone chan struct{} + once sync.Once +} + +func newGatedCheck(name string) *gatedCheck { + return &gatedCheck{ + name: name, + roundSize: 2, // liveness + readiness + firstRoundDone: make(chan struct{}), + } +} + +func (c *gatedCheck) Name() string { return c.name } + +func (c *gatedCheck) probe(ctx context.Context) error { + c.mu.Lock() + c.calls++ + n := c.calls + c.mu.Unlock() + + if n <= c.roundSize { + if n == c.roundSize { + c.once.Do(func() { close(c.firstRoundDone) }) + } + return nil + } + + <-ctx.Done() + return ctx.Err() +} + +func (c *gatedCheck) Liveness(ctx context.Context) error { return c.probe(ctx) } +func (c *gatedCheck) Readiness(ctx context.Context) error { return c.probe(ctx) } + +// waitClosed waits for ch to be closed, failing the test if it isn't within d. +func waitClosed(t *testing.T, ch chan struct{}, d time.Duration, what string) { + t.Helper() + select { + case <-ch: + case <-time.After(d): + t.Fatalf("timed out waiting for %s", what) + } +} + +// TestRegistry_Runner_CachesResultsAndServesStaleReads verifies that once +// Start has been called, a probe read is served from the background +// runner's cache: it returns near-instantly and reflects the last +// completed round, even while a new round is in flight (e.g. blocked on a +// hanging dependency). +func TestRegistry_Runner_CachesResultsAndServesStaleReads(t *testing.T) { + registry := NewRegistry(nil) + registry.SetReady(true) + + check := newGatedCheck("dep") + cfg := DefaultCheckConfig("dep") + cfg.Interval = 10 * time.Millisecond + cfg.InitialDelay = 0 + cfg.Timeout = time.Second + if err := registry.Register(check, cfg); err != nil { + t.Fatalf("Register failed: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if err := registry.Start(ctx); err != nil { + t.Fatalf("Start failed: %v", err) + } + t.Cleanup(func() { + stopCtx, stopCancel := context.WithTimeout(context.Background(), 2*time.Second) + defer stopCancel() + if err := registry.Stop(stopCtx); err != nil { + t.Errorf("Stop returned error: %v", err) + } + }) + + // Wait for the first round to complete so the cache is populated. + waitClosed(t, check.firstRoundDone, 2*time.Second, "first background round") + + // The second round is now in flight and will block forever (until + // ctx/Stop cancellation). A probe read must not wait for it - it + // should return the still-healthy result from the first round. + done := make(chan HealthStatus, 1) + go func() { done <- registry.CheckReadiness(context.Background()) }() + + select { + case status := <-done: + if status.Status != StatusHealthy { + t.Errorf("expected cached status healthy, got %s (details: %+v)", status.Status, status.Details) + } + case <-time.After(500 * time.Millisecond): + t.Fatal("CheckReadiness blocked on the in-flight (hanging) check instead of serving the cache") + } +} + +// TestRegistry_Runner_PreFirstRoundReadinessNotReady verifies that a probe +// hitting the registry before the background runner's first round has +// completed reports not-ready, rather than optimistically healthy. +func TestRegistry_Runner_PreFirstRoundReadinessNotReady(t *testing.T) { + registry := NewRegistry(nil) + registry.SetReady(true) + + check := &blockingCheck{name: "slow-start"} + cfg := DefaultCheckConfig("slow-start") + cfg.InitialDelay = time.Hour // effectively "never during this test" + if err := registry.Register(check, cfg); err != nil { + t.Fatalf("Register failed: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if err := registry.Start(ctx); err != nil { + t.Fatalf("Start failed: %v", err) + } + t.Cleanup(func() { + stopCtx, stopCancel := context.WithTimeout(context.Background(), 2*time.Second) + defer stopCancel() + registry.Stop(stopCtx) //nolint:errcheck + }) + + // InitialDelay means the runner goroutine hasn't run a round yet, so + // this must be answered from the "pending" synthetic result, not by + // invoking the (permanently blocking) check. + status := registry.CheckReadiness(context.Background()) + if status.Status != StatusUnhealthy { + t.Errorf("expected pending check to report unhealthy/not-ready, got %s", status.Status) + } + if result, ok := status.Details["slow-start"]; ok { + if result.Status != StatusUnknown { + t.Errorf("expected pending check detail status %s, got %s", StatusUnknown, result.Status) + } + } else { + t.Error("expected a details entry for the pending check") + } +} + +// TestRegistry_Runner_LateRegisteredCheckGetsScheduled verifies that a +// check registered after Start still gets its own background runner. +func TestRegistry_Runner_LateRegisteredCheckGetsScheduled(t *testing.T) { + registry := NewRegistry(nil) + registry.SetReady(true) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if err := registry.Start(ctx); err != nil { + t.Fatalf("Start failed: %v", err) + } + t.Cleanup(func() { + stopCtx, stopCancel := context.WithTimeout(context.Background(), 2*time.Second) + defer stopCancel() + if err := registry.Stop(stopCtx); err != nil { + t.Errorf("Stop returned error: %v", err) + } + }) + + check := newGatedCheck("late") + cfg := DefaultCheckConfig("late") + cfg.Interval = 10 * time.Millisecond + if err := registry.Register(check, cfg); err != nil { + t.Fatalf("Register failed: %v", err) + } + + waitClosed(t, check.firstRoundDone, 2*time.Second, "late-registered check's first background round") + + status := registry.CheckLiveness(context.Background()) + if status.Status != StatusHealthy { + t.Errorf("expected late-registered check to report healthy after its round, got %s", status.Status) + } +} + +// TestRegistry_Stop_NoGoroutineLeak verifies that Stop terminates all +// runner goroutines - including one currently blocked inside a check - +// without leaking. The assertion is that Stop itself returns (its +// implementation waits on a done channel derived from the runner +// goroutines' WaitGroup), not a fixed sleep. +func TestRegistry_Stop_NoGoroutineLeak(t *testing.T) { + registry := NewRegistry(nil) + + started := make(chan struct{}) + check := &blockingCheck{onCall: func() { close(started) }, name: "blocking"} + cfg := DefaultCheckConfig("blocking") + if err := registry.Register(check, cfg); err != nil { + t.Fatalf("Register failed: %v", err) + } + + if err := registry.Start(context.Background()); err != nil { + t.Fatalf("Start failed: %v", err) + } + + waitClosed(t, started, 2*time.Second, "background runner to invoke the blocking check") + + stopDone := make(chan error, 1) + go func() { stopDone <- registry.Stop(context.Background()) }() + + select { + case err := <-stopDone: + if err != nil { + t.Errorf("Stop returned error: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Stop did not return in time - a runner goroutine leaked") + } +} + +// TestRegistry_Fallback_NotStarted verifies the registry still computes +// live when Start has never been called, preserving the pre-existing API +// contract for unit tests / direct library use. +func TestRegistry_Fallback_NotStarted(t *testing.T) { + registry := NewRegistry(nil) + registry.SetReady(true) + + calls := 0 + var mu sync.Mutex + check := &testCheck{name: "fallback"} + // Wrap to count invocations without changing testCheck's shape. + counting := &countingCheck{testCheck: check, onCall: func() { + mu.Lock() + calls++ + mu.Unlock() + }} + + if err := registry.Register(counting, DefaultCheckConfig("fallback")); err != nil { + t.Fatalf("Register failed: %v", err) + } + + ctx := context.Background() + for i := 0; i < 3; i++ { + status := registry.CheckLiveness(ctx) + if status.Status != StatusHealthy { + t.Fatalf("expected healthy status, got %s", status.Status) + } + } + + mu.Lock() + defer mu.Unlock() + if calls != 3 { + t.Errorf("expected the fallback (non-started) path to invoke the check live on every call (3), got %d", calls) + } +} + +// countingCheck wraps a testCheck and calls onCall on every Liveness call, +// used to distinguish "computed live" from "served from cache". +type countingCheck struct { + *testCheck + onCall func() +} + +func (c *countingCheck) Liveness(ctx context.Context) error { + if c.onCall != nil { + c.onCall() + } + return c.testCheck.Liveness(ctx) +} From 8023d2db68dde5ad5d1d756d1e44a51351f5818d Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 08:57:56 -0600 Subject: [PATCH 6/8] chore: gitignore compiled example binaries Co-Authored-By: Claude Fable 5 --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 568aa5c..c6dbf26 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,12 @@ examples/*/jwt-service examples/*/httpclient-service examples/*/prometheus-service examples/*/config-service + +# Compiled example binaries (go build in examples// produces examples//) +examples/simple-service/simple-service +examples/config-service/config-service +examples/jwt-service/jwt-service +examples/postgresql-service/postgresql-service +examples/prometheus-service/prometheus-service +examples/redis-service/redis-service +examples/httpclient-service/httpclient-service From 9145b1096ca72ddd2720c35a7fb2b676ed92e295 Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 17:30:24 -0600 Subject: [PATCH 7/8] docs: make README and doc.go truthful against the actual API - Quick Start now compiles: two-value framework.New, real postgresql.NewBundle(config) - drop claimed-but-unbuilt features (Redis Streams event publishing, transaction patterns) in favor of what ships - remove phantom Adapters layer; fix dead doc links; clarify env config comes via configloader bundle; note gRPC server is registrar-gated Co-Authored-By: Claude Fable 5 --- README.md | 39 +++++++++++++++++++++++---------------- doc.go | 6 ++++-- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 1c2d99c..717744b 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ Forge is inspired by DropWizard but designed specifically for Go's strengths - i - **Observability Built-in**: OpenTelemetry tracing, structured logging, Prometheus metrics - **Health Checks**: Comprehensive liveness and readiness checks - **Graceful Lifecycle**: Sophisticated startup and shutdown orchestration -- **Configuration Management**: Environment-based config with validation -- **Database Integration**: Transaction-safe PostgreSQL patterns -- **Event Publishing**: Redis Streams integration +- **Configuration Management**: Validated config with YAML + env overrides via the configloader bundle +- **Database Integration**: PostgreSQL connection pooling with health checks +- **Redis Integration**: Caching, pub/sub, distributed locks, and rate limiting +- **Resilient HTTP Client**: Circuit breaker, retries, and backoff built in - **Security First**: No hardcoded credentials, explicit validation requirements ## Quick Start @@ -24,21 +25,29 @@ package main import ( "context" - "github.com/datariot/forge/framework" + "log" + "github.com/datariot/forge/bundles/postgresql" + "github.com/datariot/forge/config" + "github.com/datariot/forge/framework" ) func main() { - cfg := MustLoadConfig() + cfg := config.DefaultBaseConfig() + cfg.ServiceName = "my-service" - myComponent := NewMyComponent(cfg) + pgConfig := postgresql.DefaultConfig() + pgConfig.DatabaseURL = "postgres://user:pass@localhost:5432/mydb" - app := framework.New( - framework.WithConfig(&cfg.BaseConfig), + app, err := framework.New( + framework.WithConfig(&cfg), framework.WithVersion("1.0.0"), - framework.WithComponent(myComponent), - framework.WithBundle(postgresql.Bundle()), + framework.WithComponent(NewMyComponent(&cfg)), + framework.WithBundle(postgresql.NewBundle(pgConfig)), ) + if err != nil { + log.Fatal(err) + } if err := app.Run(context.Background()); err != nil { log.Fatal(err) @@ -112,17 +121,15 @@ See [TESTING.md](TESTING.md) for comprehensive testing strategy. Forge follows Clean Architecture principles: - **Framework**: Core application lifecycle and interfaces -- **Bundles**: Pre-built integrations (PostgreSQL, Redis, Prometheus, etc.) +- **Bundles**: Pre-built integrations (PostgreSQL, Redis, JWT, HTTP client, Prometheus, configloader) - **Components**: Your business logic implementing framework interfaces -- **Adapters**: Infrastructure integrations -- **Config**: Environment-driven configuration management +- **Config**: Common service configuration with validation ## Documentation - [Getting Started](docs/getting-started.md) -- [Component Development](docs/components.md) -- [Configuration Guide](docs/configuration.md) -- [Health Checks](docs/health-checks.md) +- [API Reference](docs/api-reference.md) +- [Bundles Guide](docs/bundles.md) - [Examples](examples/) - [Development Guide](CLAUDE.md) diff --git a/doc.go b/doc.go index f071729..4baa5cd 100644 --- a/doc.go +++ b/doc.go @@ -74,14 +74,16 @@ // // Forge automatically provides these HTTP endpoints: // -// - :8080 - gRPC server (configurable via GRPC_ADDR) +// - :8080 - gRPC server (configurable via BaseConfig.GRPCAddr; only started +// when gRPC registrars are configured) // - :8081/health - Combined health status // - :8081/health/live - Liveness probe // - :8081/health/ready - Readiness probe // // # Configuration // -// All configuration is environment-driven with sensible defaults: +// BaseConfig provides sensible defaults; set fields in code, or add the +// configloader bundle to load YAML files with environment variable overrides: // // SERVICE_NAME=my-service // APP_ENV=production From 0c5b54cf406d7c9ab17cf25b561101530af409d0 Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 17:45:39 -0600 Subject: [PATCH 8/8] fix: resolve all golangci-lint v2 findings and modernize lint CI - CI lint job was failing since April: golangci-lint v1.55.2 (Go 1.21) cannot typecheck a Go 1.25 module; switch to golangci-lint-action@v8 pinned to v2.12.2 - errcheck: handle or explicitly ignore all unchecked Close/Write/Stop/Register returns (prod code returns/logs where meaningful; tests assert) - staticcheck: drop deprecated PreferServerCipherSuites, De Morgan simplifications, single-case select to plain receive, nil ctx -> context.TODO in tests, error string casing - unused: remove dead configloader Bundle.mu field Co-Authored-By: Claude Fable 5 --- .github/workflows/test.yml | 9 +++--- bundles/configloader/bundle.go | 9 +++--- bundles/configloader/bundle_test.go | 4 +-- bundles/httpclient/bundle.go | 7 ++--- bundles/httpclient/bundle_test.go | 14 +++++---- bundles/jwt/bundle.go | 6 ++-- bundles/postgresql/bundle.go | 6 ++-- bundles/prometheus/bundle.go | 12 ++++---- bundles/redis/bundle.go | 8 +++-- bundles/redis/bundle_test.go | 4 +-- framework/app.go | 24 +++++++-------- framework/app_http_test.go | 2 +- framework/app_lifecycle_test.go | 20 +++++++++---- framework/http_test.go | 2 +- health/registry_test.go | 46 +++++++++++++++++++++-------- testutil/testutil.go | 4 +-- testutil/testutil_test.go | 17 ++++++----- 17 files changed, 115 insertions(+), 79 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e6fca2..d00fde0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,12 +74,11 @@ jobs: - name: Run go vet run: go vet ./... - - name: Install golangci-lint - run: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2 - - name: Run golangci-lint - run: golangci-lint run --timeout=5m + uses: golangci/golangci-lint-action@v8 + with: + version: v2.12.2 + args: --timeout=5m build: name: Build Examples diff --git a/bundles/configloader/bundle.go b/bundles/configloader/bundle.go index 11ca332..91c4ac1 100644 --- a/bundles/configloader/bundle.go +++ b/bundles/configloader/bundle.go @@ -176,7 +176,6 @@ type Bundle struct { watcher *fsnotify.Watcher watchedFiles []string logger zerolog.Logger - mu sync.RWMutex } // NewBundle creates a new configuration loading bundle. @@ -242,7 +241,9 @@ func (b *Bundle) Stop(ctx context.Context) error { return err case <-ctx.Done(): // Force close after timeout - b.watcher.Close() + if closeErr := b.watcher.Close(); closeErr != nil { + b.logger.Error().Err(closeErr).Msg("failed to force-close config watcher") + } return fmt.Errorf("config watcher close timed out: %w", ctx.Err()) } } @@ -301,7 +302,7 @@ func (l *Loader) Load(dest interface{}) (*LoadResult, error) { } destValue := reflect.ValueOf(dest) - if destValue.Kind() != reflect.Ptr || destValue.Elem().Kind() != reflect.Struct { + if destValue.Kind() != reflect.Pointer || destValue.Elem().Kind() != reflect.Struct { return nil, stderrors.New("destination must be a pointer to a struct") } @@ -395,7 +396,7 @@ func (l *Loader) loadFromFile(filename string, dest interface{}) error { if err != nil { return fmt.Errorf("failed to open config file: %w", err) } - defer file.Close() + defer func() { _ = file.Close() }() // Use io.LimitReader to enforce size limits limitedReader := io.LimitReader(file, l.config.MaxFileSize) diff --git a/bundles/configloader/bundle_test.go b/bundles/configloader/bundle_test.go index 64412f4..9eecbed 100644 --- a/bundles/configloader/bundle_test.go +++ b/bundles/configloader/bundle_test.go @@ -665,12 +665,12 @@ func TestLoadFromFile_RelativePath(t *testing.T) { if err != nil { t.Fatalf("os.CreateTemp: %v", err) } - defer os.Remove(tmp.Name()) + defer func() { _ = os.Remove(tmp.Name()) }() if _, err := tmp.WriteString("key: value\n"); err != nil { t.Fatalf("write: %v", err) } - tmp.Close() + _ = tmp.Close() // Chmod to 0644 to satisfy RequiredFileMode default. if err := os.Chmod(tmp.Name(), 0o644); err != nil { diff --git a/bundles/httpclient/bundle.go b/bundles/httpclient/bundle.go index 8cb8410..aabb794 100644 --- a/bundles/httpclient/bundle.go +++ b/bundles/httpclient/bundle.go @@ -334,8 +334,7 @@ func (b *Bundle) Initialize(app *framework.App) error { // Enforce secure TLS configuration tlsConfig.MinVersion = tls.VersionTLS12 // Require TLS 1.2 minimum - tlsConfig.PreferServerCipherSuites = true - tlsConfig.InsecureSkipVerify = false // Never skip certificate verification + tlsConfig.InsecureSkipVerify = false // Never skip certificate verification // Set secure cipher suites tlsConfig.CipherSuites = []uint16{ @@ -603,7 +602,7 @@ func (c *Client) executeRequest(ctx context.Context, method, url string, body in } return backoff.Permanent(err) // Non-retryable error } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() // Read response body respBody, err := io.ReadAll(resp.Body) @@ -873,7 +872,7 @@ func (c *Client) HealthCheck(ctx context.Context, healthURL string) error { if err != nil { return fmt.Errorf("health check request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("health check failed: HTTP %d %s", resp.StatusCode, resp.Status) diff --git a/bundles/httpclient/bundle_test.go b/bundles/httpclient/bundle_test.go index e198d55..b93dd73 100644 --- a/bundles/httpclient/bundle_test.go +++ b/bundles/httpclient/bundle_test.go @@ -107,7 +107,9 @@ func TestBundle_Stop(t *testing.T) { bundle := NewBundle(cfg) // Initialize first - bundle.Initialize(nil) + if err := bundle.Initialize(nil); err != nil { + t.Fatalf("Initialize failed: %v", err) + } ctx := context.Background() if err := bundle.Stop(ctx); err != nil { @@ -439,7 +441,7 @@ func TestClient_Get_Success(t *testing.T) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"name":"test"}`)) + _, _ = w.Write([]byte(`{"name":"test"}`)) })) defer server.Close() @@ -457,7 +459,7 @@ func TestClient_Get_Success(t *testing.T) { func TestClient_Get_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) - w.Write([]byte(`{"error":"not found"}`)) + _, _ = w.Write([]byte(`{"error":"not found"}`)) })) defer server.Close() @@ -477,7 +479,7 @@ func TestClient_Post_Success(t *testing.T) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - w.Write([]byte(`{"id":"123"}`)) + _, _ = w.Write([]byte(`{"id":"123"}`)) })) defer server.Close() @@ -525,7 +527,7 @@ func TestClient_RawRequest(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("expected 200, got %d", resp.StatusCode) } @@ -569,7 +571,7 @@ func TestClient_Put_Success(t *testing.T) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"updated":true}`)) + _, _ = w.Write([]byte(`{"updated":true}`)) })) defer server.Close() diff --git a/bundles/jwt/bundle.go b/bundles/jwt/bundle.go index 25e0b2d..a06acfb 100644 --- a/bundles/jwt/bundle.go +++ b/bundles/jwt/bundle.go @@ -267,17 +267,17 @@ func (b *Bundle) validateClaims(claims *ServiceClaims) error { now := time.Now() // Check expiration with clock skew - if claims.ExpiresAt != nil && now.After(claims.ExpiresAt.Time.Add(b.config.ClockSkew)) { + if claims.ExpiresAt != nil && now.After(claims.ExpiresAt.Add(b.config.ClockSkew)) { return errors.ErrInvalidCredential.WithMessage("token has expired") } // Check not before with clock skew (subtract clock skew to be more permissive) - if claims.NotBefore != nil && now.Before(claims.NotBefore.Time.Add(-b.config.ClockSkew)) { + if claims.NotBefore != nil && now.Before(claims.NotBefore.Add(-b.config.ClockSkew)) { return errors.ErrInvalidCredential.WithMessage("token not yet valid") } // Validate audience - if claims.Audience == nil || len(claims.Audience) == 0 { + if len(claims.Audience) == 0 { return errors.ErrInvalidCredential.WithMessage("token missing audience") } diff --git a/bundles/postgresql/bundle.go b/bundles/postgresql/bundle.go index 6a85907..c632b88 100644 --- a/bundles/postgresql/bundle.go +++ b/bundles/postgresql/bundle.go @@ -159,7 +159,7 @@ func (b *Bundle) Initialize(app *framework.App) error { defer cancel() if err := db.PingContext(ctx); err != nil { - db.Close() + _ = db.Close() return errors.ErrRepositoryUnavailable.WithMessage("failed to ping PostgreSQL database").WithCause(err) } @@ -192,7 +192,9 @@ func (b *Bundle) Stop(ctx context.Context) error { return err case <-ctx.Done(): // Force close after timeout - b.db.Close() + if closeErr := b.db.Close(); closeErr != nil { + return fmt.Errorf("database connection close timed out: %w (close error: %w)", ctx.Err(), closeErr) + } return fmt.Errorf("database connection close timed out: %w", ctx.Err()) } } diff --git a/bundles/prometheus/bundle.go b/bundles/prometheus/bundle.go index 16fc76f..65ebe24 100644 --- a/bundles/prometheus/bundle.go +++ b/bundles/prometheus/bundle.go @@ -667,15 +667,15 @@ func isValidLabelName(name string) bool { // First character must be letter or underscore first := name[0] - if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_') { + if (first < 'a' || first > 'z') && (first < 'A' || first > 'Z') && first != '_' { return false } // Remaining characters must be letters, digits, or underscores for i := 1; i < len(name); i++ { char := name[i] - if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || - (char >= '0' && char <= '9') || char == '_') { + if (char < 'a' || char > 'z') && (char < 'A' || char > 'Z') && + (char < '0' || char > '9') && char != '_' { return false } } @@ -825,15 +825,15 @@ func isValidMetricName(name string) bool { // First character must be letter or underscore first := name[0] - if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_' || first == ':') { + if (first < 'a' || first > 'z') && (first < 'A' || first > 'Z') && first != '_' && first != ':' { return false } // Remaining characters must be letters, digits, underscores, or colons for i := 1; i < len(name); i++ { char := name[i] - if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || - (char >= '0' && char <= '9') || char == '_' || char == ':') { + if (char < 'a' || char > 'z') && (char < 'A' || char > 'Z') && + (char < '0' || char > '9') && char != '_' && char != ':' { return false } } diff --git a/bundles/redis/bundle.go b/bundles/redis/bundle.go index bedd4fd..f18f173 100644 --- a/bundles/redis/bundle.go +++ b/bundles/redis/bundle.go @@ -291,7 +291,7 @@ func (b *Bundle) Initialize(app *framework.App) error { defer cancel() if err := b.client.Ping(ctx).Err(); err != nil { - b.client.Close() + _ = b.client.Close() return errors.ErrRepositoryUnavailable.WithMessage( "failed to connect to Redis at %s", b.config.SanitizedRedisURL(), ).WithCause(err) @@ -338,7 +338,9 @@ func (b *Bundle) Stop(ctx context.Context) error { return err case <-ctx.Done(): // Force close after timeout - b.client.Close() + if closeErr := b.client.Close(); closeErr != nil { + return fmt.Errorf("redis connection close timed out: %w (close error: %w)", ctx.Err(), closeErr) + } return fmt.Errorf("redis connection close timed out: %w", ctx.Err()) } } @@ -408,7 +410,7 @@ func (c *RedisHealthCheck) Readiness(ctx context.Context) error { } if err := c.client.Ping(ctx).Err(); err != nil { - return fmt.Errorf("Redis ping failed: %w", err) + return fmt.Errorf("redis ping failed: %w", err) } return nil diff --git a/bundles/redis/bundle_test.go b/bundles/redis/bundle_test.go index 75c2239..a9098d1 100644 --- a/bundles/redis/bundle_test.go +++ b/bundles/redis/bundle_test.go @@ -23,11 +23,11 @@ func newLiveTestClient(t *testing.T) goredis.UniversalClient { defer cancel() if err := client.Ping(ctx).Err(); err != nil { - client.Close() + _ = client.Close() t.Skipf("skipping: no Redis available at localhost:6379: %v", err) } - t.Cleanup(func() { client.Close() }) + t.Cleanup(func() { _ = client.Close() }) return client } diff --git a/framework/app.go b/framework/app.go index b5539b7..5d3668e 100644 --- a/framework/app.go +++ b/framework/app.go @@ -498,13 +498,11 @@ func (a *App) Run(ctx context.Context) error { signalCtx, signalCancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer signalCancel() - select { - case <-signalCtx.Done(): - if ctx.Err() != nil { - logger.Info().Msg("Context cancelled") - } else { - logger.Info().Msg("Received shutdown signal") - } + <-signalCtx.Done() + if ctx.Err() != nil { + logger.Info().Msg("Context cancelled") + } else { + logger.Info().Msg("Received shutdown signal") } // Shutdown the application @@ -866,9 +864,9 @@ func (a *App) handleHealth(w http.ResponseWriter, r *http.Request) { w.WriteHeader(status.HTTPStatus()) if data, err := status.JSON(); err == nil { - w.Write(data) + _, _ = w.Write(data) } else { - w.Write([]byte(`{"status":"error","message":"failed to serialize health status"}`)) + _, _ = w.Write([]byte(`{"status":"error","message":"failed to serialize health status"}`)) } } @@ -881,9 +879,9 @@ func (a *App) handleReady(w http.ResponseWriter, r *http.Request) { w.WriteHeader(status.HTTPStatus()) if data, err := status.JSON(); err == nil { - w.Write(data) + _, _ = w.Write(data) } else { - w.Write([]byte(`{"status":"error","message":"failed to serialize readiness status"}`)) + _, _ = w.Write([]byte(`{"status":"error","message":"failed to serialize readiness status"}`)) } } @@ -896,9 +894,9 @@ func (a *App) handleLive(w http.ResponseWriter, r *http.Request) { w.WriteHeader(status.HTTPStatus()) if data, err := status.JSON(); err == nil { - w.Write(data) + _, _ = w.Write(data) } else { - w.Write([]byte(`{"status":"error","message":"failed to serialize liveness status"}`)) + _, _ = w.Write([]byte(`{"status":"error","message":"failed to serialize liveness status"}`)) } } diff --git a/framework/app_http_test.go b/framework/app_http_test.go index 16ebe93..d6cfaf5 100644 --- a/framework/app_http_test.go +++ b/framework/app_http_test.go @@ -113,7 +113,7 @@ func TestApp_WithGRPCRegistrar(t *testing.T) { if err := app.Start(ctx); err != nil { t.Fatalf("Failed to start app: %v", err) } - defer app.Stop(context.Background()) + defer func() { _ = app.Stop(context.Background()) }() // Verify registrar was called if !registrarCalled { diff --git a/framework/app_lifecycle_test.go b/framework/app_lifecycle_test.go index 4d908f2..1974538 100644 --- a/framework/app_lifecycle_test.go +++ b/framework/app_lifecycle_test.go @@ -125,7 +125,9 @@ func TestApp_Lifecycle_ComponentStartOrder(t *testing.T) { // Cleanup stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second) defer stopCancel() - app.Stop(stopCtx) + if err := app.Stop(stopCtx); err != nil { + t.Errorf("Failed to stop app: %v", err) + } } // TestApp_Lifecycle_ComponentStopReverseOrder tests components stop in reverse order @@ -237,7 +239,9 @@ func TestApp_Lifecycle_BundleInitialization(t *testing.T) { // Cleanup stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second) defer stopCancel() - app.Stop(stopCtx) + if err := app.Stop(stopCtx); err != nil { + t.Errorf("Failed to stop app: %v", err) + } if !bundle.stopped { t.Error("Bundle should be stopped after Stop()") @@ -278,7 +282,9 @@ func TestApp_Lifecycle_StartupHooks(t *testing.T) { // Cleanup stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second) defer stopCancel() - app.Stop(stopCtx) + if err := app.Stop(stopCtx); err != nil { + t.Errorf("Failed to stop app: %v", err) + } } // TestApp_Lifecycle_ShutdownHooks tests shutdown hook execution @@ -367,7 +373,9 @@ func TestApp_Lifecycle_HealthCheckRegistration(t *testing.T) { // Cleanup stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second) defer stopCancel() - app.Stop(stopCtx) + if err := app.Stop(stopCtx); err != nil { + t.Errorf("Failed to stop app: %v", err) + } } // TestApp_Lifecycle_MultipleStartCallsFail tests that calling Start() twice fails @@ -399,7 +407,9 @@ func TestApp_Lifecycle_MultipleStartCallsFail(t *testing.T) { // Cleanup stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second) defer stopCancel() - app.Stop(stopCtx) + if err := app.Stop(stopCtx); err != nil { + t.Errorf("Failed to stop app: %v", err) + } } // TestApp_Lifecycle_Uptime tests uptime tracking diff --git a/framework/http_test.go b/framework/http_test.go index 1ac41b6..c2dfae1 100644 --- a/framework/http_test.go +++ b/framework/http_test.go @@ -133,7 +133,7 @@ func TestResponseWriter_Write_PreservesExistingStatus(t *testing.T) { rec := httptest.NewRecorder() rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusCreated} - rw.Write([]byte("data")) + _, _ = rw.Write([]byte("data")) if rw.statusCode != http.StatusCreated { t.Errorf("expected status 201 preserved, got %d", rw.statusCode) } diff --git a/health/registry_test.go b/health/registry_test.go index bda6d05..1f62d70 100644 --- a/health/registry_test.go +++ b/health/registry_test.go @@ -67,7 +67,9 @@ func TestRegistry_Unregister(t *testing.T) { check := NewAlwaysHealthyCheck("test-check") config := DefaultCheckConfig("test-check") - registry.Register(check, config) + if err := registry.Register(check, config); err != nil { + t.Fatalf("Failed to register check: %v", err) + } registry.Unregister("test-check") // Should be able to re-register after unregister @@ -100,7 +102,9 @@ func TestRegistry_CheckLiveness(t *testing.T) { registry := NewRegistry(nil) healthyCheck := NewAlwaysHealthyCheck("healthy-check") - registry.Register(healthyCheck, DefaultCheckConfig("healthy-check")) + if err := registry.Register(healthyCheck, DefaultCheckConfig("healthy-check")); err != nil { + t.Fatalf("Failed to register check: %v", err) + } ctx := context.Background() status := registry.CheckLiveness(ctx) @@ -126,7 +130,9 @@ func TestRegistry_CheckLiveness_FailingCheck(t *testing.T) { config := DefaultCheckConfig("failing-check") config.Required = true - registry.Register(failingCheck, config) + if err := registry.Register(failingCheck, config); err != nil { + t.Fatalf("Failed to register check: %v", err) + } ctx := context.Background() status := registry.CheckLiveness(ctx) @@ -152,7 +158,9 @@ func TestRegistry_CheckLiveness_OptionalFailingCheck(t *testing.T) { config := DefaultCheckConfig("optional-failing") config.Required = false - registry.Register(failingCheck, config) + if err := registry.Register(failingCheck, config); err != nil { + t.Fatalf("Failed to register check: %v", err) + } ctx := context.Background() status := registry.CheckLiveness(ctx) @@ -176,7 +184,9 @@ func TestRegistry_CheckReadiness(t *testing.T) { registry.SetReady(true) healthyCheck := NewAlwaysHealthyCheck("healthy-check") - registry.Register(healthyCheck, DefaultCheckConfig("healthy-check")) + if err := registry.Register(healthyCheck, DefaultCheckConfig("healthy-check")); err != nil { + t.Fatalf("Failed to register check: %v", err) + } ctx := context.Background() status := registry.CheckReadiness(ctx) @@ -196,7 +206,9 @@ func TestRegistry_CheckReadiness_NotMarkedReady(t *testing.T) { // Don't call SetReady(true) healthyCheck := NewAlwaysHealthyCheck("healthy-check") - registry.Register(healthyCheck, DefaultCheckConfig("healthy-check")) + if err := registry.Register(healthyCheck, DefaultCheckConfig("healthy-check")); err != nil { + t.Fatalf("Failed to register check: %v", err) + } ctx := context.Background() status := registry.CheckReadiness(ctx) @@ -216,7 +228,9 @@ func TestRegistry_CheckHealth(t *testing.T) { registry.SetReady(true) healthyCheck := NewAlwaysHealthyCheck("healthy-check") - registry.Register(healthyCheck, DefaultCheckConfig("healthy-check")) + if err := registry.Register(healthyCheck, DefaultCheckConfig("healthy-check")); err != nil { + t.Fatalf("Failed to register check: %v", err) + } ctx := context.Background() status := registry.CheckHealth(ctx) @@ -237,7 +251,9 @@ func TestRegistry_ConcurrentChecks(t *testing.T) { name: "slow-check-" + string(rune('A'+i)), delay: 100 * time.Millisecond, } - registry.Register(check, DefaultCheckConfig(check.name)) + if err := registry.Register(check, DefaultCheckConfig(check.name)); err != nil { + t.Fatalf("Failed to register check: %v", err) + } } ctx := context.Background() @@ -271,7 +287,9 @@ func TestRegistry_CheckTimeout(t *testing.T) { config := DefaultCheckConfig("very-slow-check") config.Required = true - registry.Register(slowCheck, config) + if err := registry.Register(slowCheck, config); err != nil { + t.Fatalf("Failed to register check: %v", err) + } // Use context timeout to control check duration ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) @@ -296,10 +314,12 @@ func TestRegistry_MixedChecks(t *testing.T) { registry.SetReady(true) // Required healthy check - registry.Register( + if err := registry.Register( NewAlwaysHealthyCheck("required-healthy"), DefaultCheckConfig("required-healthy"), - ) + ); err != nil { + t.Fatalf("Failed to register check: %v", err) + } // Optional failing check failingCheck := &testCheck{ @@ -309,7 +329,9 @@ func TestRegistry_MixedChecks(t *testing.T) { } failingConfig := DefaultCheckConfig("optional-failing") failingConfig.Required = false - registry.Register(failingCheck, failingConfig) + if err := registry.Register(failingCheck, failingConfig); err != nil { + t.Fatalf("Failed to register check: %v", err) + } ctx := context.Background() status := registry.CheckLiveness(ctx) diff --git a/testutil/testutil.go b/testutil/testutil.go index ff13439..8a58f23 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -182,7 +182,7 @@ func getRandomPort() string { if err != nil { return ":0" // Fallback to any port } - defer listener.Close() + defer func() { _ = listener.Close() }() addr := listener.Addr().(*net.TCPAddr) return fmt.Sprintf(":%d", addr.Port) @@ -221,7 +221,7 @@ func (c *TestHTTPClient) CheckHealth(t *testing.T) { t.Helper() resp := c.Get(t, "/health") - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("Expected health check to return 200, got %d", resp.StatusCode) diff --git a/testutil/testutil_test.go b/testutil/testutil_test.go index 11bd3bc..a3a35b6 100644 --- a/testutil/testutil_test.go +++ b/testutil/testutil_test.go @@ -1,6 +1,7 @@ package testutil import ( + "context" "errors" "net/http" "net/http/httptest" @@ -30,14 +31,14 @@ func TestNewTestComponent(t *testing.T) { func TestTestComponent_StartStop(t *testing.T) { comp := NewTestComponent("test") - if err := comp.Start(nil); err != nil { + if err := comp.Start(context.TODO()); err != nil { t.Errorf("expected no error, got: %v", err) } if !comp.StartCalled { t.Error("StartCalled should be true after Start()") } - if err := comp.Stop(nil); err != nil { + if err := comp.Stop(context.TODO()); err != nil { t.Errorf("expected no error, got: %v", err) } if !comp.StopCalled { @@ -49,7 +50,7 @@ func TestTestComponent_StartError(t *testing.T) { comp := NewTestComponent("test") comp.StartError = errors.New("start failed") - if err := comp.Start(nil); err == nil { + if err := comp.Start(context.TODO()); err == nil { t.Error("expected error from Start(), got nil") } } @@ -58,7 +59,7 @@ func TestTestComponent_StopError(t *testing.T) { comp := NewTestComponent("test") comp.StopError = errors.New("stop failed") - if err := comp.Stop(nil); err == nil { + if err := comp.Stop(context.TODO()); err == nil { t.Error("expected error from Stop(), got nil") } } @@ -83,7 +84,7 @@ func TestTestBundle_InitializeStop(t *testing.T) { t.Error("InitCalled should be true after Initialize()") } - if err := bundle.Stop(nil); err != nil { + if err := bundle.Stop(context.TODO()); err != nil { t.Errorf("expected no error, got: %v", err) } if !bundle.StopCalled { @@ -144,7 +145,7 @@ func TestNewTestServer(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"ok"}`)) + _, _ = w.Write([]byte(`{"status":"ok"}`)) }) server := NewTestServer(handler) @@ -167,7 +168,7 @@ func TestTestHTTPClient_Get(t *testing.T) { client := NewTestHTTPClient(testServer.URL) resp := client.Get(t, "/") - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("expected 200, got %d", resp.StatusCode) @@ -178,7 +179,7 @@ func TestTestHTTPClient_CheckHealth(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"healthy"}`)) + _, _ = w.Write([]byte(`{"status":"healthy"}`)) })) defer testServer.Close()