From e9bc8524c0cff639c96fa76eab8761d52ba55c92 Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 6 Jul 2026 21:26:37 -0600 Subject: [PATCH] refactor!: idiomatic API naming (strip Get prefix, HealthStatus->Report, errors->forgeerrors, interface{}->any) Pre-1.0 breaking cleanup of API naming conventions across the framework. BREAKING CHANGES / migration notes: - Get* getters renamed to drop the redundant prefix (idiomatic Go): - framework.ObservabilityManager: GetTracer -> Tracer, GetMeter -> Meter - health.Registry: GetRegisteredChecks -> RegisteredChecks, GetCheckConfig -> CheckConfig - health.Report (fka HealthStatus): GetHealthySummary -> HealthySummary - bundles/prometheus.Bundle: GetMetricsHandler -> MetricsHandler, GetSecureMetricsHandler -> SecureMetricsHandler - bundles/configloader.Loader: GetConfigInfo -> ConfigInfo - bundles/httpclient.Client: GetCircuitBreakerState -> CircuitBreakerState, GetCircuitBreakerCounts -> CircuitBreakerCounts - bundles/jwt package funcs: GetServiceID -> ServiceID, GetServiceName -> ServiceName - bundles/httpclient.CredentialProvider's GetAPIKey/GetJWTToken are intentionally left unchanged: they are context-taking, error-returning fetch operations, not field accessors. - health.HealthStatus renamed to health.Report to remove the package/type stutter; constructors renamed to match: NewHealthyStatus -> NewHealthyReport, NewUnhealthyStatus -> NewUnhealthyReport, NewUnknownStatus -> NewUnknownReport. health.Status, health.StatusResponse/NewStatusResponse, health.HealthySummary, and health.CheckResult are unchanged. - errors/ package renamed to forgeerrors/ (import path github.com/datariot/forge/errors -> github.com/datariot/forge/forgeerrors) so it stops shadowing the stdlib errors package. The 6 bundles that used it (jwt, postgresql, httpclient, redis, configloader, prometheus) now import forge errors as forgeerrors and stdlib errors as plain "errors", dropping the stderrors alias where it existed. - interface{} replaced with any (gofmt -r) across the module and all example modules. All call sites, tests, examples, and docs (README.md, doc.go, CLAUDE.md, docs/*.md) updated accordingly. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 4 +- bundles/configloader/bundle.go | 52 ++++++------ bundles/configloader/bundle_test.go | 12 +-- bundles/httpclient/bundle.go | 60 ++++++------- bundles/httpclient/bundle_test.go | 8 +- bundles/jwt/bundle.go | 72 ++++++++-------- bundles/jwt/bundle_test.go | 34 ++++---- bundles/postgresql/bundle.go | 20 ++--- bundles/postgresql/bundle_test.go | 6 +- bundles/prometheus/bundle.go | 38 ++++----- bundles/prometheus/bundle_middleware_test.go | 8 +- bundles/prometheus/bundle_test.go | 8 +- bundles/redis/bundle.go | 32 +++---- bundles/redis/bundle_test.go | 10 +-- doc.go | 2 +- docs/api-reference.md | 16 ++-- examples/config-service/main.go | 16 ++-- examples/httpclient-service/main.go | 32 +++---- examples/jwt-service/main.go | 8 +- examples/prometheus-service/main.go | 10 +-- examples/redis-service/main.go | 30 +++---- {errors => forgeerrors}/errors.go | 20 ++--- {errors => forgeerrors}/errors_test.go | 2 +- framework/app_http_test.go | 4 +- framework/app_middleware_test.go | 4 +- framework/app_test.go | 6 +- framework/http.go | 2 +- framework/logging.go | 12 +-- framework/observability.go | 8 +- health/registry.go | 52 ++++++------ health/registry_test.go | 16 ++-- health/status.go | 54 ++++++------ health/status_test.go | 88 ++++++++++---------- 33 files changed, 373 insertions(+), 373 deletions(-) rename {errors => forgeerrors}/errors.go (92%) rename {errors => forgeerrors}/errors_test.go (99%) diff --git a/CLAUDE.md b/CLAUDE.md index 53b4b3b..76d252f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,7 +143,7 @@ Each bundle provides: - Graceful cleanup in `Stop()` method - Production-ready defaults -### Error Handling (`errors/`) +### Error Handling (`forgeerrors/`) Domain error patterns and classification utilities for consistent error handling across services. ### Observability (`framework/observability.go`, `framework/logging.go`) @@ -230,7 +230,7 @@ forge/ ├── config/ # Configuration management (BaseConfig, validation, env detection) ├── health/ # Health check system (registry, status, concurrent execution) ├── bundles/ # Pre-built integrations (postgresql, redis, jwt, etc.) -├── errors/ # Error handling utilities +├── forgeerrors/ # Error handling utilities ├── examples/ # Example service implementations ├── docs/ # GitHub Pages documentation site (Jekyll) ├── testutil/ # Testing utilities (assertions, test configs, zerolog test logger) diff --git a/bundles/configloader/bundle.go b/bundles/configloader/bundle.go index 401233a..57129e5 100644 --- a/bundles/configloader/bundle.go +++ b/bundles/configloader/bundle.go @@ -83,7 +83,7 @@ package configloader import ( "context" "encoding/json" - stderrors "errors" + "errors" "fmt" "io" "net/url" @@ -99,7 +99,7 @@ import ( "github.com/rs/zerolog" "gopkg.in/yaml.v3" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" "github.com/datariot/forge/framework" ) @@ -154,13 +154,13 @@ func DefaultConfig() Config { // Validate validates the configuration loader settings. func (c *Config) Validate() error { if len(c.ConfigPaths) == 0 { - return stderrors.New("at least one config path must be specified") + return errors.New("at least one config path must be specified") } // Validate config paths for _, path := range c.ConfigPaths { if path == "" { - return stderrors.New("config path cannot be empty") + return errors.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) @@ -194,7 +194,7 @@ func (b *Bundle) Name() string { // Initialize sets up the configuration loader. func (b *Bundle) Initialize(app *framework.App) error { if err := b.config.Validate(); err != nil { - return errors.ErrInvalidConfiguration.WithMessage("Configuration loader validation failed").WithCause(err) + return forgeerrors.ErrInvalidConfiguration.WithMessage("Configuration loader validation failed").WithCause(err) } if app != nil { @@ -283,7 +283,7 @@ func (b *Bundle) initializeWatcher() error { type Loader struct { config Config loadedFrom string - changeCallbacks []func(interface{}) + changeCallbacks []func(any) mu sync.RWMutex } @@ -297,14 +297,14 @@ type LoadResult struct { } // Load loads configuration into the provided struct from multiple sources. -func (l *Loader) Load(dest interface{}) (*LoadResult, error) { +func (l *Loader) Load(dest any) (*LoadResult, error) { if dest == nil { - return nil, stderrors.New("destination cannot be nil") + return nil, errors.New("destination cannot be nil") } destValue := reflect.ValueOf(dest) if destValue.Kind() != reflect.Pointer || destValue.Elem().Kind() != reflect.Struct { - return nil, stderrors.New("destination must be a pointer to a struct") + return nil, errors.New("destination must be a pointer to a struct") } result := &LoadResult{ @@ -365,7 +365,7 @@ func (l *Loader) findConfigFile() (string, error) { } // loadFromFile loads configuration from a file based on its extension with security validation. -func (l *Loader) loadFromFile(filename string, dest interface{}) error { +func (l *Loader) loadFromFile(filename string, dest any) error { // Validate file path for security if err := l.validateFilePath(filename); err != nil { return fmt.Errorf("invalid config file path: %w", err) @@ -418,7 +418,7 @@ func (l *Loader) loadFromFile(filename string, dest interface{}) error { } // loadYAML loads configuration from YAML data. -func (l *Loader) loadYAML(data []byte, dest interface{}) error { +func (l *Loader) loadYAML(data []byte, dest any) error { if err := yaml.Unmarshal(data, dest); err != nil { return fmt.Errorf("failed to parse YAML: %w", err) } @@ -426,7 +426,7 @@ func (l *Loader) loadYAML(data []byte, dest interface{}) error { } // loadJSON loads configuration from JSON data. -func (l *Loader) loadJSON(data []byte, dest interface{}) error { +func (l *Loader) loadJSON(data []byte, dest any) error { if err := json.Unmarshal(data, dest); err != nil { return fmt.Errorf("failed to parse JSON: %w", err) } @@ -573,7 +573,7 @@ func (l *Loader) setFieldValue(field reflect.Value, value string) error { field.Set(reflect.ValueOf(values)) } } else { - return stderrors.New("unsupported slice type for field") + return errors.New("unsupported slice type for field") } default: return fmt.Errorf("unsupported field type: %s", field.Kind()) @@ -701,16 +701,16 @@ func toEnvVarName(fieldName string) string { } // OnConfigChange registers a callback for configuration changes (hot reload). -func (l *Loader) OnConfigChange(callback func(interface{})) { +func (l *Loader) OnConfigChange(callback func(any)) { l.mu.Lock() defer l.mu.Unlock() l.changeCallbacks = append(l.changeCallbacks, callback) } // StartWatching starts watching configuration files for changes. -func (b *Bundle) StartWatching(ctx context.Context, dest interface{}) error { +func (b *Bundle) StartWatching(ctx context.Context, dest any) error { if b.watcher == nil { - return stderrors.New("file watcher not initialized") + return errors.New("file watcher not initialized") } go func() { @@ -731,7 +731,7 @@ func (b *Bundle) StartWatching(ctx context.Context, dest interface{}) error { // Notify callbacks b.loader.mu.RLock() - callbacks := make([]func(interface{}), len(b.loader.changeCallbacks)) + callbacks := make([]func(any), len(b.loader.changeCallbacks)) copy(callbacks, b.loader.changeCallbacks) b.loader.mu.RUnlock() @@ -755,12 +755,12 @@ func (b *Bundle) StartWatching(ctx context.Context, dest interface{}) error { return nil } -// GetConfigInfo returns information about the loaded configuration. -func (l *Loader) GetConfigInfo() map[string]interface{} { +// ConfigInfo returns information about the loaded configuration. +func (l *Loader) ConfigInfo() map[string]any { l.mu.RLock() defer l.mu.RUnlock() - return map[string]interface{}{ + return map[string]any{ "loaded_from": l.loadedFrom, "config_paths": l.config.ConfigPaths, "env_prefix": l.config.EnvPrefix, @@ -772,7 +772,7 @@ func (l *Loader) GetConfigInfo() map[string]interface{} { } // LoadFromString loads configuration from a string (useful for testing). -func (l *Loader) LoadFromString(data, format string, dest interface{}) error { +func (l *Loader) LoadFromString(data, format string, dest any) error { switch strings.ToLower(format) { case "yaml", "yml": return l.loadYAML([]byte(data), dest) @@ -784,7 +784,7 @@ func (l *Loader) LoadFromString(data, format string, dest interface{}) error { } // MustLoad loads configuration and panics on error (useful for application startup). -func (l *Loader) MustLoad(dest interface{}) *LoadResult { +func (l *Loader) MustLoad(dest any) *LoadResult { result, err := l.Load(dest) if err != nil { panic(fmt.Sprintf("Configuration loading failed: %v", err)) @@ -793,7 +793,7 @@ func (l *Loader) MustLoad(dest interface{}) *LoadResult { } // Reload reloads configuration from all sources. -func (l *Loader) Reload(dest interface{}) (*LoadResult, error) { +func (l *Loader) Reload(dest any) (*LoadResult, error) { return l.Load(dest) // Load method already handles all sources } @@ -802,14 +802,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 stderrors.New("configuration file path cannot be empty") + return errors.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 stderrors.New("path traversal not allowed in configuration file path") + return errors.New("path traversal not allowed in configuration file path") } // Resolve relative paths to absolute @@ -858,7 +858,7 @@ func LoadConfig[T any](configPaths ...string) (*T, *LoadResult, error) { return &cfg, result, nil } -// MustLoadConfig is a convenience function that panics on configuration loading errors. +// MustLoadConfig is a convenience function that panics on configuration loading forgeerrors. func MustLoadConfig[T any](configPaths ...string) (*T, *LoadResult) { cfg, result, err := LoadConfig[T](configPaths...) if err != nil { diff --git a/bundles/configloader/bundle_test.go b/bundles/configloader/bundle_test.go index d7d090e..d3845d5 100644 --- a/bundles/configloader/bundle_test.go +++ b/bundles/configloader/bundle_test.go @@ -409,10 +409,10 @@ func TestLoaderLoadFromString_UnsupportedFormat(t *testing.T) { } } -// TestLoaderGetConfigInfo tests the GetConfigInfo method. -func TestLoaderGetConfigInfo(t *testing.T) { +// TestLoaderConfigInfo tests the ConfigInfo method. +func TestLoaderConfigInfo(t *testing.T) { l := loaderWithConfig(DefaultConfig()) - info := l.GetConfigInfo() + info := l.ConfigInfo() if info["config_paths"] == nil { t.Error("expected config_paths in info") @@ -426,7 +426,7 @@ func TestLoaderGetConfigInfo(t *testing.T) { func TestLoaderOnConfigChange(t *testing.T) { l := loaderWithConfig(DefaultConfig()) called := 0 - l.OnConfigChange(func(cfg interface{}) { + l.OnConfigChange(func(cfg any) { called++ }) if len(l.changeCallbacks) != 1 { @@ -721,7 +721,7 @@ func TestLoadFromFile_RelativePath(t *testing.T) { l := loaderWithConfig(DefaultConfig()) - var dest map[string]interface{} + var dest map[string]any if err := l.loadFromFile(relPath, &dest); err != nil { t.Errorf("loadFromFile with relative path failed: %v", err) } @@ -790,7 +790,7 @@ func TestMustLoadConfig_Panics(t *testing.T) { }() // Force a config with a required file that doesn't exist // Use a path that definitely doesn't exist - MustLoadConfig[map[string]interface{}]("/nonexistent/path/that/does/not/exist.yaml") + MustLoadConfig[map[string]any]("/nonexistent/path/that/does/not/exist.yaml") } func TestLooksLikeSensitiveData_JWTToken(t *testing.T) { diff --git a/bundles/httpclient/bundle.go b/bundles/httpclient/bundle.go index 0ebf0f2..bcf3b98 100644 --- a/bundles/httpclient/bundle.go +++ b/bundles/httpclient/bundle.go @@ -69,7 +69,7 @@ import ( "context" "crypto/tls" "encoding/json" - stderrors "errors" + "errors" "fmt" "io" "net" @@ -82,7 +82,7 @@ import ( "github.com/rs/zerolog" "github.com/sony/gobreaker" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" "github.com/datariot/forge/framework" ) @@ -113,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 "", stderrors.New("no API key configured") + return "", errors.New("no API key configured") } return p.apiKey, nil } @@ -121,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 "", stderrors.New("no JWT token configured") + return "", errors.New("no JWT token configured") } return p.jwtToken, nil } @@ -272,7 +272,7 @@ func (c *Config) Validate() error { // Validate circuit breaker configuration if c.CircuitBreakerConfig.MaxRequests == 0 { - return stderrors.New("circuit breaker max_requests must be positive") + return errors.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) @@ -312,7 +312,7 @@ func (b *Bundle) Name() string { // Initialize sets up the HTTP client with all configured features. func (b *Bundle) Initialize(app *framework.App) error { if err := b.config.Validate(); err != nil { - return errors.ErrInvalidConfiguration.WithMessage("HTTP client configuration validation failed").WithCause(err) + return forgeerrors.ErrInvalidConfiguration.WithMessage("HTTP client configuration validation failed").WithCause(err) } if app != nil { @@ -518,23 +518,23 @@ func (e *HTTPError) IsRetryableError() bool { // Common errors var ( - ErrCircuitBreakerOpen = stderrors.New("circuit breaker is open") - ErrMaxRetriesExceeded = stderrors.New("maximum retries exceeded") - ErrHostNotAllowed = stderrors.New("host not allowed") + ErrCircuitBreakerOpen = errors.New("circuit breaker is open") + ErrMaxRetriesExceeded = errors.New("maximum retries exceeded") + ErrHostNotAllowed = errors.New("host not allowed") ) // Get performs a GET request and unmarshals the response into dest. -func (c *Client) Get(ctx context.Context, path string, dest interface{}) error { +func (c *Client) Get(ctx context.Context, path string, dest any) error { return c.request(ctx, http.MethodGet, path, nil, dest) } // Post performs a POST request with the given body and unmarshals the response into dest. -func (c *Client) Post(ctx context.Context, path string, body interface{}, dest interface{}) error { +func (c *Client) Post(ctx context.Context, path string, body any, dest any) error { return c.request(ctx, http.MethodPost, path, body, dest) } // Put performs a PUT request with the given body and unmarshals the response into dest. -func (c *Client) Put(ctx context.Context, path string, body interface{}, dest interface{}) error { +func (c *Client) Put(ctx context.Context, path string, body any, dest any) error { return c.request(ctx, http.MethodPut, path, body, dest) } @@ -544,7 +544,7 @@ func (c *Client) Delete(ctx context.Context, path string) error { } // request performs an HTTP request with retries, circuit breaker, and observability. -func (c *Client) request(ctx context.Context, method, path string, body interface{}, dest interface{}) error { +func (c *Client) request(ctx context.Context, method, path string, body any, dest any) error { // Build full URL fullURL, err := c.buildURL(path) if err != nil { @@ -556,13 +556,13 @@ func (c *Client) request(ctx context.Context, method, path string, body interfac } // Execute request with circuit breaker protection - _, err = c.circuitBreaker.Execute(func() (interface{}, error) { + _, err = c.circuitBreaker.Execute(func() (any, error) { return nil, c.executeWithRetry(ctx, method, fullURL, body, dest) }) if err != nil { // Check if circuit breaker is open - if stderrors.Is(err, gobreaker.ErrOpenState) { + if errors.Is(err, gobreaker.ErrOpenState) { return ErrCircuitBreakerOpen } return err @@ -572,7 +572,7 @@ func (c *Client) request(ctx context.Context, method, path string, body interfac } // executeWithRetry executes an HTTP request with retry logic. -func (c *Client) executeWithRetry(ctx context.Context, method, url string, body interface{}, dest interface{}) error { +func (c *Client) executeWithRetry(ctx context.Context, method, url string, body any, dest any) error { operation := func() error { return c.executeRequest(ctx, method, url, body, dest) } @@ -585,7 +585,7 @@ func (c *Client) executeWithRetry(ctx context.Context, method, url string, body 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) { + if errors.Is(lastErr, context.Canceled) || errors.Is(lastErr, context.DeadlineExceeded) { return fmt.Errorf("request context ended: %w", lastErr) } return fmt.Errorf("%w: %w", ErrMaxRetriesExceeded, lastErr) @@ -595,7 +595,7 @@ func (c *Client) executeWithRetry(ctx context.Context, method, url string, body } // executeRequest executes a single HTTP request with logging and metrics. -func (c *Client) executeRequest(ctx context.Context, method, url string, body interface{}, dest interface{}) error { +func (c *Client) executeRequest(ctx context.Context, method, url string, body any, dest any) error { start := time.Now() // Prepare request body @@ -647,7 +647,7 @@ func (c *Client) executeRequest(ctx context.Context, method, url string, body in // Check if error is retryable var netErr net.Error - if stderrors.As(err, &netErr) && netErr.Timeout() { + if errors.As(err, &netErr) && netErr.Timeout() { return err // Retryable timeout error } return backoff.Permanent(err) // Non-retryable error @@ -768,7 +768,7 @@ func (c *Client) sanitizeURLForLogging(rawURL string) string { return parsedURL.String() } -// logRequestError logs HTTP request errors. +// logRequestError logs HTTP request forgeerrors. func (c *Client) logRequestError(method, url string, duration time.Duration, err error) { c.logger.Error(). Str("method", method). @@ -875,13 +875,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 stderrors.New("token cannot be empty") + return errors.New("token cannot be empty") } // Basic JWT format validation (header.payload.signature) parts := strings.Split(token, ".") if len(parts) != 3 { - return stderrors.New("invalid JWT token format") + return errors.New("invalid JWT token format") } // Additional validation would be performed by JWT bundle @@ -905,7 +905,7 @@ func getJWTTokenFromContext(ctx context.Context) string { } // EnableJWTIntegration creates a client option that automatically injects JWT tokens. -func (b *Bundle) EnableJWTIntegration(jwtBundle interface{}) error { +func (b *Bundle) EnableJWTIntegration(jwtBundle any) error { // This would be called during bundle initialization to integrate with JWT bundle // For now, JWT tokens can be manually added to context using WithJWTToken b.config.EnableJWTAuth = true @@ -939,12 +939,12 @@ func (c *Client) RawRequest(ctx context.Context, method, url string, headers map } // Execute with circuit breaker but without automatic retries - result, err := c.circuitBreaker.Execute(func() (interface{}, error) { + result, err := c.circuitBreaker.Execute(func() (any, error) { return c.httpClient.Do(req) }) if err != nil { - if stderrors.Is(err, gobreaker.ErrOpenState) { + if errors.Is(err, gobreaker.ErrOpenState) { return nil, ErrCircuitBreakerOpen } return nil, err @@ -956,7 +956,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 stderrors.New("health check URL not configured") + return errors.New("health check URL not configured") } // Perform a simple GET request to health endpoint @@ -976,12 +976,12 @@ func (c *Client) HealthCheck(ctx context.Context, healthURL string) error { return nil } -// GetCircuitBreakerState returns the current circuit breaker state. -func (c *Client) GetCircuitBreakerState() gobreaker.State { +// CircuitBreakerState returns the current circuit breaker state. +func (c *Client) CircuitBreakerState() gobreaker.State { return c.circuitBreaker.State() } -// GetCircuitBreakerCounts returns the current circuit breaker counts. -func (c *Client) GetCircuitBreakerCounts() gobreaker.Counts { +// CircuitBreakerCounts returns the current circuit breaker counts. +func (c *Client) CircuitBreakerCounts() gobreaker.Counts { return c.circuitBreaker.Counts() } diff --git a/bundles/httpclient/bundle_test.go b/bundles/httpclient/bundle_test.go index 927b121..8eb742d 100644 --- a/bundles/httpclient/bundle_test.go +++ b/bundles/httpclient/bundle_test.go @@ -554,15 +554,15 @@ func TestClient_HealthCheck_EmptyURL(t *testing.T) { } } -func TestClient_GetCircuitBreakerState(t *testing.T) { +func TestClient_CircuitBreakerState(t *testing.T) { client := newTestClient(t, "") - state := client.GetCircuitBreakerState() + state := client.CircuitBreakerState() _ = state } -func TestClient_GetCircuitBreakerCounts(t *testing.T) { +func TestClient_CircuitBreakerCounts(t *testing.T) { client := newTestClient(t, "") - counts := client.GetCircuitBreakerCounts() + counts := client.CircuitBreakerCounts() _ = counts } diff --git a/bundles/jwt/bundle.go b/bundles/jwt/bundle.go index 5739222..e0fd6cf 100644 --- a/bundles/jwt/bundle.go +++ b/bundles/jwt/bundle.go @@ -75,7 +75,7 @@ package jwt import ( "context" - stderrors "errors" + "errors" "fmt" "net/http" "path/filepath" @@ -90,7 +90,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" "github.com/datariot/forge/framework" ) @@ -146,31 +146,31 @@ func DefaultConfig() Config { // Validate validates the JWT configuration. func (c *Config) Validate() error { if len(c.SecretKey) == 0 { - return stderrors.New("jwt secret key is required") + return errors.New("jwt secret key is required") } if len(c.SecretKey) < 32 { - return stderrors.New("jwt secret key must be at least 32 bytes for security") + return errors.New("jwt secret key must be at least 32 bytes for security") } if c.Issuer == "" { - return stderrors.New("jwt issuer is required") + return errors.New("jwt issuer is required") } if c.Audience == "" { - return stderrors.New("jwt audience is required") + return errors.New("jwt audience is required") } if c.ServiceName == "" { - return stderrors.New("service name is required for JWT authentication") + return errors.New("service name is required for JWT authentication") } if c.TokenDuration <= 0 { - return stderrors.New("token duration must be positive") + return errors.New("token duration must be positive") } if c.ClockSkew < 0 { - return stderrors.New("clock skew must be non-negative") + return errors.New("clock skew must be non-negative") } return nil @@ -208,7 +208,7 @@ func (b *Bundle) Name() string { // Initialize sets up JWT authentication. func (b *Bundle) Initialize(app *framework.App) error { if err := b.config.Validate(); err != nil { - return errors.ErrInvalidConfiguration.WithMessage("JWT configuration validation failed").WithCause(err) + return forgeerrors.ErrInvalidConfiguration.WithMessage("JWT configuration validation failed").WithCause(err) } return nil @@ -247,25 +247,25 @@ func (b *Bundle) GenerateToken(serviceID, serviceName string, permissions []stri // ValidateToken validates a JWT token and returns the claims. func (b *Bundle) ValidateToken(tokenString string) (*ServiceClaims, error) { if tokenString == "" { - return nil, errors.ErrInvalidCredential.WithMessage("token is required") + return nil, forgeerrors.ErrInvalidCredential.WithMessage("token is required") } // Parse and validate token - token, err := b.parser.ParseWithClaims(tokenString, &ServiceClaims{}, func(token *jwt.Token) (interface{}, error) { + token, err := b.parser.ParseWithClaims(tokenString, &ServiceClaims{}, func(token *jwt.Token) (any, error) { return b.config.SecretKey, nil }) if err != nil { - return nil, errors.ErrInvalidCredential.WithMessage("invalid token").WithCause(err) + return nil, forgeerrors.ErrInvalidCredential.WithMessage("invalid token").WithCause(err) } if !token.Valid { - return nil, errors.ErrInvalidCredential.WithMessage("token is not valid") + return nil, forgeerrors.ErrInvalidCredential.WithMessage("token is not valid") } claims, ok := token.Claims.(*ServiceClaims) if !ok { - return nil, errors.ErrInvalidCredential.WithMessage("invalid token claims") + return nil, forgeerrors.ErrInvalidCredential.WithMessage("invalid token claims") } // Validate claims @@ -282,17 +282,17 @@ func (b *Bundle) validateClaims(claims *ServiceClaims) error { // Check expiration with clock skew if claims.ExpiresAt != nil && now.After(claims.ExpiresAt.Add(b.config.ClockSkew)) { - return errors.ErrInvalidCredential.WithMessage("token has expired") + return forgeerrors.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.Add(-b.config.ClockSkew)) { - return errors.ErrInvalidCredential.WithMessage("token not yet valid") + return forgeerrors.ErrInvalidCredential.WithMessage("token not yet valid") } // Validate audience if len(claims.Audience) == 0 { - return errors.ErrInvalidCredential.WithMessage("token missing audience") + return forgeerrors.ErrInvalidCredential.WithMessage("token missing audience") } audienceValid := false @@ -303,12 +303,12 @@ func (b *Bundle) validateClaims(claims *ServiceClaims) error { } } if !audienceValid { - return errors.ErrInvalidCredential.WithMessage("invalid token audience") + return forgeerrors.ErrInvalidCredential.WithMessage("invalid token audience") } // Validate issuer if claims.Issuer != b.config.Issuer { - return errors.ErrInvalidCredential.WithMessage("invalid token issuer") + return forgeerrors.ErrInvalidCredential.WithMessage("invalid token issuer") } return nil @@ -340,7 +340,7 @@ func (b *Bundle) authenticateContext(ctx context.Context) (*ServiceClaims, error // UnaryServerInterceptor returns a gRPC server interceptor for JWT authentication. func (b *Bundle) UnaryServerInterceptor() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { claims, err := b.authenticateContext(ctx) if err != nil { return nil, err @@ -359,7 +359,7 @@ func (b *Bundle) UnaryServerInterceptor() grpc.UnaryServerInterceptor { // (grpc.ServerStream.Context()) and injects claims into a wrapped stream so // handlers can retrieve them via ClaimsFromContext. func (b *Bundle) StreamServerInterceptor() grpc.StreamServerInterceptor { - return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { claims, err := b.authenticateContext(ss.Context()) if err != nil { return err @@ -388,7 +388,7 @@ func (w *wrappedServerStream) Context() context.Context { // UnaryClientInterceptor returns a gRPC client interceptor for JWT token injection. func (b *Bundle) UnaryClientInterceptor() grpc.UnaryClientInterceptor { - return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { // Get or generate cached token for this service cacheKey := fmt.Sprintf("service:%s:permissions:", b.config.ServiceName) @@ -461,17 +461,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 "", stderrors.New("no metadata found") + return "", errors.New("no metadata found") } authHeaders := md.Get("authorization") if len(authHeaders) == 0 { - return "", stderrors.New("no authorization header found") + return "", errors.New("no authorization header found") } authHeader := authHeaders[0] if !strings.HasPrefix(authHeader, "Bearer ") { - return "", stderrors.New("invalid authorization header format") + return "", errors.New("invalid authorization header format") } return strings.TrimPrefix(authHeader, "Bearer "), nil @@ -487,11 +487,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 "", stderrors.New("no authorization header found") + return "", errors.New("no authorization header found") } if !strings.HasPrefix(authHeader, "Bearer ") { - return "", stderrors.New("invalid authorization header format") + return "", errors.New("invalid authorization header format") } return strings.TrimPrefix(authHeader, "Bearer "), nil @@ -571,8 +571,8 @@ func HasPermission(ctx context.Context, permission string) bool { return false } -// GetServiceID returns the authenticated service ID from context. -func GetServiceID(ctx context.Context) string { +// ServiceID returns the authenticated service ID from context. +func ServiceID(ctx context.Context) string { claims := ClaimsFromContext(ctx) if claims == nil { return "" @@ -580,8 +580,8 @@ func GetServiceID(ctx context.Context) string { return claims.ServiceID } -// GetServiceName returns the authenticated service name from context. -func GetServiceName(ctx context.Context) string { +// ServiceName returns the authenticated service name from context. +func ServiceName(ctx context.Context) string { claims := ClaimsFromContext(ctx) if claims == nil { return "" @@ -717,22 +717,22 @@ func (tc *tokenCache) cleanup() { // validateServiceIdentifier validates service ID and name formats to prevent injection attacks. func validateServiceIdentifier(identifier string) error { if identifier == "" { - return stderrors.New("identifier cannot be empty") + return errors.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 stderrors.New("identifier contains invalid characters, only alphanumeric, hyphens, and underscores allowed") + return errors.New("identifier contains invalid characters, only alphanumeric, hyphens, and underscores allowed") } // Reasonable length limits if len(identifier) > 64 { - return stderrors.New("identifier too long, maximum 64 characters") + return errors.New("identifier too long, maximum 64 characters") } if len(identifier) < 2 { - return stderrors.New("identifier too short, minimum 2 characters") + return errors.New("identifier too short, minimum 2 characters") } return nil diff --git a/bundles/jwt/bundle_test.go b/bundles/jwt/bundle_test.go index bb31be4..86e76d6 100644 --- a/bundles/jwt/bundle_test.go +++ b/bundles/jwt/bundle_test.go @@ -11,7 +11,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" ) // TestDefaultConfig tests default configuration values @@ -178,7 +178,7 @@ func TestBundle_Initialize_InvalidConfig(t *testing.T) { t.Fatal("Expected error for invalid configuration") } - if !errors.IsConfigurationError(err) { + if !forgeerrors.IsConfigurationError(err) { t.Error("Expected configuration error") } } @@ -437,40 +437,40 @@ func TestHasPermission_NoClaims(t *testing.T) { } } -// TestGetServiceID tests service ID extraction from context. -func TestGetServiceID(t *testing.T) { +// TestServiceID tests service ID extraction from context. +func TestServiceID(t *testing.T) { b := newValidBundle() token, _ := b.GenerateToken("my-svc", "test-service", nil) claims, _ := b.ValidateToken(token) ctx := b.contextWithClaims(context.Background(), claims) - if id := GetServiceID(ctx); id != "my-svc" { + if id := ServiceID(ctx); id != "my-svc" { t.Errorf("expected 'my-svc', got %q", id) } } -// TestGetServiceID_NoClaims returns empty string without claims. -func TestGetServiceID_NoClaims(t *testing.T) { - if id := GetServiceID(context.Background()); id != "" { +// TestServiceID_NoClaims returns empty string without claims. +func TestServiceID_NoClaims(t *testing.T) { + if id := ServiceID(context.Background()); id != "" { t.Errorf("expected empty string, got %q", id) } } -// TestGetServiceName tests service name extraction from context. -func TestGetServiceName(t *testing.T) { +// TestServiceName tests service name extraction from context. +func TestServiceName(t *testing.T) { b := newValidBundle() token, _ := b.GenerateToken("svc-123", "my-service", nil) claims, _ := b.ValidateToken(token) ctx := b.contextWithClaims(context.Background(), claims) - if name := GetServiceName(ctx); name != "my-service" { + if name := ServiceName(ctx); name != "my-service" { t.Errorf("expected 'my-service', got %q", name) } } -// TestGetServiceName_NoClaims returns empty string without claims. -func TestGetServiceName_NoClaims(t *testing.T) { - if name := GetServiceName(context.Background()); name != "" { +// TestServiceName_NoClaims returns empty string without claims. +func TestServiceName_NoClaims(t *testing.T) { + if name := ServiceName(context.Background()); name != "" { t.Errorf("expected empty string, got %q", name) } } @@ -665,7 +665,7 @@ func TestBundle_StreamServerInterceptor_RejectsMissingToken(t *testing.T) { stream := &fakeServerStream{ctx: context.Background()} handlerCalled := false - handler := func(srv interface{}, ss grpc.ServerStream) error { + handler := func(srv any, ss grpc.ServerStream) error { handlerCalled = true return nil } @@ -692,7 +692,7 @@ func TestBundle_StreamServerInterceptor_RejectsInvalidToken(t *testing.T) { ctx := metadata.NewIncomingContext(context.Background(), md) stream := &fakeServerStream{ctx: ctx} handlerCalled := false - handler := func(srv interface{}, ss grpc.ServerStream) error { + handler := func(srv any, ss grpc.ServerStream) error { handlerCalled = true return nil } @@ -725,7 +725,7 @@ func TestBundle_StreamServerInterceptor_AcceptsValidToken(t *testing.T) { stream := &fakeServerStream{ctx: ctx} var gotClaims *ServiceClaims - handler := func(srv interface{}, ss grpc.ServerStream) error { + handler := func(srv any, ss grpc.ServerStream) error { gotClaims = ClaimsFromContext(ss.Context()) return nil } diff --git a/bundles/postgresql/bundle.go b/bundles/postgresql/bundle.go index fd26ff8..45677d0 100644 --- a/bundles/postgresql/bundle.go +++ b/bundles/postgresql/bundle.go @@ -66,7 +66,7 @@ import ( _ "github.com/jackc/pgx/v5/stdlib" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" "github.com/datariot/forge/framework" forgeHealth "github.com/datariot/forge/health" ) @@ -119,33 +119,33 @@ func (b *Bundle) Name() string { // Initialize sets up the PostgreSQL connection and performs migrations if configured. func (b *Bundle) Initialize(app *framework.App) error { if b.config.DatabaseURL == "" { - return errors.ErrInvalidConfiguration.WithMessage("database_url is required for PostgreSQL bundle") + return forgeerrors.ErrInvalidConfiguration.WithMessage("database_url is required for PostgreSQL bundle") } // Validate connection pool configuration if b.config.MaxOpenConns <= 0 { - return errors.ErrInvalidConfiguration.WithMessage("max_open_conns must be positive, got %d", b.config.MaxOpenConns) + return forgeerrors.ErrInvalidConfiguration.WithMessage("max_open_conns must be positive, got %d", b.config.MaxOpenConns) } if b.config.MaxIdleConns < 0 { - return errors.ErrInvalidConfiguration.WithMessage("max_idle_conns must be non-negative, got %d", b.config.MaxIdleConns) + return forgeerrors.ErrInvalidConfiguration.WithMessage("max_idle_conns must be non-negative, got %d", b.config.MaxIdleConns) } if b.config.MaxIdleConns > b.config.MaxOpenConns { - return errors.ErrInvalidConfiguration.WithMessage("max_idle_conns (%d) cannot exceed max_open_conns (%d)", b.config.MaxIdleConns, b.config.MaxOpenConns) + return forgeerrors.ErrInvalidConfiguration.WithMessage("max_idle_conns (%d) cannot exceed max_open_conns (%d)", b.config.MaxIdleConns, b.config.MaxOpenConns) } if b.config.ConnMaxLifetime <= 0 { - return errors.ErrInvalidConfiguration.WithMessage("conn_max_lifetime must be positive, got %v", b.config.ConnMaxLifetime) + return forgeerrors.ErrInvalidConfiguration.WithMessage("conn_max_lifetime must be positive, got %v", b.config.ConnMaxLifetime) } if b.config.ConnMaxIdleTime < 0 { - return errors.ErrInvalidConfiguration.WithMessage("conn_max_idle_time must be non-negative, got %v", b.config.ConnMaxIdleTime) + return forgeerrors.ErrInvalidConfiguration.WithMessage("conn_max_idle_time must be non-negative, got %v", b.config.ConnMaxIdleTime) } if b.config.HealthCheckTimeout <= 0 { - return errors.ErrInvalidConfiguration.WithMessage("health_check_timeout must be positive, got %v", b.config.HealthCheckTimeout) + return forgeerrors.ErrInvalidConfiguration.WithMessage("health_check_timeout must be positive, got %v", b.config.HealthCheckTimeout) } // Open database connection db, err := sql.Open("pgx", b.config.DatabaseURL) if err != nil { - return errors.ErrRepositoryUnavailable.WithMessage("failed to open PostgreSQL connection").WithCause(err) + return forgeerrors.ErrRepositoryUnavailable.WithMessage("failed to open PostgreSQL connection").WithCause(err) } // Configure connection pool @@ -160,7 +160,7 @@ func (b *Bundle) Initialize(app *framework.App) error { if err := db.PingContext(ctx); err != nil { _ = db.Close() - return errors.ErrRepositoryUnavailable.WithMessage("failed to ping PostgreSQL database").WithCause(err) + return forgeerrors.ErrRepositoryUnavailable.WithMessage("failed to ping PostgreSQL database").WithCause(err) } b.db = db diff --git a/bundles/postgresql/bundle_test.go b/bundles/postgresql/bundle_test.go index 8e39f99..db41538 100644 --- a/bundles/postgresql/bundle_test.go +++ b/bundles/postgresql/bundle_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" ) // TestDefaultConfig tests default configuration values @@ -70,7 +70,7 @@ func TestBundle_Initialize_MissingDatabaseURL(t *testing.T) { t.Fatal("Expected error for missing database URL") } - if !errors.IsConfigurationError(err) { + if !forgeerrors.IsConfigurationError(err) { t.Error("Expected configuration error") } } @@ -88,7 +88,7 @@ func TestBundle_Initialize_InvalidMaxOpenConns(t *testing.T) { t.Fatal("Expected error for invalid MaxOpenConns") } - if !errors.IsConfigurationError(err) { + if !forgeerrors.IsConfigurationError(err) { t.Error("Expected configuration error") } } diff --git a/bundles/prometheus/bundle.go b/bundles/prometheus/bundle.go index 0900e56..fdd73a5 100644 --- a/bundles/prometheus/bundle.go +++ b/bundles/prometheus/bundle.go @@ -80,7 +80,7 @@ package prometheus import ( "context" - stderrors "errors" + "errors" "fmt" "net/http" "strconv" @@ -94,7 +94,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/status" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" "github.com/datariot/forge/framework" forgeHealth "github.com/datariot/forge/health" ) @@ -143,16 +143,16 @@ func DefaultConfig() Config { // Validate validates the Prometheus configuration. func (c *Config) Validate() error { if c.Namespace == "" { - return stderrors.New("namespace is required for Prometheus metrics") + return errors.New("namespace is required for Prometheus metrics") } // Validate metric naming (Prometheus has strict naming rules) if !isValidMetricName(c.Namespace) { - return stderrors.New("invalid namespace format: must match [a-zA-Z_:][a-zA-Z0-9_:]*") + return errors.New("invalid namespace format: must match [a-zA-Z_:][a-zA-Z0-9_:]*") } if c.Subsystem != "" && !isValidMetricName(c.Subsystem) { - return stderrors.New("invalid subsystem format: must match [a-zA-Z_:][a-zA-Z0-9_:]*") + return errors.New("invalid subsystem format: must match [a-zA-Z_:][a-zA-Z0-9_:]*") } // Validate service labels for security and cardinality @@ -177,7 +177,7 @@ func (c *Config) Validate() error { // Validate histogram buckets if len(c.HistogramBuckets) == 0 { - return stderrors.New("histogram buckets cannot be empty") + return errors.New("histogram buckets cannot be empty") } if len(c.HistogramBuckets) > 50 { @@ -187,13 +187,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 stderrors.New("histogram buckets must be in ascending order") + return errors.New("histogram buckets must be in ascending order") } } // Validate bucket ranges are reasonable if c.HistogramBuckets[0] <= 0 { - return stderrors.New("histogram buckets must be positive") + return errors.New("histogram buckets must be positive") } return nil @@ -238,7 +238,7 @@ func (b *Bundle) Name() string { // Initialize sets up Prometheus metrics collection. func (b *Bundle) Initialize(app *framework.App) error { if err := b.config.Validate(); err != nil { - return errors.ErrInvalidConfiguration.WithMessage("Prometheus configuration validation failed").WithCause(err) + return forgeerrors.ErrInvalidConfiguration.WithMessage("Prometheus configuration validation failed").WithCause(err) } b.logger = app.Logger().WithService("prometheus", "prometheus") @@ -287,7 +287,7 @@ func (b *Bundle) Initialize(app *framework.App) error { // status label is derived from the returned error via gRPC status codes // (e.g. "OK", "NotFound", "Internal"), so a nil error always records "OK". func (b *Bundle) UnaryServerInterceptor() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { start := time.Now() resp, err := handler(ctx, req) b.RecordGRPCRequest(info.FullMethod, status.Code(err).String(), time.Since(start)) @@ -299,7 +299,7 @@ func (b *Bundle) UnaryServerInterceptor() grpc.UnaryServerInterceptor { // automatically records RecordGRPCRequest metrics for every streaming call. // See UnaryServerInterceptor for how the status label is derived. func (b *Bundle) StreamServerInterceptor() grpc.StreamServerInterceptor { - return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { start := time.Now() err := handler(srv, ss) b.RecordGRPCRequest(info.FullMethod, status.Code(err).String(), time.Since(start)) @@ -545,7 +545,7 @@ func (b *Bundle) registerMetric(collector prometheus.Collector) error { err := b.registry.Register(collector) if err != nil { var alreadyRegisteredErr prometheus.AlreadyRegisteredError - if stderrors.As(err, &alreadyRegisteredErr) { + if errors.As(err, &alreadyRegisteredErr) { // Metric already exists - this is not an error in most cases return nil } @@ -731,7 +731,7 @@ func (b *Bundle) validateMetricDefinition(name, help string, labelNames []string } if help == "" { - return stderrors.New("metric help text is required") + return errors.New("metric help text is required") } // Prevent high cardinality @@ -791,13 +791,13 @@ func DefaultSecurityConfig() SecurityConfig { } } -// GetMetricsHandler returns an HTTP handler for the /metrics endpoint with basic security. -func (b *Bundle) GetMetricsHandler() http.Handler { - return b.GetSecureMetricsHandler(DefaultSecurityConfig()) +// MetricsHandler returns an HTTP handler for the /metrics endpoint with basic security. +func (b *Bundle) MetricsHandler() http.Handler { + return b.SecureMetricsHandler(DefaultSecurityConfig()) } -// GetSecureMetricsHandler returns a secured HTTP handler for the /metrics endpoint. -func (b *Bundle) GetSecureMetricsHandler(secConfig SecurityConfig) http.Handler { +// SecureMetricsHandler returns a secured HTTP handler for the /metrics endpoint. +func (b *Bundle) SecureMetricsHandler(secConfig SecurityConfig) http.Handler { handler := promhttp.HandlerFor( b.gatherer, promhttp.HandlerOpts{ @@ -883,7 +883,7 @@ func (c *PrometheusHealthCheck) Readiness(ctx context.Context) error { // Ensure we have at least some metrics registered if len(metricFamilies) == 0 { - return stderrors.New("no metrics registered in Prometheus registry") + return errors.New("no metrics registered in Prometheus registry") } // Check for expected namespace metrics diff --git a/bundles/prometheus/bundle_middleware_test.go b/bundles/prometheus/bundle_middleware_test.go index 95fb48d..d139227 100644 --- a/bundles/prometheus/bundle_middleware_test.go +++ b/bundles/prometheus/bundle_middleware_test.go @@ -83,7 +83,7 @@ func TestBundle_Initialize_WiresAutomaticGRPCMetrics(t *testing.T) { // Invoke the interceptor directly with a fake handler + UnaryServerInfo, // mirroring how grpc.ChainUnaryInterceptor would call it for a real RPC. interceptor := b.UnaryServerInterceptor() - fakeHandler := func(ctx context.Context, req interface{}) (interface{}, error) { + fakeHandler := func(ctx context.Context, req any) (any, error) { return "ok", nil } info := &grpc.UnaryServerInfo{FullMethod: "/test.Service/Method"} @@ -112,7 +112,7 @@ func TestBundle_UnaryServerInterceptor_RecordsOKStatus(t *testing.T) { info := &grpc.UnaryServerInfo{FullMethod: "/test.Service/Get"} _, err := interceptor(context.Background(), nil, info, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { return nil, nil }) if err != nil { @@ -137,7 +137,7 @@ func TestBundle_UnaryServerInterceptor_RecordsErrorStatus(t *testing.T) { info := &grpc.UnaryServerInfo{FullMethod: "/test.Service/Get"} _, err := interceptor(context.Background(), nil, info, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { return nil, status.Error(codes.NotFound, "missing") }) if err == nil { @@ -163,7 +163,7 @@ func TestBundle_StreamServerInterceptor_RecordsMetrics(t *testing.T) { interceptor := b.StreamServerInterceptor() info := &grpc.StreamServerInfo{FullMethod: "/test.Service/Stream"} - err := interceptor(nil, nil, info, func(srv interface{}, stream grpc.ServerStream) error { + err := interceptor(nil, nil, info, func(srv any, stream grpc.ServerStream) error { return nil }) if err != nil { diff --git a/bundles/prometheus/bundle_test.go b/bundles/prometheus/bundle_test.go index 140a707..c85f351 100644 --- a/bundles/prometheus/bundle_test.go +++ b/bundles/prometheus/bundle_test.go @@ -479,7 +479,7 @@ func TestCreateCustomCounter_TooManyLabels(t *testing.T) { // --- Metrics handler tests --- -func TestGetMetricsHandler(t *testing.T) { +func TestMetricsHandler(t *testing.T) { cfg := Config{ Namespace: "test", EnableHTTPMetrics: true, @@ -488,7 +488,7 @@ func TestGetMetricsHandler(t *testing.T) { } b := newInitializedBundle(t, cfg) - handler := b.GetMetricsHandler() + handler := b.MetricsHandler() if handler == nil { t.Fatal("expected non-nil handler") } @@ -502,7 +502,7 @@ func TestGetMetricsHandler(t *testing.T) { } } -func TestGetSecureMetricsHandler_BasicAuth(t *testing.T) { +func TestSecureMetricsHandler_BasicAuth(t *testing.T) { cfg := Config{ Namespace: "test", HistogramBuckets: []float64{0.01, 0.1}, @@ -517,7 +517,7 @@ func TestGetSecureMetricsHandler_BasicAuth(t *testing.T) { Username: "admin", Password: "secret", } - handler := b.GetSecureMetricsHandler(secConfig) + handler := b.SecureMetricsHandler(secConfig) t.Run("missing credentials returns 401", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/metrics", nil) diff --git a/bundles/redis/bundle.go b/bundles/redis/bundle.go index e425165..b8e06a6 100644 --- a/bundles/redis/bundle.go +++ b/bundles/redis/bundle.go @@ -76,7 +76,7 @@ import ( "crypto/tls" "encoding/hex" "encoding/json" - stderrors "errors" + "errors" "fmt" "net/url" "os" @@ -85,7 +85,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" "github.com/datariot/forge/framework" forgeHealth "github.com/datariot/forge/health" ) @@ -140,7 +140,7 @@ func DefaultConfig() Config { // Validate validates the Redis configuration. func (c *Config) Validate() error { if c.RedisURL == "" { - return stderrors.New("redis_url is required") + return errors.New("redis_url is required") } // Parse and validate Redis URL @@ -151,28 +151,28 @@ func (c *Config) Validate() error { // Validate scheme if parsedURL.Scheme != "redis" && parsedURL.Scheme != "rediss" { - return stderrors.New("redis_url must use 'redis://' or 'rediss://' scheme") + return errors.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 stderrors.New("authentication required for remote Redis connections") + return errors.New("authentication required for remote Redis connections") } if password, ok := parsedURL.User.Password(); !ok || password == "" { - return stderrors.New("password required for remote Redis connections") + return errors.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 stderrors.New("TLS required for remote Redis connections, use rediss:// scheme") + return errors.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 stderrors.New("TLS certificate verification cannot be disabled for security") + return errors.New("TLS certificate verification cannot be disabled for security") } } @@ -255,13 +255,13 @@ func (b *Bundle) Name() string { // Initialize sets up the Redis connection and services. func (b *Bundle) Initialize(app *framework.App) error { if err := b.config.Validate(); err != nil { - return errors.ErrInvalidConfiguration.WithMessage("Redis configuration validation failed").WithCause(err) + return forgeerrors.ErrInvalidConfiguration.WithMessage("Redis configuration validation failed").WithCause(err) } // Parse Redis URL opts, err := redis.ParseURL(b.config.RedisURL) if err != nil { - return errors.ErrInvalidConfiguration.WithMessage("invalid Redis URL format").WithCause(err) + return forgeerrors.ErrInvalidConfiguration.WithMessage("invalid Redis URL format").WithCause(err) } // Apply configuration overrides @@ -293,7 +293,7 @@ func (b *Bundle) Initialize(app *framework.App) error { if err := b.client.Ping(ctx).Err(); err != nil { _ = b.client.Close() - return errors.ErrRepositoryUnavailable.WithMessage( + return forgeerrors.ErrRepositoryUnavailable.WithMessage( "failed to connect to Redis at %s", b.config.SanitizedRedisURL(), ).WithCause(err) } @@ -418,7 +418,7 @@ func (c *RedisHealthCheck) Readiness(ctx context.Context) error { } // ErrCacheMiss indicates the requested key does not exist in the cache. -var ErrCacheMiss = stderrors.New("cache miss") +var ErrCacheMiss = errors.New("cache miss") // CacheService provides high-level caching operations. type CacheService struct { @@ -431,7 +431,7 @@ func NewCacheService(client redis.UniversalClient) *CacheService { } // Set stores a value in the cache with the specified TTL. -func (c *CacheService) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { +func (c *CacheService) Set(ctx context.Context, key string, value any, ttl time.Duration) error { data, err := json.Marshal(value) if err != nil { return fmt.Errorf("failed to marshal cache value: %w", err) @@ -441,10 +441,10 @@ func (c *CacheService) Set(ctx context.Context, key string, value interface{}, t } // Get retrieves a value from the cache and unmarshals it into the provided destination. -func (c *CacheService) Get(ctx context.Context, key string, dest interface{}) error { +func (c *CacheService) Get(ctx context.Context, key string, dest any) error { data, err := c.client.Get(ctx, key).Result() if err != nil { - if stderrors.Is(err, redis.Nil) { + if errors.Is(err, redis.Nil) { return fmt.Errorf("cache key %q: %w", key, ErrCacheMiss) } return fmt.Errorf("failed to get cache value: %w", err) @@ -491,7 +491,7 @@ func NewPubSubService(client redis.UniversalClient) *PubSubService { } // Publish publishes a message to the specified channel. -func (p *PubSubService) Publish(ctx context.Context, channel string, message interface{}) error { +func (p *PubSubService) Publish(ctx context.Context, channel string, message any) error { data, err := json.Marshal(message) if err != nil { return fmt.Errorf("failed to marshal pub/sub message: %w", err) diff --git a/bundles/redis/bundle_test.go b/bundles/redis/bundle_test.go index 08746dc..ca35552 100644 --- a/bundles/redis/bundle_test.go +++ b/bundles/redis/bundle_test.go @@ -2,7 +2,7 @@ package redis import ( "context" - stderrors "errors" + "errors" "fmt" "sync" "sync/atomic" @@ -11,7 +11,7 @@ import ( goredis "github.com/redis/go-redis/v9" - "github.com/datariot/forge/errors" + "github.com/datariot/forge/forgeerrors" ) // newLiveTestClient returns a Redis client connected to a local test instance, @@ -99,7 +99,7 @@ func TestBundle_Initialize_MissingRedisURL(t *testing.T) { t.Fatal("Expected error for missing Redis URL") } - if !errors.IsConfigurationError(err) { + if !forgeerrors.IsConfigurationError(err) { t.Error("Expected configuration error") } } @@ -346,7 +346,7 @@ func TestConfig_Validate_EdgeCases(t *testing.T) { t.Error("Expected error but got nil") } // Note: "valid config" will fail without actual Redis, but validates config logic - if !tt.wantErr && err != nil && !errors.IsRepositoryError(err) { + if !tt.wantErr && err != nil && !forgeerrors.IsRepositoryError(err) { // Only fail if it's NOT a repository error (connection failure is expected) t.Errorf("Expected no validation error but got: %v", err) } @@ -371,7 +371,7 @@ func TestCacheService_Get_CacheMiss(t *testing.T) { if err == nil { t.Fatal("expected error for missing cache key") } - if !stderrors.Is(err, ErrCacheMiss) { + if !errors.Is(err, ErrCacheMiss) { t.Errorf("expected errors.Is(err, ErrCacheMiss), got %v", err) } } diff --git a/doc.go b/doc.go index 4baa5cd..d7965ef 100644 --- a/doc.go +++ b/doc.go @@ -68,7 +68,7 @@ // - framework: Core application lifecycle and interfaces // - config: Environment-based configuration management // - health: Kubernetes-compatible health check system -// - errors: Structured error handling with classification +// - forgeerrors: Structured error handling with classification // // # HTTP Endpoints // diff --git a/docs/api-reference.md b/docs/api-reference.md index b5812c5..ad2a5d9 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -150,9 +150,9 @@ registry.Register(check, config) **Methods:** - `Register(Check, CheckConfig) error` - Register health check -- `CheckLiveness(context.Context) HealthStatus` - Check liveness -- `CheckReadiness(context.Context) HealthStatus` - Check readiness -- `CheckHealth(context.Context) HealthStatus` - Check overall health +- `CheckLiveness(context.Context) Report` - Check liveness +- `CheckReadiness(context.Context) Report` - Check readiness +- `CheckHealth(context.Context) Report` - Check overall health ### Built-in Checks @@ -160,7 +160,7 @@ registry.Register(check, config) - `NewAlwaysUnhealthyCheck(name, err)` - Always reports unhealthy - `NewBasicCheck(config, liveness, readiness)` - Custom check functions -## Error Handling (`errors`) +## Error Handling (`forgeerrors`) ### DomainError @@ -284,7 +284,7 @@ mux.Handle("/admin/", bundle.RequirePermissions("admin")(adminHandler)) // Access claims in handlers claims := jwt.ClaimsFromContext(ctx) -serviceID := jwt.GetServiceID(ctx) +serviceID := jwt.ServiceID(ctx) hasPermission := jwt.HasPermission(ctx, "read:users") ``` @@ -315,8 +315,8 @@ err := client.Post(ctx, "/users", createRequest, &response) resp, err := client.RawRequest(ctx, "GET", "/custom", headers, body) // Circuit breaker monitoring -state := client.GetCircuitBreakerState() -counts := client.GetCircuitBreakerCounts() +state := client.CircuitBreakerState() +counts := client.CircuitBreakerCounts() ``` ### Prometheus Bundle @@ -349,7 +349,7 @@ bundle.RecordGRPCRequest("UserService.GetUser", "OK", duration) bundle.RecordHealthCheck("database", "readiness", true, duration) // Get metrics handler -handler := bundle.GetMetricsHandler() +handler := bundle.MetricsHandler() ``` ### Configuration Loading Bundle diff --git a/examples/config-service/main.go b/examples/config-service/main.go index dce1243..406d853 100644 --- a/examples/config-service/main.go +++ b/examples/config-service/main.go @@ -142,7 +142,7 @@ func (s *ConfigService) Start(ctx context.Context) error { log.Printf("ConfigService started with automatic configuration loading") // Setup configuration change monitoring - s.configBundle.Loader().OnConfigChange(func(newConfig interface{}) { + s.configBundle.Loader().OnConfigChange(func(newConfig any) { if cfg, ok := newConfig.(*ServiceConfig); ok { log.Printf("Configuration changed! New debug setting: %v", cfg.Debug) log.Printf("New max connections: %d", cfg.MaxConnections) @@ -192,14 +192,14 @@ func (s *ConfigService) setupHTTPEndpoints(mux *http.ServeMux) { // handleConfigInfo returns current configuration information (sanitized). func (s *ConfigService) handleConfigInfo(w http.ResponseWriter, r *http.Request) { - info := map[string]interface{}{ + info := map[string]any{ "service": s.config.ServiceName, "environment": s.config.AppEnv, "debug": s.config.Debug, "max_connections": s.config.MaxConnections, "cache_timeout": s.config.CacheTimeout.String(), "features": s.config.Features, - "external_services": map[string]interface{}{ + "external_services": map[string]any{ "user_service_url": s.config.ExternalServices.UserServiceURL, "email_service_url": s.config.ExternalServices.EmailServiceURL, "timeout": s.config.ExternalServices.Timeout, @@ -208,7 +208,7 @@ func (s *ConfigService) handleConfigInfo(w http.ResponseWriter, r *http.Request) "database_url": "[REDACTED]", "api_key": "[REDACTED]", "jwt_secret": "[REDACTED]", - "load_info": s.configBundle.Loader().GetConfigInfo(), + "load_info": s.configBundle.Loader().ConfigInfo(), "timestamp": time.Now().UTC(), } @@ -236,7 +236,7 @@ func (s *ConfigService) handleConfigReload(w http.ResponseWriter, r *http.Reques // Update current config (in a real app, you'd need more sophisticated handling) s.config = &newConfig - response := map[string]interface{}{ + response := map[string]any{ "reloaded": true, "timestamp": time.Now().UTC(), "load_info": result, @@ -248,9 +248,9 @@ func (s *ConfigService) handleConfigReload(w http.ResponseWriter, r *http.Reques // handleConfigSources returns information about configuration sources. func (s *ConfigService) handleConfigSources(w http.ResponseWriter, r *http.Request) { - sources := map[string]interface{}{ + sources := map[string]any{ "load_result": s.loadResult, - "loader_info": s.configBundle.Loader().GetConfigInfo(), + "loader_info": s.configBundle.Loader().ConfigInfo(), "timestamp": time.Now().UTC(), } @@ -262,7 +262,7 @@ func (s *ConfigService) handleConfigSources(w http.ResponseWriter, r *http.Reque func (s *ConfigService) handleConfigValidate(w http.ResponseWriter, r *http.Request) { err := s.config.Validate() - response := map[string]interface{}{ + response := map[string]any{ "valid": err == nil, "timestamp": time.Now().UTC(), } diff --git a/examples/httpclient-service/main.go b/examples/httpclient-service/main.go index 7611c9c..833adeb 100644 --- a/examples/httpclient-service/main.go +++ b/examples/httpclient-service/main.go @@ -160,7 +160,7 @@ func (s *HTTPClientService) handleTestGet(w http.ResponseWriter, r *http.Request client := s.httpBundle.Client() // Make GET request to httpbin.org - var response map[string]interface{} + var response map[string]any err := client.Get(r.Context(), "/get?test=true", &response) if err != nil { http.Error(w, fmt.Sprintf("GET request failed: %v", err), http.StatusBadGateway) @@ -169,7 +169,7 @@ 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{}{ + json.NewEncoder(w).Encode(map[string]any{ "success": true, "method": "GET", "target": s.config.TargetServiceURL + "/get", @@ -189,7 +189,7 @@ func (s *HTTPClientService) handleTestPost(w http.ResponseWriter, r *http.Reques } // Make POST request - var response map[string]interface{} + var response map[string]any err := client.Post(r.Context(), "/post", user, &response) if err != nil { http.Error(w, fmt.Sprintf("POST request failed: %v", err), http.StatusBadGateway) @@ -198,7 +198,7 @@ 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{}{ + json.NewEncoder(w).Encode(map[string]any{ "success": true, "method": "POST", "target": s.config.TargetServiceURL + "/post", @@ -215,7 +215,7 @@ func (s *HTTPClientService) handleTestUnreliable(w http.ResponseWriter, r *http. statusCodes := []int{200, 500, 502, 503, 504} randomStatus := statusCodes[rand.Intn(len(statusCodes))] - var response map[string]interface{} + var response map[string]any err := client.Get(r.Context(), "/status/"+strconv.Itoa(randomStatus), &response) w.Header().Set("Content-Type", "application/json") @@ -223,7 +223,7 @@ func (s *HTTPClientService) handleTestUnreliable(w http.ResponseWriter, r *http. if err != nil { // Check if circuit breaker is open if err == httpclient.ErrCircuitBreakerOpen { - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "success": false, "error": "Circuit breaker is open", "circuit_breaker": "OPEN", @@ -232,7 +232,7 @@ func (s *HTTPClientService) handleTestUnreliable(w http.ResponseWriter, r *http. return } - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "success": false, "error": err.Error(), "target": s.config.TargetServiceURL + "/status/" + strconv.Itoa(randomStatus), @@ -240,7 +240,7 @@ func (s *HTTPClientService) handleTestUnreliable(w http.ResponseWriter, r *http. return } - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "success": true, "method": "GET", "target": s.config.TargetServiceURL + "/status/" + strconv.Itoa(randomStatus), @@ -263,7 +263,7 @@ func (s *HTTPClientService) handleTestAuth(w http.ResponseWriter, r *http.Reques } // Make authenticated request - var response map[string]interface{} + var response map[string]any err := client.Get(ctx, "/bearer", &response) if err != nil { http.Error(w, fmt.Sprintf("Authenticated request failed: %v", err), http.StatusBadGateway) @@ -271,7 +271,7 @@ func (s *HTTPClientService) handleTestAuth(w http.ResponseWriter, r *http.Reques } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "success": true, "method": "GET", "target": s.config.TargetServiceURL + "/bearer", @@ -284,12 +284,12 @@ func (s *HTTPClientService) handleTestAuth(w http.ResponseWriter, r *http.Reques func (s *HTTPClientService) handleCircuitBreakerStatus(w http.ResponseWriter, r *http.Request) { client := s.httpBundle.Client() - state := client.GetCircuitBreakerState() - counts := client.GetCircuitBreakerCounts() + state := client.CircuitBreakerState() + counts := client.CircuitBreakerCounts() - status := map[string]interface{}{ + status := map[string]any{ "state": state.String(), - "counts": map[string]interface{}{ + "counts": map[string]any{ "requests": counts.Requests, "total_successes": counts.TotalSuccesses, "total_failures": counts.TotalFailures, @@ -307,7 +307,7 @@ func (s *HTTPClientService) handleCircuitBreakerStatus(w http.ResponseWriter, r func (s *HTTPClientService) handleClientStats(w http.ResponseWriter, r *http.Request) { // This would show connection pool stats, request metrics, etc. // For now, return basic information - stats := map[string]interface{}{ + stats := map[string]any{ "service": s.config.ServiceName, "target_url": s.config.TargetServiceURL, "timeout": s.clientTimeout.String(), @@ -339,7 +339,7 @@ func (c *HTTPClientHealthCheck) Liveness(ctx context.Context) error { // Readiness performs a comprehensive HTTP client readiness check. func (c *HTTPClientHealthCheck) Readiness(ctx context.Context) error { // Check circuit breaker state - state := c.client.GetCircuitBreakerState() + state := c.client.CircuitBreakerState() if state == gobreaker.StateOpen { return fmt.Errorf("HTTP client circuit breaker is open") } diff --git a/examples/jwt-service/main.go b/examples/jwt-service/main.go index a2ab92c..9c71872 100644 --- a/examples/jwt-service/main.go +++ b/examples/jwt-service/main.go @@ -141,7 +141,7 @@ func (s *AuthenticatedService) setupHTTPEndpoints(mux *http.ServeMux, jwtBundle // handlePublic handles public endpoints that don't require authentication. func (s *AuthenticatedService) handlePublic(w http.ResponseWriter, r *http.Request) { - response := map[string]interface{}{ + response := map[string]any{ "message": "This is a public endpoint", "service": s.config.ServiceName, "time": time.Now().UTC(), @@ -160,7 +160,7 @@ func (s *AuthenticatedService) handleProtected(w http.ResponseWriter, r *http.Re return } - response := map[string]interface{}{ + response := map[string]any{ "message": "This is a protected endpoint", "service": s.config.ServiceName, "authenticated_service": claims.ServiceName, @@ -181,7 +181,7 @@ func (s *AuthenticatedService) handleAdmin(w http.ResponseWriter, r *http.Reques return } - response := map[string]interface{}{ + response := map[string]any{ "message": "This is an admin endpoint", "service": s.config.ServiceName, "authenticated_service": claims.ServiceName, @@ -227,7 +227,7 @@ func (s *AuthenticatedService) handleGenerateToken(w http.ResponseWriter, r *htt return } - response := map[string]interface{}{ + response := map[string]any{ "token": token, "service_id": request.ServiceID, "expires_in": "24h", diff --git a/examples/prometheus-service/main.go b/examples/prometheus-service/main.go index a74860b..3433618 100644 --- a/examples/prometheus-service/main.go +++ b/examples/prometheus-service/main.go @@ -260,7 +260,7 @@ func (s *MetricsService) handleGetUsers(w http.ResponseWriter, r *http.Request) } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "users": users, "cache_hit": cacheHit, "timestamp": time.Now().UTC(), @@ -284,7 +284,7 @@ func (s *MetricsService) handleCreateUser(w http.ResponseWriter, r *http.Request s.userOperations.WithLabelValues("create", "success").Inc() w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "user": user, "created": true, "timestamp": time.Now().UTC(), @@ -326,7 +326,7 @@ func (s *MetricsService) handleSimulateSlow(w http.ResponseWriter, r *http.Reque s.prometheusBundle.RecordHTTPRequest(r.Method, endpoint, 200, duration) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "message": "Slow operation completed", "delay": delay.String(), "duration": duration.String(), @@ -336,7 +336,7 @@ 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{}{ + info := map[string]any{ "service": s.config.ServiceName, "namespace": s.config.MetricsNamespace, "subsystem": s.config.MetricsSubsystem, @@ -392,7 +392,7 @@ func (s *MetricsService) handleUpdateMetrics(w http.ResponseWriter, r *http.Requ } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "updated": true, "message": "Metrics updated with random values", "timestamp": time.Now().UTC(), diff --git a/examples/redis-service/main.go b/examples/redis-service/main.go index 8d793ad..cb52910 100644 --- a/examples/redis-service/main.go +++ b/examples/redis-service/main.go @@ -176,7 +176,7 @@ func (s *CacheService) handleCache(w http.ResponseWriter, r *http.Request) { // handleCacheGet retrieves an item from cache. func (s *CacheService) handleCacheGet(w http.ResponseWriter, r *http.Request, key string) { - var result interface{} + var result any err := s.redisBundle.Cache().Get(r.Context(), key, &result) if err != nil { if strings.Contains(err.Error(), "not found") { @@ -188,7 +188,7 @@ func (s *CacheService) handleCacheGet(w http.ResponseWriter, r *http.Request, ke } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "key": key, "value": result, "found": true, @@ -197,7 +197,7 @@ func (s *CacheService) handleCacheGet(w http.ResponseWriter, r *http.Request, ke // handleCacheSet stores an item in cache. func (s *CacheService) handleCacheSet(w http.ResponseWriter, r *http.Request, key, cacheType string) { - var data interface{} + var data any if err := json.NewDecoder(r.Body).Decode(&data); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return @@ -222,7 +222,7 @@ func (s *CacheService) handleCacheSet(w http.ResponseWriter, r *http.Request, ke } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "key": key, "stored": true, "ttl": ttl.String(), @@ -237,7 +237,7 @@ func (s *CacheService) handleCacheDelete(w http.ResponseWriter, r *http.Request, } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "key": key, "deleted": true, }) @@ -257,7 +257,7 @@ func (s *CacheService) handleEvents(w http.ResponseWriter, r *http.Request) { return } - var event interface{} + var event any if err := json.NewDecoder(r.Body).Decode(&event); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return @@ -270,7 +270,7 @@ func (s *CacheService) handleEvents(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "channel": channel, "published": true, "timestamp": time.Now().UTC(), @@ -297,7 +297,7 @@ func (s *CacheService) handleRateLimited(w http.ResponseWriter, r *http.Request) } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "message": "Request allowed", "client_ip": clientIP, "timestamp": time.Now().UTC(), @@ -326,7 +326,7 @@ func (s *CacheService) handleDistributedLock(w http.ResponseWriter, r *http.Requ } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "resource": resource, "acquired": acquired, "ttl": "30s", @@ -340,7 +340,7 @@ func (s *CacheService) handleDistributedLock(w http.ResponseWriter, r *http.Requ } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "resource": resource, "released": true, }) @@ -364,9 +364,9 @@ func (s *CacheService) handleRedisStats(w http.ResponseWriter, r *http.Request) // Get connection pool stats stats := client.PoolStats() - response := map[string]interface{}{ + response := map[string]any{ "redis_info": strings.Split(info, "\r\n"), - "pool_stats": map[string]interface{}{ + "pool_stats": map[string]any{ "hits": stats.Hits, "misses": stats.Misses, "timeouts": stats.Timeouts, @@ -400,7 +400,7 @@ func (s *CacheService) listenForEvents(ctx context.Context) { log.Printf("Received event: channel=%s, payload=%s", msg.Channel, msg.Payload) // Process event (in a real application, you might parse JSON and handle different event types) - var event map[string]interface{} + var event map[string]any if err := json.Unmarshal([]byte(msg.Payload), &event); err == nil { log.Printf("Processed event: %+v", event) } @@ -433,7 +433,7 @@ func (c *CacheServiceHealthCheck) Liveness(ctx context.Context) error { func (c *CacheServiceHealthCheck) Readiness(ctx context.Context) error { // Test cache operations testKey := "health:readiness:test" - testValue := map[string]interface{}{ + testValue := map[string]any{ "timestamp": time.Now().UTC(), "service": "cache-service", } @@ -444,7 +444,7 @@ func (c *CacheServiceHealthCheck) Readiness(ctx context.Context) error { } // Test get operation - var retrieved map[string]interface{} + var retrieved map[string]any if err := c.cache.Get(ctx, testKey, &retrieved); err != nil { return fmt.Errorf("cache get operation failed: %w", err) } diff --git a/errors/errors.go b/forgeerrors/errors.go similarity index 92% rename from errors/errors.go rename to forgeerrors/errors.go index 59131a9..6626c6e 100644 --- a/errors/errors.go +++ b/forgeerrors/errors.go @@ -1,6 +1,6 @@ -// Package errors provides domain-specific error handling patterns for Forge microservices. +// Package forgeerrors provides domain-specific error handling patterns for Forge microservices. // -// The errors package implements structured error handling with error codes, context, +// The forgeerrors package implements structured error handling with error codes, context, // and error classification. It provides common error patterns that can be shared // across microservices and enables consistent error handling and reporting. // @@ -9,25 +9,25 @@ // Use the predefined domain errors and customize with context: // // if db.Ping() != nil { -// return errors.ErrRepositoryUnavailable.WithMessage("database connection failed") +// return forgeerrors.ErrRepositoryUnavailable.WithMessage("database connection failed") // } // // // With cause // if err := validateUser(user); err != nil { -// return errors.ErrInvalidConfiguration.WithCause(err) +// return forgeerrors.ErrInvalidConfiguration.WithCause(err) // } // // # Error Classification // // The package provides classification functions for error handling: // -// if errors.IsTransientError(err) { +// if forgeerrors.IsTransientError(err) { // // Retry the operation // time.Sleep(backoff) // return retryOperation() // } // -// if errors.IsAuthenticationError(err) { +// if forgeerrors.IsAuthenticationError(err) { // // Return 401 Unauthorized // return handleAuthError(err) // } @@ -36,14 +36,14 @@ // // Create service-specific errors using the DomainError pattern: // -// var ErrUserNotFound = errors.DomainError{ +// var ErrUserNotFound = forgeerrors.DomainError{ // Code: "USER_NOT_FOUND", // Message: "user not found", // } // // // Usage // return ErrUserNotFound.WithMessage("user %s not found", userID) -package errors +package forgeerrors import ( "errors" @@ -86,7 +86,7 @@ func (e DomainError) Error() string { } // WithMessage returns a new DomainError with the specified message -func (e DomainError) WithMessage(format string, args ...interface{}) DomainError { +func (e DomainError) WithMessage(format string, args ...any) DomainError { return DomainError{ Code: e.Code, Message: fmt.Sprintf(format, args...), @@ -203,7 +203,7 @@ var ( // // Use this for implementing retry logic: // -// if errors.IsTransientError(err) { +// if forgeerrors.IsTransientError(err) { // return backoff.Retry(operation, backoff.NewExponentialBackOff()) // } func IsTransientError(err error) bool { diff --git a/errors/errors_test.go b/forgeerrors/errors_test.go similarity index 99% rename from errors/errors_test.go rename to forgeerrors/errors_test.go index 18747fd..60672bf 100644 --- a/errors/errors_test.go +++ b/forgeerrors/errors_test.go @@ -1,4 +1,4 @@ -package errors +package forgeerrors import ( "errors" diff --git a/framework/app_http_test.go b/framework/app_http_test.go index 1774f2c..a47c68e 100644 --- a/framework/app_http_test.go +++ b/framework/app_http_test.go @@ -45,7 +45,7 @@ func TestApp_HandleReady_NotReady(t *testing.T) { } // Verify JSON response - var response map[string]interface{} + var response map[string]any if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse JSON response: %v", err) } @@ -102,7 +102,7 @@ func TestApp_HealthEndpoints_RedactErrorDetail(t *testing.T) { t.Errorf("expected response body to not leak raw dependency error, got: %s", body) } - var parsed map[string]interface{} + var parsed map[string]any if err := json.Unmarshal(w.Body.Bytes(), &parsed); err != nil { t.Fatalf("failed to parse JSON response: %v", err) } diff --git a/framework/app_middleware_test.go b/framework/app_middleware_test.go index 618b9c8..069b320 100644 --- a/framework/app_middleware_test.go +++ b/framework/app_middleware_test.go @@ -62,7 +62,7 @@ func TestApp_AddUnaryInterceptor_RunsForRealCall(t *testing.T) { } called := false - app.AddUnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + app.AddUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { called = true return handler(ctx, req) }) @@ -113,7 +113,7 @@ func TestApp_AddUnaryInterceptor_NoopAfterServerBuilt(t *testing.T) { defer func() { _ = app.Stop(context.Background()) }() before := len(app.unaryInterceptors) - app.AddUnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + app.AddUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { return handler(ctx, req) }) if len(app.unaryInterceptors) != before { diff --git a/framework/app_test.go b/framework/app_test.go index b92f838..a62aec4 100644 --- a/framework/app_test.go +++ b/framework/app_test.go @@ -465,7 +465,7 @@ func TestApp_WithUnaryInterceptor_Valid(t *testing.T) { cfg := config.DefaultBaseConfig() cfg.ServiceName = "test-service" - interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + interceptor := func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { return handler(ctx, req) } @@ -615,7 +615,7 @@ func TestLoggingManager_WithContext(t *testing.T) { t.Fatalf("failed to initialize logging manager: %v", err) } - logger := lm.WithContext(map[string]interface{}{ + logger := lm.WithContext(map[string]any{ "key1": "value1", "key2": 42, }) @@ -714,7 +714,7 @@ func TestApp_WithStreamInterceptor_Valid(t *testing.T) { cfg := config.DefaultBaseConfig() cfg.ServiceName = "test-service" - interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + interceptor := func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { return handler(srv, ss) } diff --git a/framework/http.go b/framework/http.go index acf3c6f..05c767d 100644 --- a/framework/http.go +++ b/framework/http.go @@ -444,6 +444,6 @@ type promLogAdapter struct { } // Println implements the log.Logger interface for Prometheus error logging. -func (p *promLogAdapter) Println(v ...interface{}) { +func (p *promLogAdapter) Println(v ...any) { p.logger.Error().Interface("prometheus_error", v).Msg("Prometheus metrics error") } diff --git a/framework/logging.go b/framework/logging.go index b05bfca..9d0ca28 100644 --- a/framework/logging.go +++ b/framework/logging.go @@ -67,7 +67,7 @@ func (lm *LoggingManager) WithService(service, component string) zerolog.Logger } // WithContext creates a logger with additional context fields. -func (lm *LoggingManager) WithContext(fields map[string]interface{}) zerolog.Logger { +func (lm *LoggingManager) WithContext(fields map[string]any) zerolog.Logger { ctx := lm.logger.With() for key, value := range fields { ctx = ctx.Interface(key, value) @@ -86,32 +86,32 @@ type healthLoggerAdapter struct { logger zerolog.Logger } -func (h *healthLoggerAdapter) Debug(msg string, fields ...interface{}) { +func (h *healthLoggerAdapter) Debug(msg string, fields ...any) { event := h.logger.Debug() h.addFields(event, fields) event.Msg(msg) } -func (h *healthLoggerAdapter) Info(msg string, fields ...interface{}) { +func (h *healthLoggerAdapter) Info(msg string, fields ...any) { event := h.logger.Info() h.addFields(event, fields) event.Msg(msg) } -func (h *healthLoggerAdapter) Warn(msg string, fields ...interface{}) { +func (h *healthLoggerAdapter) Warn(msg string, fields ...any) { event := h.logger.Warn() h.addFields(event, fields) event.Msg(msg) } -func (h *healthLoggerAdapter) Error(msg string, fields ...interface{}) { +func (h *healthLoggerAdapter) Error(msg string, fields ...any) { event := h.logger.Error() h.addFields(event, fields) event.Msg(msg) } // addFields safely adds key-value pairs to a zerolog event -func (h *healthLoggerAdapter) addFields(event *zerolog.Event, fields []interface{}) { +func (h *healthLoggerAdapter) addFields(event *zerolog.Event, fields []any) { for i := 0; i < len(fields)-1; i += 2 { key, ok := fields[i].(string) if !ok { diff --git a/framework/observability.go b/framework/observability.go index aeba689..0ea4d1a 100644 --- a/framework/observability.go +++ b/framework/observability.go @@ -236,16 +236,16 @@ func (om *ObservabilityManager) Shutdown(ctx context.Context) error { return nil } -// GetTracer returns a tracer for the given name. -func (om *ObservabilityManager) GetTracer(name string) oteltrace.Tracer { +// Tracer returns a tracer for the given name. +func (om *ObservabilityManager) Tracer(name string) oteltrace.Tracer { if om.traceProvider == nil { return otel.Tracer(name) } return om.traceProvider.Tracer(name) } -// GetMeter returns a meter for the given name. -func (om *ObservabilityManager) GetMeter(name string) otelmetric.Meter { +// Meter returns a meter for the given name. +func (om *ObservabilityManager) Meter(name string) otelmetric.Meter { if om.meterProvider == nil { return otel.Meter(name) } diff --git a/health/registry.go b/health/registry.go index 8dc5051..4053426 100644 --- a/health/registry.go +++ b/health/registry.go @@ -73,19 +73,19 @@ type Registry struct { // Logger interface for health check logging. type Logger interface { - Debug(msg string, fields ...interface{}) - Info(msg string, fields ...interface{}) - Warn(msg string, fields ...interface{}) - Error(msg string, fields ...interface{}) + Debug(msg string, fields ...any) + Info(msg string, fields ...any) + Warn(msg string, fields ...any) + Error(msg string, fields ...any) } // NoopLogger is a logger that does nothing. type NoopLogger struct{} -func (NoopLogger) Debug(msg string, fields ...interface{}) {} -func (NoopLogger) Info(msg string, fields ...interface{}) {} -func (NoopLogger) Warn(msg string, fields ...interface{}) {} -func (NoopLogger) Error(msg string, fields ...interface{}) {} +func (NoopLogger) Debug(msg string, fields ...any) {} +func (NoopLogger) Info(msg string, fields ...any) {} +func (NoopLogger) Warn(msg string, fields ...any) {} +func (NoopLogger) Error(msg string, fields ...any) {} // NewRegistry creates a new health check registry. func NewRegistry(logger Logger) *Registry { @@ -341,7 +341,7 @@ func (r *Registry) executeOne(parent context.Context, timeout time.Duration, nam // // 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 { +func (r *Registry) CheckLiveness(ctx context.Context) Report { r.mu.RLock() if r.started { status := r.snapshotStatus(probeLiveness) @@ -363,7 +363,7 @@ func (r *Registry) CheckLiveness(ctx context.Context) HealthStatus { // // 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 { +func (r *Registry) CheckReadiness(ctx context.Context) Report { r.mu.RLock() if r.started { status := r.snapshotStatus(probeReadiness) @@ -386,7 +386,7 @@ func (r *Registry) CheckReadiness(ctx context.Context) HealthStatus { // 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 { +func applyReadyGate(status Report, ready bool) Report { if !ready { status.Status = StatusUnhealthy status.Message = "Service not marked as ready" @@ -399,7 +399,7 @@ func applyReadyGate(status HealthStatus, ready bool) HealthStatus { // 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 { +func (r *Registry) CheckHealth(ctx context.Context) Report { r.mu.RLock() if r.started { liveness := r.snapshotStatus(probeLiveness) @@ -414,10 +414,10 @@ func (r *Registry) CheckHealth(ctx context.Context) HealthStatus { return combineHealth(liveness, readiness) } -// combineHealth merges a liveness and a readiness HealthStatus into the +// combineHealth merges a liveness and a readiness Report into the // combined status returned by CheckHealth. -func combineHealth(liveness, readiness HealthStatus) HealthStatus { - status := HealthStatus{ +func combineHealth(liveness, readiness Report) Report { + status := Report{ Status: StatusHealthy, Message: "All checks passed", Timestamp: time.Now().UTC(), @@ -444,16 +444,16 @@ func combineHealth(liveness, readiness HealthStatus) HealthStatus { return status } -// snapshotStatus builds a HealthStatus from the cached background results +// snapshotStatus builds a Report 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 { +func (r *Registry) snapshotStatus(kind *probeKind) Report { if len(r.checks) == 0 { - return HealthStatus{ + return Report{ Status: StatusHealthy, Message: "No health checks registered", Timestamp: time.Now().UTC(), @@ -496,7 +496,7 @@ func (r *Registry) snapshotStatus(kind *probeKind) HealthStatus { message = "One or more required checks failed" } - return HealthStatus{ + return Report{ Status: status, Message: message, Timestamp: time.Now().UTC(), @@ -507,9 +507,9 @@ func (r *Registry) snapshotStatus(kind *probeKind) HealthStatus { // 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 { +func (r *Registry) performChecks(ctx context.Context, checks map[string]Check, configs map[string]CheckConfig, kind *probeKind) Report { if len(checks) == 0 { - return HealthStatus{ + return Report{ Status: StatusHealthy, Message: "No health checks registered", Timestamp: time.Now().UTC(), @@ -560,7 +560,7 @@ func (r *Registry) performChecks(ctx context.Context, checks map[string]Check, c message = "One or more required checks failed" } - return HealthStatus{ + return Report{ Status: status, Message: message, Timestamp: time.Now().UTC(), @@ -568,8 +568,8 @@ func (r *Registry) performChecks(ctx context.Context, checks map[string]Check, c } } -// GetRegisteredChecks returns the names of all registered checks. -func (r *Registry) GetRegisteredChecks() []string { +// RegisteredChecks returns the names of all registered checks. +func (r *Registry) RegisteredChecks() []string { r.mu.RLock() defer r.mu.RUnlock() @@ -580,8 +580,8 @@ func (r *Registry) GetRegisteredChecks() []string { return names } -// GetCheckConfig returns the configuration for a specific check. -func (r *Registry) GetCheckConfig(name string) (CheckConfig, bool) { +// CheckConfig returns the configuration for a specific check. +func (r *Registry) CheckConfig(name string) (CheckConfig, bool) { r.mu.RLock() defer r.mu.RUnlock() diff --git a/health/registry_test.go b/health/registry_test.go index 1f62d70..a76b71b 100644 --- a/health/registry_test.go +++ b/health/registry_test.go @@ -374,14 +374,14 @@ func TestRegistry_MustRegister_Panic(t *testing.T) { registry.MustRegister(check, cfg) } -func TestRegistry_GetRegisteredChecks(t *testing.T) { +func TestRegistry_RegisteredChecks(t *testing.T) { registry := NewRegistry(nil) check1 := NewAlwaysHealthyCheck("check-1") check2 := NewAlwaysHealthyCheck("check-2") registry.MustRegister(check1, DefaultCheckConfig("check-1")) registry.MustRegister(check2, DefaultCheckConfig("check-2")) - names := registry.GetRegisteredChecks() + names := registry.RegisteredChecks() if len(names) != 2 { t.Fatalf("expected 2 registered checks, got %d", len(names)) } @@ -394,15 +394,15 @@ func TestRegistry_GetRegisteredChecks(t *testing.T) { } } -func TestRegistry_GetCheckConfig(t *testing.T) { +func TestRegistry_CheckConfig(t *testing.T) { registry := NewRegistry(nil) cfg := DefaultCheckConfig("db") cfg.Required = false registry.MustRegister(NewAlwaysHealthyCheck("db"), cfg) - got, ok := registry.GetCheckConfig("db") + got, ok := registry.CheckConfig("db") if !ok { - t.Fatal("expected GetCheckConfig to return true for registered check") + t.Fatal("expected CheckConfig to return true for registered check") } if got.Name != "db" { t.Errorf("expected config name 'db', got %q", got.Name) @@ -411,9 +411,9 @@ func TestRegistry_GetCheckConfig(t *testing.T) { t.Error("expected Required=false") } - _, ok2 := registry.GetCheckConfig("nonexistent") + _, ok2 := registry.CheckConfig("nonexistent") if ok2 { - t.Error("expected GetCheckConfig to return false for unknown check") + t.Error("expected CheckConfig to return false for unknown check") } } @@ -575,7 +575,7 @@ func TestRegistry_Runner_CachesResultsAndServesStaleReads(t *testing.T) { // 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) + done := make(chan Report, 1) go func() { done <- registry.CheckReadiness(context.Background()) }() select { diff --git a/health/status.go b/health/status.go index dc1dc56..63c4293 100644 --- a/health/status.go +++ b/health/status.go @@ -5,9 +5,9 @@ import ( "time" ) -// HealthStatus represents the overall health status of a service, +// Report represents the overall health status of a service, // including aggregated results from individual health checks. -type HealthStatus struct { +type Report struct { Status Status `json:"status"` Message string `json:"message,omitempty"` Timestamp time.Time `json:"timestamp"` @@ -18,24 +18,24 @@ type HealthStatus struct { } // IsHealthy returns true if the overall status is healthy. -func (h HealthStatus) IsHealthy() bool { +func (h Report) IsHealthy() bool { return h.Status == StatusHealthy } // IsReady returns true if the status indicates the service is ready to serve requests. // This is an alias for IsHealthy for readiness checks. -func (h HealthStatus) IsReady() bool { +func (h Report) IsReady() bool { return h.IsHealthy() } // IsLive returns true if the status indicates the service is alive. // For liveness checks, we consider the service live if it's not explicitly unhealthy. -func (h HealthStatus) IsLive() bool { +func (h Report) IsLive() bool { return h.Status != StatusUnhealthy } // HTTPStatus returns the appropriate HTTP status code for this health status. -func (h HealthStatus) HTTPStatus() int { +func (h Report) HTTPStatus() int { switch h.Status { case StatusHealthy: return 200 // OK @@ -49,12 +49,12 @@ func (h HealthStatus) HTTPStatus() int { } // JSON returns the JSON representation of the health status. -func (h HealthStatus) JSON() ([]byte, error) { +func (h Report) JSON() ([]byte, error) { return json.Marshal(h) } // String returns a string representation of the health status. -func (h HealthStatus) String() string { +func (h Report) String() string { data, err := h.JSON() if err != nil { return "{\"status\":\"error\",\"message\":\"failed to serialize health status\"}" @@ -62,12 +62,12 @@ func (h HealthStatus) String() string { return string(data) } -// Redacted returns a copy of the HealthStatus safe for exposure over +// Redacted returns a copy of the Report safe for exposure over // unauthenticated interfaces such as the public HTTP health endpoints. // Every entry in Details has its raw error detail stripped via // CheckResult.Redacted, while the overall Status, Message, and per-check // name/status/duration/required metadata are preserved. -func (h HealthStatus) Redacted() HealthStatus { +func (h Report) Redacted() Report { if len(h.Details) == 0 { return h } @@ -82,7 +82,7 @@ func (h HealthStatus) Redacted() HealthStatus { } // FailedChecks returns a slice of check results that failed. -func (h HealthStatus) FailedChecks() []CheckResult { +func (h Report) FailedChecks() []CheckResult { var failed []CheckResult for _, result := range h.Details { if !result.IsHealthy() { @@ -93,7 +93,7 @@ func (h HealthStatus) FailedChecks() []CheckResult { } // RequiredFailedChecks returns a slice of required check results that failed. -func (h HealthStatus) RequiredFailedChecks() []CheckResult { +func (h Report) RequiredFailedChecks() []CheckResult { var failed []CheckResult for _, result := range h.Details { if result.Required && !result.IsHealthy() { @@ -111,8 +111,8 @@ type HealthySummary struct { Required int `json:"required"` } -// GetHealthySummary returns a summary of the health check results. -func (h HealthStatus) GetHealthySummary() HealthySummary { +// HealthySummary returns a summary of the health check results. +func (h Report) HealthySummary() HealthySummary { summary := HealthySummary{ Total: len(h.Details), } @@ -132,29 +132,29 @@ func (h HealthStatus) GetHealthySummary() HealthySummary { } // WithService sets the service name on the health status. -func (h HealthStatus) WithService(service string) HealthStatus { +func (h Report) WithService(service string) Report { h.Service = service return h } // WithVersion sets the version on the health status. -func (h HealthStatus) WithVersion(version string) HealthStatus { +func (h Report) WithVersion(version string) Report { h.Version = version return h } // WithUptime sets the uptime on the health status. -func (h HealthStatus) WithUptime(uptime time.Duration) HealthStatus { +func (h Report) WithUptime(uptime time.Duration) Report { h.Uptime = uptime.String() return h } -// NewHealthyStatus creates a new healthy status with the given message. -func NewHealthyStatus(message string) HealthStatus { +// NewHealthyReport creates a new healthy status with the given message. +func NewHealthyReport(message string) Report { if message == "" { message = "Service is healthy" } - return HealthStatus{ + return Report{ Status: StatusHealthy, Message: message, Timestamp: time.Now().UTC(), @@ -162,12 +162,12 @@ func NewHealthyStatus(message string) HealthStatus { } } -// NewUnhealthyStatus creates a new unhealthy status with the given message. -func NewUnhealthyStatus(message string) HealthStatus { +// NewUnhealthyReport creates a new unhealthy status with the given message. +func NewUnhealthyReport(message string) Report { if message == "" { message = "Service is unhealthy" } - return HealthStatus{ + return Report{ Status: StatusUnhealthy, Message: message, Timestamp: time.Now().UTC(), @@ -175,12 +175,12 @@ func NewUnhealthyStatus(message string) HealthStatus { } } -// NewUnknownStatus creates a new unknown status with the given message. -func NewUnknownStatus(message string) HealthStatus { +// NewUnknownReport creates a new unknown status with the given message. +func NewUnknownReport(message string) Report { if message == "" { message = "Service health is unknown" } - return HealthStatus{ + return Report{ Status: StatusUnknown, Message: message, Timestamp: time.Now().UTC(), @@ -195,7 +195,7 @@ type StatusResponse struct { } // NewStatusResponse creates a new status response from a health status. -func NewStatusResponse(status HealthStatus) StatusResponse { +func NewStatusResponse(status Report) StatusResponse { return StatusResponse{ Status: status.Status, Message: status.Message, diff --git a/health/status_test.go b/health/status_test.go index 4b25717..f65a625 100644 --- a/health/status_test.go +++ b/health/status_test.go @@ -7,50 +7,50 @@ import ( "time" ) -func TestHealthStatus_IsHealthy(t *testing.T) { - healthy := NewHealthyStatus("all good") +func TestReport_IsHealthy(t *testing.T) { + healthy := NewHealthyReport("all good") if !healthy.IsHealthy() { t.Error("expected healthy status to be healthy") } - unhealthy := NewUnhealthyStatus("broken") + unhealthy := NewUnhealthyReport("broken") if unhealthy.IsHealthy() { t.Error("expected unhealthy status to not be healthy") } } -func TestHealthStatus_IsReady(t *testing.T) { - healthy := NewHealthyStatus("") +func TestReport_IsReady(t *testing.T) { + healthy := NewHealthyReport("") if !healthy.IsReady() { t.Error("expected healthy status to be ready") } } -func TestHealthStatus_IsLive(t *testing.T) { - healthy := NewHealthyStatus("") +func TestReport_IsLive(t *testing.T) { + healthy := NewHealthyReport("") if !healthy.IsLive() { t.Error("expected healthy status to be live") } - unknown := NewUnknownStatus("") + unknown := NewUnknownReport("") if !unknown.IsLive() { t.Error("expected unknown status to be live (not explicitly unhealthy)") } - unhealthy := NewUnhealthyStatus("") + unhealthy := NewUnhealthyReport("") if unhealthy.IsLive() { t.Error("expected unhealthy status to not be live") } } -func TestHealthStatus_HTTPStatus(t *testing.T) { +func TestReport_HTTPStatus(t *testing.T) { tests := []struct { - status HealthStatus + status Report expected int }{ - {NewHealthyStatus(""), 200}, - {NewUnhealthyStatus(""), 503}, - {NewUnknownStatus(""), 503}, + {NewHealthyReport(""), 200}, + {NewUnhealthyReport(""), 503}, + {NewUnknownReport(""), 503}, } for _, tt := range tests { code := tt.status.HTTPStatus() @@ -60,8 +60,8 @@ func TestHealthStatus_HTTPStatus(t *testing.T) { } } -func TestHealthStatus_JSON(t *testing.T) { - status := NewHealthyStatus("all good") +func TestReport_JSON(t *testing.T) { + status := NewHealthyReport("all good") data, err := status.JSON() if err != nil { t.Fatalf("unexpected error: %v", err) @@ -71,16 +71,16 @@ func TestHealthStatus_JSON(t *testing.T) { } } -func TestHealthStatus_String(t *testing.T) { - status := NewHealthyStatus("ok") +func TestReport_String(t *testing.T) { + status := NewHealthyReport("ok") s := status.String() if !strings.Contains(s, "healthy") { t.Errorf("expected String() to contain 'healthy', got: %s", s) } } -func TestHealthStatus_FailedChecks(t *testing.T) { - status := NewHealthyStatus("") +func TestReport_FailedChecks(t *testing.T) { + status := NewHealthyReport("") status.Details["db"] = NewCheckResult("db", true, time.Millisecond, nil) status.Details["redis"] = NewCheckResult("redis", false, time.Millisecond, errTest("connection refused")) @@ -93,8 +93,8 @@ func TestHealthStatus_FailedChecks(t *testing.T) { } } -func TestHealthStatus_RequiredFailedChecks(t *testing.T) { - status := NewHealthyStatus("") +func TestReport_RequiredFailedChecks(t *testing.T) { + status := NewHealthyReport("") // required, healthy status.Details["db"] = NewCheckResult("db", true, time.Millisecond, nil) // required, unhealthy @@ -111,13 +111,13 @@ func TestHealthStatus_RequiredFailedChecks(t *testing.T) { } } -func TestHealthStatus_GetHealthySummary(t *testing.T) { - status := NewHealthyStatus("") +func TestReport_HealthySummary(t *testing.T) { + status := NewHealthyReport("") status.Details["db"] = NewCheckResult("db", true, time.Millisecond, nil) status.Details["redis"] = NewCheckResult("redis", true, time.Millisecond, errTest("failed")) status.Details["cache"] = NewCheckResult("cache", false, time.Millisecond, nil) - summary := status.GetHealthySummary() + summary := status.HealthySummary() if summary.Total != 3 { t.Errorf("expected Total=3, got %d", summary.Total) } @@ -132,50 +132,50 @@ func TestHealthStatus_GetHealthySummary(t *testing.T) { } } -func TestHealthStatus_WithService(t *testing.T) { - status := NewHealthyStatus("").WithService("my-service") +func TestReport_WithService(t *testing.T) { + status := NewHealthyReport("").WithService("my-service") if status.Service != "my-service" { t.Errorf("expected Service='my-service', got %q", status.Service) } } -func TestHealthStatus_WithVersion(t *testing.T) { - status := NewHealthyStatus("").WithVersion("1.2.3") +func TestReport_WithVersion(t *testing.T) { + status := NewHealthyReport("").WithVersion("1.2.3") if status.Version != "1.2.3" { t.Errorf("expected Version='1.2.3', got %q", status.Version) } } -func TestHealthStatus_WithUptime(t *testing.T) { - status := NewHealthyStatus("").WithUptime(5 * time.Minute) +func TestReport_WithUptime(t *testing.T) { + status := NewHealthyReport("").WithUptime(5 * time.Minute) if status.Uptime == "" { t.Error("expected non-empty Uptime") } } -func TestNewHealthyStatus_EmptyMessage(t *testing.T) { - status := NewHealthyStatus("") +func TestNewHealthyReport_EmptyMessage(t *testing.T) { + status := NewHealthyReport("") if status.Message == "" { t.Error("expected default message for empty input") } } -func TestNewUnhealthyStatus_EmptyMessage(t *testing.T) { - status := NewUnhealthyStatus("") +func TestNewUnhealthyReport_EmptyMessage(t *testing.T) { + status := NewUnhealthyReport("") if status.Message == "" { t.Error("expected default message for empty input") } } -func TestNewUnknownStatus_EmptyMessage(t *testing.T) { - status := NewUnknownStatus("") +func TestNewUnknownReport_EmptyMessage(t *testing.T) { + status := NewUnknownReport("") if status.Message == "" { t.Error("expected default message for empty input") } } func TestStatusResponse_JSON(t *testing.T) { - status := NewHealthyStatus("ok") + status := NewHealthyReport("ok") resp := NewStatusResponse(status) data, err := resp.JSON() @@ -235,7 +235,7 @@ func TestCheckResult_IsHealthy(t *testing.T) { } func TestStatusResponse_Creation(t *testing.T) { - hs := HealthStatus{ + hs := Report{ Status: StatusHealthy, Message: "test message", } @@ -251,10 +251,10 @@ func TestStatusResponse_Creation(t *testing.T) { } } -// --- HealthStatus.Redacted tests --- +// --- Report.Redacted tests --- -func TestHealthStatus_Redacted_StripsCheckErrors(t *testing.T) { - status := HealthStatus{ +func TestReport_Redacted_StripsCheckErrors(t *testing.T) { + status := Report{ Status: StatusUnhealthy, Message: "One or more checks failed", Details: map[string]CheckResult{ @@ -283,8 +283,8 @@ func TestHealthStatus_Redacted_StripsCheckErrors(t *testing.T) { } } -func TestHealthStatus_Redacted_NoDetails(t *testing.T) { - status := NewHealthyStatus("") +func TestReport_Redacted_NoDetails(t *testing.T) { + status := NewHealthyReport("") redacted := status.Redacted()