fix(consul): Fix health checks for gRPC services in Docker

- Add gRPC health check support to Consul registry
  - Services are gRPC-only, not HTTP
  - Consul was trying HTTP health checks which failed
  - Now uses gRPC health checks via grpc.health.v1.Health service

- Update HealthCheckConfig to support both HTTP and gRPC
  - Add GRPC field for gRPC service name
  - Add UseGRPC flag to choose health check type
  - Default to gRPC for services (use_grpc: true in config)

- Fix service address registration in Docker
  - Services now register with Docker service name (e.g., auth-service)
  - Allows Consul to reach services via Docker network DNS
  - Falls back to localhost for local development

- Update default.yaml to enable gRPC health checks
  - Set use_grpc: true
  - Set grpc: grpc.health.v1.Health

This fixes services being deregistered from Consul due to failed
HTTP health checks. Services will now pass gRPC health checks.
This commit is contained in:
2025-11-06 21:17:33 +01:00
parent 54e1866997
commit b02c1d44c8
7 changed files with 58 additions and 8 deletions

View File

@@ -183,8 +183,14 @@ func registerLifecycle(
serviceID := fmt.Sprintf("audit-service-%d", time.Now().Unix()) serviceID := fmt.Sprintf("audit-service-%d", time.Now().Unix())
host := cfg.GetString("services.audit.host") host := cfg.GetString("services.audit.host")
if host == "" { if host == "" {
// In Docker, use service name for Consul to reach the service
// For local development, use localhost
if os.Getenv("ENVIRONMENT") == "production" || os.Getenv("DOCKER") == "true" {
host = "audit-service" // Docker service name
} else {
host = "localhost" host = "localhost"
} }
}
port := grpcServer.Port() port := grpcServer.Port()
instance := &registry.ServiceInstance{ instance := &registry.ServiceInstance{

View File

@@ -190,8 +190,14 @@ func registerLifecycle(
serviceID := fmt.Sprintf("auth-service-%d", time.Now().Unix()) serviceID := fmt.Sprintf("auth-service-%d", time.Now().Unix())
host := cfg.GetString("services.auth.host") host := cfg.GetString("services.auth.host")
if host == "" { if host == "" {
// In Docker, use service name for Consul to reach the service
// For local development, use localhost
if os.Getenv("ENVIRONMENT") == "production" || os.Getenv("DOCKER") == "true" {
host = "auth-service" // Docker service name
} else {
host = "localhost" host = "localhost"
} }
}
port := grpcServer.Port() port := grpcServer.Port()
instance := &registry.ServiceInstance{ instance := &registry.ServiceInstance{

View File

@@ -183,8 +183,14 @@ func registerLifecycle(
serviceID := fmt.Sprintf("authz-service-%d", time.Now().Unix()) serviceID := fmt.Sprintf("authz-service-%d", time.Now().Unix())
host := cfg.GetString("services.authz.host") host := cfg.GetString("services.authz.host")
if host == "" { if host == "" {
// In Docker, use service name for Consul to reach the service
// For local development, use localhost
if os.Getenv("ENVIRONMENT") == "production" || os.Getenv("DOCKER") == "true" {
host = "authz-service" // Docker service name
} else {
host = "localhost" host = "localhost"
} }
}
port := grpcServer.Port() port := grpcServer.Port()
instance := &registry.ServiceInstance{ instance := &registry.ServiceInstance{

View File

@@ -183,8 +183,14 @@ func registerLifecycle(
serviceID := fmt.Sprintf("identity-service-%d", time.Now().Unix()) serviceID := fmt.Sprintf("identity-service-%d", time.Now().Unix())
host := cfg.GetString("services.identity.host") host := cfg.GetString("services.identity.host")
if host == "" { if host == "" {
// In Docker, use service name for Consul to reach the service
// For local development, use localhost
if os.Getenv("ENVIRONMENT") == "production" || os.Getenv("DOCKER") == "true" {
host = "identity-service" // Docker service name
} else {
host = "localhost" host = "localhost"
} }
}
port := grpcServer.Port() port := grpcServer.Port()
instance := &registry.ServiceInstance{ instance := &registry.ServiceInstance{

View File

@@ -36,6 +36,8 @@ registry:
timeout: "3s" timeout: "3s"
deregister_after: "30s" deregister_after: "30s"
http: "/healthz" http: "/healthz"
grpc: "grpc.health.v1.Health"
use_grpc: true
services: services:
audit: audit:

View File

@@ -218,8 +218,14 @@ func ProvideServiceRegistry() fx.Option {
healthCheckDeregisterAfter = 30 * time.Second healthCheckDeregisterAfter = 30 * time.Second
} }
healthCheckHTTP := cfg.GetString("registry.consul.health_check.http") healthCheckHTTP := cfg.GetString("registry.consul.health_check.http")
if healthCheckHTTP == "" { healthCheckGRPC := cfg.GetString("registry.consul.health_check.grpc")
healthCheckHTTP = "/healthz" useGRPC := cfg.GetBool("registry.consul.health_check.use_grpc")
// Default to gRPC if not explicitly set (services are gRPC by default)
if !cfg.IsSet("registry.consul.health_check.use_grpc") {
useGRPC = true
}
if healthCheckGRPC == "" {
healthCheckGRPC = "grpc.health.v1.Health"
} }
consulCfg.HealthCheck = consul.HealthCheckConfig{ consulCfg.HealthCheck = consul.HealthCheckConfig{
@@ -227,6 +233,8 @@ func ProvideServiceRegistry() fx.Option {
Timeout: healthCheckTimeout, Timeout: healthCheckTimeout,
DeregisterAfter: healthCheckDeregisterAfter, DeregisterAfter: healthCheckDeregisterAfter,
HTTP: healthCheckHTTP, HTTP: healthCheckHTTP,
GRPC: healthCheckGRPC,
UseGRPC: useGRPC,
} }
return consul.NewRegistry(consulCfg) return consul.NewRegistry(consulCfg)

View File

@@ -29,7 +29,9 @@ type HealthCheckConfig struct {
Interval time.Duration // Health check interval Interval time.Duration // Health check interval
Timeout time.Duration // Health check timeout Timeout time.Duration // Health check timeout
DeregisterAfter time.Duration // Time to wait before deregistering unhealthy service DeregisterAfter time.Duration // Time to wait before deregistering unhealthy service
HTTP string // HTTP health check endpoint (e.g., "/healthz") HTTP string // HTTP health check endpoint (e.g., "/healthz") - for HTTP services
GRPC string // gRPC health check service name (e.g., "grpc.health.v1.Health") - for gRPC services
UseGRPC bool // Whether to use gRPC health checks instead of HTTP
} }
// NewRegistry creates a new Consul-based service registry. // NewRegistry creates a new Consul-based service registry.
@@ -68,7 +70,21 @@ func (r *ConsulRegistry) Register(ctx context.Context, service *registry.Service
} }
// Add health check if configured // Add health check if configured
if r.config.HealthCheck.HTTP != "" { if r.config.HealthCheck.UseGRPC {
// Use gRPC health check for gRPC services
// Format: host:port/service or host:port (uses default health service)
grpcAddr := fmt.Sprintf("%s:%d", service.Address, service.Port)
if r.config.HealthCheck.GRPC != "" {
grpcAddr = fmt.Sprintf("%s:%d/%s", service.Address, service.Port, r.config.HealthCheck.GRPC)
}
registration.Check = &consulapi.AgentServiceCheck{
GRPC: grpcAddr,
Interval: r.config.HealthCheck.Interval.String(),
Timeout: r.config.HealthCheck.Timeout.String(),
DeregisterCriticalServiceAfter: r.config.HealthCheck.DeregisterAfter.String(),
}
} else if r.config.HealthCheck.HTTP != "" {
// Use HTTP health check for HTTP services
healthCheckURL := fmt.Sprintf("http://%s:%d%s", service.Address, service.Port, r.config.HealthCheck.HTTP) healthCheckURL := fmt.Sprintf("http://%s:%d%s", service.Address, service.Port, r.config.HealthCheck.HTTP)
registration.Check = &consulapi.AgentServiceCheck{ registration.Check = &consulapi.AgentServiceCheck{
HTTP: healthCheckURL, HTTP: healthCheckURL,