Skip to content

Conversation

@capcom6
Copy link
Member

@capcom6 capcom6 commented Oct 23, 2025

Summary by CodeRabbit

  • New Features

    • Added granular health endpoints: /health/live (liveness), /health/ready (readiness), /health/startup (startup); /health now maps to readiness.
    • Health responses now include system runtime metrics (goroutines, memory) and a database connectivity check with failure counts.
  • Chores

    • Updated Kubernetes probes, startup timings and Docker HEALTHCHECK to use the new probe endpoints.
    • Added local request examples and updated deployment image tag.

@coderabbitai
Copy link

coderabbitai bot commented Oct 23, 2025

Walkthrough

Reworks health into separate startup/readiness/liveness probes with new endpoints and handlers; moves DB health provider into the db module; converts package-level fx.Module variables to Module() constructors; updates Dockerfile healthcheck, Helm probes/values, API requests, go.mod, and related wiring.

Changes

Cohort / File(s) Summary
API & local requests
api/requests.http
Replaced base /health with /3rdparty/v1/health endpoints, added /3rdparty/v1/health/{startup,ready,live} and appended local /health/{startup,ready,live} requests.
Dockerfile
build/package/Dockerfile
Changed HEALTHCHECK target to http://localhost:3000/health/live.
K8s probes / Helm
deployments/helm-chart/templates/deployment.yaml, deployments/helm-chart/values.yaml
Added livenessProbe (/health/live), readinessProbe (/health/ready), and startupProbe (/health/startup) with timing params; updated image tag in values.yaml.
App wiring
internal/sms-gateway/app.go
Switched module registrations from package-level module variables to constructor calls (e.g., appdb.Module() and health.Module()).
DB module: new provider & Module()
internal/sms-gateway/modules/db/module.go, internal/sms-gateway/modules/db/health.go
Added Module() fx.Option and a DB health provider that pings the DB, tracks failed-pings, and exposes readiness check details.
Health module: providers, types & Module()
internal/sms-gateway/modules/health/module.go, internal/sms-gateway/modules/health/health.go, internal/sms-gateway/modules/health/types.go
Replaced package-level Module with Module() function; added AsHealthProvider; introduced a system provider (goroutines/memory); renamed CheckCheckResult, expanded CheckDetail; split provider interface into StartedProbe/ReadyProbe/LiveProbe.
Health service logic
internal/sms-gateway/modules/health/service.go
Replaced single HealthCheck with probe-specific helpers and public methods CheckLiveness, CheckReadiness, CheckStartup; added checkProvider helper and centralized error handling.
HTTP handler
internal/sms-gateway/handlers/health.go
Refactored to newHealthHandler, added getLiveness, getReadiness, getStartup; mapped /health to readiness; centralized response via writeProbe; uses lo.MapValues for checks.
Removed / relocated
internal/sms-gateway/modules/health/cli.go, internal/sms-gateway/modules/health/db.go
Deleted CLI test helper and removed old DB provider from health module (DB provider moved to db module).
Dependencies
go.mod
Upgraded github.com/capcom6/go-infra-fx from v0.4.0 → v0.5.2 (and added/updated related deps).
Minor formatting
internal/sms-gateway/modules/messages/metrics.go
Whitespace/alignment adjustments only.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    rect rgb(245,250,255)
    participant Client
    participant Handler as HealthHandler
    participant Service as HealthService
    participant ProvSys as SystemProvider
    participant ProvDB as DBProvider
    end

    Client->>Handler: GET /health  (mapped → readiness)
    Client->>Handler: GET /health/live
    Client->>Handler: GET /health/ready
    Client->>Handler: GET /health/startup

    Handler->>Service: CheckLiveness/CheckReadiness/CheckStartup(ctx)
    par probe providers
        Service->>ProvSys: Live/Ready/StartedProbe(ctx)
        Service->>ProvDB: Live/Ready/StartedProbe(ctx)
    end
    ProvSys-->>Service: Checks (goroutines, memory)
    ProvDB-->>Service: Checks (ping, failed-pings)
    Service-->>Handler: CheckResult (aggregated)
    Handler-->>Client: 200 (pass/warn) or 503 (fail) + JSON body
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay extra attention to:
    • Types and interface changes in internal/sms-gateway/modules/health/types.go (renaming to CheckResult and Status() semantics).
    • Probe orchestration and error handling in internal/sms-gateway/modules/health/service.go (checkProvider and new probe methods).
    • DB provider implementation and fx wiring in internal/sms-gateway/modules/db/module.go and .../db/health.go.
    • Handler routing and serialization in internal/sms-gateway/handlers/health.go (mapping /health → readiness and use of lo.MapValues).

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "[health] improve health reporting" is directly related to the changeset's primary objective. The PR implements a significant refactoring of the health checking system to use Kubernetes-style probes with separate liveness, readiness, and startup endpoints, replacing the previous single health endpoint. The title accurately captures this core improvement to health reporting mechanisms across the codebase, including updates to HTTP handlers, Kubernetes deployment configurations, Docker health checks, and the underlying health service architecture. While the title could be more specific about the probe implementation details, it is sufficiently clear and specific to communicate the main change to a teammate scanning the repository history.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch handlers/improve-health-handler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (10)
build/package/Dockerfile (1)

51-51: Healthcheck path updated — add curl timeouts to honor 5s probe.

To avoid the curl call exceeding Docker’s 5s timeout, cap total/connect time.

-    CMD curl -fs http://localhost:3000/health/live
+    CMD curl -fsS --connect-timeout 2 -m 4 http://localhost:3000/health/live

Confirm /health/live is a pure liveness check (no DB/cache calls) so restarts don’t flap on dependencies.

internal/sms-gateway/handlers/health.go (1)

96-113: Minor: deduplicate probe response writing.

Liveness/readiness/startup share the same status selection + JSON response. Extract a helper to reduce repetition.

+func writeProbe(c *fiber.Ctx, r health.CheckResult) error {
+    status := fiber.StatusOK
+    if r.Status() == health.StatusFail {
+        status = fiber.StatusServiceUnavailable
+    }
+    return c.Status(status).JSON(makeResponse(r))
+}
@@
- result := h.healthSvc.CheckLiveness(c.UserContext())
- statusCode := fiber.StatusOK
- if result.Status() == health.StatusFail {
-     statusCode = fiber.StatusServiceUnavailable
- }
- return c.Status(statusCode).JSON(makeResponse(result))
+ return writeProbe(c, h.healthSvc.CheckLiveness(c.UserContext()))
@@
- result := h.healthSvc.CheckReadiness(c.UserContext())
- statusCode := fiber.StatusOK
- if result.Status() == health.StatusFail {
-     statusCode = fiber.StatusServiceUnavailable
- }
- return c.Status(statusCode).JSON(makeResponse(result))
+ return writeProbe(c, h.healthSvc.CheckReadiness(c.UserContext()))
@@
- result := h.healthSvc.CheckStartup(c.UserContext())
- statusCode := fiber.StatusOK
- if result.Status() == health.StatusFail {
-     statusCode = fiber.StatusServiceUnavailable
- }
- return c.Status(statusCode).JSON(makeResponse(result))
+ return writeProbe(c, h.healthSvc.CheckStartup(c.UserContext()))
deployments/helm-chart/templates/deployment.yaml (1)

95-101: Service port "http" is correctly exposed — probe port references are valid.

The Service in deployments/helm-chart/templates/service.yaml properly exposes a port named "http" (line 17), matching the port references in all probe blocks. The container also defines the "http" port (deployment.yaml:47), and the service correctly maps targetPort: http to reference it by name.

Templating probe values via .Values.probes.liveness, .Values.probes.readiness, and .Values.probes.startup remains a valid optimization for per-environment configuration tuning, though the hard-coded defaults are functionally sound.

internal/sms-gateway/modules/health/types.go (3)

27-30: Consider JSON tags on CheckResult.Checks to lock response casing and avoid accidental field renames in API output later (e.g., json:"checks").


50-61: ObservedValue is int; for metrics like memory or counters, int64 (or uint64) is safer and avoids narrowing on 32‑bit. Consider ObservedValue int64.


69-71: Interface looks good. Minor naming nit: Kubernetes uses “startupProbe”; you’re using StartedProbe. If you plan to map 1:1 to HTTP routes, consider StartupProbe for clarity.

internal/sms-gateway/modules/health/health.go (3)

21-51: LiveProbe works. Minor cleanups:

  • Store NumGoroutine() once.
  • Use a named const for 1 GiB to avoid magic numbers and match MiB unit in output.
  • Return the alias type Checks{...} for clarity.
 func (h *health) LiveProbe(ctx context.Context) (Checks, error) {
   var m runtime.MemStats
   runtime.ReadMemStats(&m)
 
   // Basic runtime health checks
-  goroutineCheck := CheckDetail{
+  g := runtime.NumGoroutine()
+  goroutineCheck := CheckDetail{
     Description:   "Number of goroutines",
-    ObservedValue: int(runtime.NumGoroutine()),
+    ObservedValue: int(g),
     ObservedUnit:  "goroutines",
     Status:        StatusPass,
   }
 
-  memoryCheck := CheckDetail{
+  const oneGiB = 1024 * 1024 * 1024
+  memoryCheck := CheckDetail{
     Description:   "Memory usage",
     ObservedValue: int(m.Alloc / 1024 / 1024), // MiB
     ObservedUnit:  "MiB",
     Status:        StatusPass,
   }
 
   // Check for potential memory issues
-  if m.Alloc > 1024*1024*1024 { // 1GB
+  if m.Alloc > oneGiB { // 1 GiB
     memoryCheck.Status = StatusWarn
   }
 
   // Check for excessive goroutines
-  if runtime.NumGoroutine() > 1000 {
+  if g > 1000 {
     goroutineCheck.Status = StatusWarn
   }
 
-  return map[string]CheckDetail{"goroutines": goroutineCheck, "memory": memoryCheck}, nil
+  return Checks{"goroutines": goroutineCheck, "memory": memoryCheck}, nil
 }

54-56: Return an empty map instead of nil to avoid JSON “checks: null” and keep merge logic simple: return Checks{}, nil.


59-61: Same here: prefer return Checks{}, nil to normalize responses.

internal/sms-gateway/modules/db/health.go (1)

34-53: Redundant initialization on line 38.

The ObservedValue is initialized to 0 on line 38 but unconditionally overwritten on line 50. While harmless, you can remove the redundant initialization.

Apply this diff:

 func (h *health) ReadyProbe(ctx context.Context) (healthmod.Checks, error) {
 	pingCheck := healthmod.CheckDetail{
 		Description:   "Database ping",
 		ObservedUnit:  "failed pings",
-		ObservedValue: 0,
 		Status:        healthmod.StatusPass,
 	}

Note: The logic correctly tracks consecutive failed pings across health checks. The counter persists between probe calls, which is appropriate for monitoring database connection stability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3107d85 and acba6d9.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (15)
  • api/requests.http (2 hunks)
  • build/package/Dockerfile (1 hunks)
  • deployments/helm-chart/templates/deployment.yaml (1 hunks)
  • go.mod (1 hunks)
  • internal/sms-gateway/app.go (2 hunks)
  • internal/sms-gateway/handlers/health.go (1 hunks)
  • internal/sms-gateway/modules/db/health.go (1 hunks)
  • internal/sms-gateway/modules/db/module.go (1 hunks)
  • internal/sms-gateway/modules/health/cli.go (0 hunks)
  • internal/sms-gateway/modules/health/db.go (0 hunks)
  • internal/sms-gateway/modules/health/health.go (1 hunks)
  • internal/sms-gateway/modules/health/module.go (1 hunks)
  • internal/sms-gateway/modules/health/service.go (1 hunks)
  • internal/sms-gateway/modules/health/types.go (3 hunks)
  • internal/sms-gateway/modules/messages/metrics.go (1 hunks)
💤 Files with no reviewable changes (2)
  • internal/sms-gateway/modules/health/db.go
  • internal/sms-gateway/modules/health/cli.go
🧰 Additional context used
🧬 Code graph analysis (7)
internal/sms-gateway/modules/db/module.go (2)
internal/sms-gateway/app.go (1)
  • Module (35-58)
internal/sms-gateway/modules/health/module.go (2)
  • Module (8-22)
  • AsHealthProvider (24-30)
internal/sms-gateway/modules/health/module.go (5)
internal/sms-gateway/app.go (1)
  • Module (35-58)
internal/sms-gateway/modules/db/module.go (1)
  • Module (12-22)
internal/sms-gateway/modules/health/health.go (1)
  • NewHealth (11-13)
internal/sms-gateway/modules/health/service.go (1)
  • NewService (24-30)
internal/sms-gateway/modules/health/types.go (1)
  • HealthProvider (66-72)
internal/sms-gateway/app.go (2)
internal/sms-gateway/modules/db/module.go (1)
  • Module (12-22)
internal/sms-gateway/modules/health/module.go (1)
  • Module (8-22)
internal/sms-gateway/handlers/health.go (3)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/modules/health/types.go (5)
  • Status (7-7)
  • StatusFail (13-13)
  • CheckResult (27-30)
  • Checks (64-64)
  • CheckDetail (51-61)
internal/version/version.go (2)
  • AppVersion (9-9)
  • AppReleaseID (13-17)
internal/sms-gateway/modules/health/health.go (1)
internal/sms-gateway/modules/health/types.go (6)
  • Checks (64-64)
  • CheckDetail (51-61)
  • Status (7-7)
  • StatusPass (11-11)
  • StatusWarn (12-12)
  • HealthProvider (66-72)
internal/sms-gateway/modules/db/health.go (1)
internal/sms-gateway/modules/health/types.go (6)
  • Checks (64-64)
  • CheckDetail (51-61)
  • Status (7-7)
  • StatusPass (11-11)
  • StatusFail (13-13)
  • HealthProvider (66-72)
internal/sms-gateway/modules/health/service.go (1)
internal/sms-gateway/modules/health/types.go (6)
  • HealthProvider (66-72)
  • Checks (64-64)
  • CheckResult (27-30)
  • CheckDetail (51-61)
  • Status (7-7)
  • StatusFail (13-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test
  • GitHub Check: Lint
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (15)
api/requests.http (1)

7-16: All health endpoints are properly implemented and correctly registered.

The verification confirms that:

  • Local endpoints (/health, /health/startup, /health/ready, /health/live) are registered via handlers/root.go
  • 3rdparty endpoints (/3rdparty/v1/health/*) are registered via handlers/3rdparty.go using the same healthHandler.Register() method
  • The /3rdparty/v1/health general endpoint exists and maps to the readiness probe
  • All four endpoints match the requests in api/requests.http
internal/sms-gateway/modules/messages/metrics.go (1)

13-14: No-op formatting change.

Alignment only; no behavioral impact. LGTM.

go.mod (1)

22-22: Verified: dependency correctly added and integrated.

github.com/samber/lo is under MIT License. The single usage at internal/sms-gateway/handlers/health.go:101 with lo.MapValues() is confirmed, go mod tidy succeeded, and go mod verify reports all modules verified. No issues found.

internal/sms-gateway/modules/health/types.go (2)

3-5: Imports: looks good. Nothing to change.


34-48: The review comment is incorrect for this codebase.

The repository declares Go 1.24.1 in go.mod (line 3), which is well above Go 1.21. The max() predeclared function is fully available and supported. The original code using max(level, levelFail) and max(level, levelWarn) will compile without any issues. The suggested refactoring is unnecessary.

Likely an incorrect or invalid review comment.

internal/sms-gateway/modules/health/module.go (1)

24-29: AsHealthProvider helper is clean and idiomatic for grouping providers.

internal/sms-gateway/app.go (1)

39-39: Switching to Module() constructors aligns with new module APIs. LGTM.

Also applies to: 50-50

internal/sms-gateway/modules/db/module.go (2)

7-7: Import alias for health module is correct and avoids collisions.


12-22: DB module wiring looks good. Ensure newHealth returns a type implementing HealthProvider (compile‑time assert recommended: var _ healthmod.HealthProvider = (*yourType)(nil)).

internal/sms-gateway/modules/db/health.go (4)

11-15: LGTM! Clean struct design with appropriate concurrency safety.

The use of atomic.Int64 for failedPings ensures thread-safe access when multiple probes run concurrently, and the persistent counter effectively tracks consecutive ping failures across health checks.


17-21: LGTM! Constructor is straightforward and correct.

The zero-initialization of the atomic counter is appropriate for tracking failed pings from a clean state.


24-31: LGTM! No-op probes align with Kubernetes semantics.

The database provider appropriately implements health checks only for ReadyProbe, while liveness and startup probes are no-ops. This follows the pattern where database availability affects readiness but not liveness.

Also applies to: 56-58


60-60: LGTM! Standard compile-time interface verification.

internal/sms-gateway/modules/health/service.go (2)

32-66: LGTM! Solid probe orchestration with appropriate error handling.

The implementation correctly:

  • Performs non-blocking context cancellation checks between provider iterations (lines 38-42)
  • Isolates provider failures—one provider's error doesn't prevent checking others (line 53)
  • Namespaces checks with provider names to ensure uniqueness (line 61)
  • Returns partial results if the context is cancelled mid-iteration

68-84: LGTM! Clean delegation to probe-specific methods.

The three public methods correctly delegate to the generic checkProvider helper, each invoking the appropriate probe function from the HealthProvider interface.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
internal/sms-gateway/handlers/health.go (2)

44-45: Pass context.Context via Fiber’s UserContext, not fasthttp.RequestCtx.

c.Context() returns *fasthttp.RequestCtx; health.Service expects context.Context. Use c.UserContext().

- return writeProbe(c, h.healthSvc.CheckLiveness(c.Context()))
+ return writeProbe(c, h.healthSvc.CheckLiveness(c.UserContext()))
@@
- return writeProbe(c, h.healthSvc.CheckReadiness(c.Context()))
+ return writeProbe(c, h.healthSvc.CheckReadiness(c.UserContext()))
@@
- return writeProbe(c, h.healthSvc.CheckStartup(c.Context()))
+ return writeProbe(c, h.healthSvc.CheckStartup(c.UserContext()))
#!/bin/bash
# Verify all probe handlers use UserContext()
rg -nP --type go -C1 'Check(Liveness|Readiness|Startup)\(\s*c\.(Context|UserContext)\(\)\s*\)' internal/sms-gateway/handlers

Also applies to: 58-59, 71-72


101-106: Swagger documents /3rdparty/v1/health but route isn’t wired.

Register the alias to avoid docs–implementation drift.

 func (h *healthHandler) Register(router fiber.Router) {
   router.Get("/health", h.getReadiness)
   router.Get("/health/live", h.getLiveness)
   router.Get("/health/ready", h.getReadiness)
   router.Get("/health/startup", h.getStartup)
+  router.Get("/3rdparty/v1/health", h.getReadiness)
 }
#!/bin/bash
# Confirm swagger alias exists and router wiring matches
rg -n '@Router\s+/3rdparty/v1/health\s+\[get\]' internal/sms-gateway/handlers/health.go
rg -n 'router\.Get\("/3rdparty/v1/health"' internal/sms-gateway/handlers/health.go
🧹 Nitpick comments (3)
internal/sms-gateway/modules/health/health.go (2)

21-53: Type safety for 1GiB and probe efficiency/configurability.

  • Use a typed uint64 for byte comparisons; avoid implicit constant conversions.
  • Consider making thresholds configurable; hardcoded 1 GiB / 1000 goroutines will be env‑dependent.
  • ReadMemStats is relatively heavy for tight probe intervals; runtime/metrics could be a lighter alternative.

Apply:

-func (h *health) LiveProbe(ctx context.Context) (Checks, error) {
-	const oneGiB = 1024 * 1024 * 1024
+func (h *health) LiveProbe(ctx context.Context) (Checks, error) {
+	const oneGiB uint64 = 1 << 30

56-63: Return empty map instead of nil for stable JSON shape.

Prevents “checks: null” in responses downstream.

-func (h *health) ReadyProbe(ctx context.Context) (Checks, error) {
-	return nil, nil
+func (h *health) ReadyProbe(ctx context.Context) (Checks, error) {
+	return Checks{}, nil
 }
@@
-func (h *health) StartedProbe(ctx context.Context) (Checks, error) {
-	return nil, nil
+func (h *health) StartedProbe(ctx context.Context) (Checks, error) {
+	return Checks{}, nil
 }

Please confirm health.Service treats empty Checks as pass (or as intended) when no providers report readiness/startup signals.

internal/sms-gateway/handlers/health.go (1)

82-99: Ensure Checks isn’t null in JSON.

MapValues returns nil when input map is nil; emit {} for stability.

-func makeResponse(result health.CheckResult) smsgateway.HealthResponse {
-	return smsgateway.HealthResponse{
+func makeResponse(result health.CheckResult) smsgateway.HealthResponse {
+	checks := lo.MapValues(
+		result.Checks,
+		func(value health.CheckDetail, key string) smsgateway.HealthCheck {
+			return smsgateway.HealthCheck{
+				Description:   value.Description,
+				ObservedUnit:  value.ObservedUnit,
+				ObservedValue: value.ObservedValue,
+				Status:        smsgateway.HealthStatus(value.Status),
+			}
+		},
+	)
+	if checks == nil {
+		checks = map[string]smsgateway.HealthCheck{}
+	}
+	return smsgateway.HealthResponse{
 		Status:    smsgateway.HealthStatus(result.Status()),
 		Version:   version.AppVersion,
 		ReleaseID: version.AppReleaseID(),
-		Checks: lo.MapValues(
-			result.Checks,
-			func(value health.CheckDetail, key string) smsgateway.HealthCheck {
-				return smsgateway.HealthCheck{
-					Description:   value.Description,
-					ObservedUnit:  value.ObservedUnit,
-					ObservedValue: value.ObservedValue,
-					Status:        smsgateway.HealthStatus(value.Status),
-				}
-			},
-		),
+		Checks:    checks,
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82c8833 and 655e550.

📒 Files selected for processing (2)
  • internal/sms-gateway/handlers/health.go (1 hunks)
  • internal/sms-gateway/modules/health/health.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
internal/sms-gateway/modules/health/health.go (1)
internal/sms-gateway/modules/health/types.go (6)
  • Checks (64-64)
  • CheckDetail (51-61)
  • Status (7-7)
  • StatusPass (11-11)
  • StatusWarn (12-12)
  • HealthProvider (66-72)
internal/sms-gateway/handlers/health.go (3)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/modules/health/types.go (5)
  • CheckResult (27-30)
  • Status (7-7)
  • StatusFail (13-13)
  • Checks (64-64)
  • CheckDetail (51-61)
internal/version/version.go (1)
  • AppReleaseID (13-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)

@capcom6 capcom6 force-pushed the handlers/improve-health-handler branch from 940625b to b5eedc7 Compare October 24, 2025 01:46
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
api/requests.http (1)

198-206: Add local general /health for parity with 3rd‑party section.

Include the readiness alias locally so tests cover both forms. This mirrors /3rdparty/v1/health and prior feedback.

 ### 
-GET http://localhost:3000/health/startup HTTP/1.1
+GET http://localhost:3000/health HTTP/1.1
+
+###
+GET http://localhost:3000/health/startup HTTP/1.1
internal/sms-gateway/modules/health/module.go (1)

8-22: Fix fx.Private misuse and export Service from a private module.

  • fx.Private is not a constructor; don’t pass it to fx.Provide.
  • If the module is private, explicitly export *Service for external consumers (handlers).
 func Module() fx.Option {
   return fx.Module(
     "health",
     fx.Decorate(func(log *zap.Logger) *zap.Logger {
       return log.Named("health")
     }),
-    fx.Provide(
-      AsHealthProvider(NewHealth),
-      fx.Private,
-    ),
+    fx.Private,
+    fx.Export((*Service)(nil)),
+    fx.Provide(
+      AsHealthProvider(NewHealth),
+    ),
     fx.Provide(
       NewService,
     ),
   )
 }

Run to confirm external usages of *health.Service (should show handlers) and avoid accidental privacy breakage:

#!/bin/bash
# Show external references to *health.Service outside the health module
rg -nP --type=go -C2 '\*?health\.Service\b' | grep -v 'internal/sms-gateway/modules/health/'
internal/sms-gateway/handlers/health.go (2)

44-45: Use Fiber’s request context (UserContext) for service calls.

c.Context() returns *fasthttp.RequestCtx, not context.Context. Pass c.UserContext() to the health service.

-	return writeProbe(c, h.healthSvc.CheckLiveness(c.Context()))
+	return writeProbe(c, h.healthSvc.CheckLiveness(c.UserContext()))
@@
-	return writeProbe(c, h.healthSvc.CheckReadiness(c.Context()))
+	return writeProbe(c, h.healthSvc.CheckReadiness(c.UserContext()))
@@
-	return writeProbe(c, h.healthSvc.CheckStartup(c.Context()))
+	return writeProbe(c, h.healthSvc.CheckStartup(c.UserContext()))

Also applies to: 58-59, 71-72


101-106: Register the documented alias /3rdparty/v1/health.

Swagger documents this path, but it’s not wired, causing a 404. Add the alias mapped to readiness.

 func (h *healthHandler) Register(router fiber.Router) {
 	router.Get("/health", h.getReadiness)
 	router.Get("/health/live", h.getLiveness)
 	router.Get("/health/ready", h.getReadiness)
 	router.Get("/health/startup", h.getStartup)
+	router.Get("/3rdparty/v1/health", h.getReadiness)
 }
🧹 Nitpick comments (1)
internal/sms-gateway/modules/health/health.go (1)

55-63: Return empty Checks map instead of nil.

Avoid nil maps to keep responses consistent and reduce nil-handling across callers.

 func (h *health) ReadyProbe(ctx context.Context) (Checks, error) {
-	return nil, nil
+	return Checks{}, nil
 }
 
 func (h *health) StartedProbe(ctx context.Context) (Checks, error) {
-	return nil, nil
+	return Checks{}, nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 940625b and b5eedc7.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (15)
  • api/requests.http (2 hunks)
  • build/package/Dockerfile (1 hunks)
  • deployments/helm-chart/templates/deployment.yaml (1 hunks)
  • go.mod (2 hunks)
  • internal/sms-gateway/app.go (2 hunks)
  • internal/sms-gateway/handlers/health.go (1 hunks)
  • internal/sms-gateway/modules/db/health.go (1 hunks)
  • internal/sms-gateway/modules/db/module.go (1 hunks)
  • internal/sms-gateway/modules/health/cli.go (0 hunks)
  • internal/sms-gateway/modules/health/db.go (0 hunks)
  • internal/sms-gateway/modules/health/health.go (1 hunks)
  • internal/sms-gateway/modules/health/module.go (1 hunks)
  • internal/sms-gateway/modules/health/service.go (1 hunks)
  • internal/sms-gateway/modules/health/types.go (3 hunks)
  • internal/sms-gateway/modules/messages/metrics.go (1 hunks)
💤 Files with no reviewable changes (2)
  • internal/sms-gateway/modules/health/cli.go
  • internal/sms-gateway/modules/health/db.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/sms-gateway/modules/db/health.go
  • internal/sms-gateway/modules/db/module.go
  • build/package/Dockerfile
  • go.mod
🧰 Additional context used
🧬 Code graph analysis (5)
internal/sms-gateway/app.go (2)
internal/sms-gateway/modules/db/module.go (1)
  • Module (12-22)
internal/sms-gateway/modules/health/module.go (1)
  • Module (8-22)
internal/sms-gateway/modules/health/health.go (1)
internal/sms-gateway/modules/health/types.go (6)
  • Checks (64-64)
  • CheckDetail (51-61)
  • Status (7-7)
  • StatusPass (11-11)
  • StatusWarn (12-12)
  • HealthProvider (66-72)
internal/sms-gateway/modules/health/service.go (1)
internal/sms-gateway/modules/health/types.go (6)
  • HealthProvider (66-72)
  • Checks (64-64)
  • CheckResult (27-30)
  • CheckDetail (51-61)
  • Status (7-7)
  • StatusFail (13-13)
internal/sms-gateway/modules/health/module.go (5)
internal/sms-gateway/app.go (1)
  • Module (35-58)
internal/sms-gateway/modules/db/module.go (1)
  • Module (12-22)
internal/sms-gateway/modules/health/health.go (1)
  • NewHealth (11-13)
internal/sms-gateway/modules/health/service.go (1)
  • NewService (24-30)
internal/sms-gateway/modules/health/types.go (1)
  • HealthProvider (66-72)
internal/sms-gateway/handlers/health.go (3)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/modules/health/types.go (5)
  • CheckResult (27-30)
  • Status (7-7)
  • StatusFail (13-13)
  • Checks (64-64)
  • CheckDetail (51-61)
internal/version/version.go (2)
  • AppVersion (9-9)
  • AppReleaseID (13-17)
🔇 Additional comments (6)
internal/sms-gateway/modules/messages/metrics.go (1)

13-14: LGTM!

The alignment of these constant declarations improves code readability with no functional impact.

deployments/helm-chart/templates/deployment.yaml (1)

95-119: LGTM on probes; aligned with new health surface.

Paths and timings are reasonable; startupProbe gates liveness/readiness correctly.

internal/sms-gateway/app.go (1)

39-39: LGTM on Module() wiring.

Switching to constructor-based modules matches the new module APIs.

Also applies to: 50-50

internal/sms-gateway/modules/health/service.go (3)

38-42: Good practice: context cancellation check.

The non-blocking context check between provider iterations allows graceful early exit and is appropriate here. Note that callers should use a timeout context if concerned about individual probes blocking indefinitely.


44-63: Well-designed error handling and check accumulation.

The error handling correctly logs failures and records them as failed checks while allowing other providers to continue. The check naming pattern (provider:check) is clear and consistent.


32-84: Excellent refactoring: eliminates duplication while maintaining clarity.

The checkProvider helper method and three probe-specific methods (CheckReadiness, CheckLiveness, CheckStartup) eliminate code duplication through a clean higher-order function pattern. The implementation is type-safe, handles errors gracefully, and supports cooperative cancellation.

@capcom6 capcom6 marked this pull request as ready for review October 25, 2025 10:36
@capcom6 capcom6 force-pushed the handlers/improve-health-handler branch from d85ad8a to f9dec80 Compare October 28, 2025 23:59
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (7)
internal/sms-gateway/modules/health/types.go (1)

34-48: Avoid Go 1.21 builtin max; implement explicit level selection.

Using max requires Go 1.21+. The code should use explicit comparisons to ensure compatibility with the project's minimum Go version.

Apply this diff to fix:

 func (c CheckResult) Status() Status {
-	// Determine overall status
 	level := levelPass
 	for _, detail := range c.Checks {
 		switch detail.Status {
 		case StatusPass:
+			// no-op
 		case StatusFail:
-			level = max(level, levelFail)
+			level = levelFail
 		case StatusWarn:
-			level = max(level, levelWarn)
+			if level == levelPass {
+				level = levelWarn
+			}
 		}
 	}
-
 	return statusLevels[level]
 }
internal/sms-gateway/modules/health/module.go (2)

14-17: Critical: fx.Private incorrectly passed to fx.Provide.

fx.Provide expects constructor functions, not options. Passing fx.Private as an argument to fx.Provide will cause the application to fail at startup.

Apply this diff to fix:

 func Module() fx.Option {
 	return fx.Module(
 		"health",
 		fx.Decorate(func(log *zap.Logger) *zap.Logger {
 			return log.Named("health")
 		}),
+		fx.Private,
 		fx.Provide(
 			AsHealthProvider(NewHealth),
-			fx.Private,
 		),
 		fx.Provide(
 			NewService,
 		),
 	)
 }

8-22: Inconsistent module visibility: Service consumed externally but module is private.

After fixing the fx.Private placement, the module will be fully private, but *health.Service is consumed by handlers outside this module. This will cause dependency resolution failure.

Choose one of these solutions:

Option 1 (Recommended): Export Service explicitly

 func Module() fx.Option {
 	return fx.Module(
 		"health",
+		fx.Export((*Service)(nil)),
 		fx.Decorate(func(log *zap.Logger) *zap.Logger {
 			return log.Named("health")
 		}),
 		fx.Private,
 		fx.Provide(
 			AsHealthProvider(NewHealth),
 		),
 		fx.Provide(
 			NewService,
 		),
 	)
 }

Option 2: Remove fx.Private entirely

 func Module() fx.Option {
 	return fx.Module(
 		"health",
 		fx.Decorate(func(log *zap.Logger) *zap.Logger {
 			return log.Named("health")
 		}),
-		fx.Private,
 		fx.Provide(
 			AsHealthProvider(NewHealth),
 		),
 		fx.Provide(
 			NewService,
 		),
 	)
 }
internal/sms-gateway/handlers/health.go (4)

44-44: Use c.UserContext() instead of c.Context() for type correctness.

The method c.Context() returns *fasthttp.RequestCtx, but CheckLiveness expects context.Context. Use c.UserContext() to pass the correct type.

Apply this diff:

- return writeProbe(c, h.healthSvc.CheckLiveness(c.Context()))
+ return writeProbe(c, h.healthSvc.CheckLiveness(c.UserContext()))

58-58: Use c.UserContext() instead of c.Context() for type correctness.

The method c.Context() returns *fasthttp.RequestCtx, but CheckReadiness expects context.Context. Use c.UserContext() to pass the correct type.

Apply this diff:

- return writeProbe(c, h.healthSvc.CheckReadiness(c.Context()))
+ return writeProbe(c, h.healthSvc.CheckReadiness(c.UserContext()))

71-71: Use c.UserContext() instead of c.Context() for type correctness.

The method c.Context() returns *fasthttp.RequestCtx, but CheckStartup expects context.Context. Use c.UserContext() to pass the correct type.

Apply this diff:

- return writeProbe(c, h.healthSvc.CheckStartup(c.Context()))
+ return writeProbe(c, h.healthSvc.CheckStartup(c.UserContext()))

101-106: Register the /3rdparty/v1/health route documented in swagger.

The swagger annotation at line 54 documents /3rdparty/v1/health as an alias for the readiness check, but this route is not registered in the Register function, causing documentation-implementation drift.

Apply this diff to add the missing route:

 func (h *healthHandler) Register(router fiber.Router) {
     router.Get("/health", h.getReadiness)
     router.Get("/health/live", h.getLiveness)
     router.Get("/health/ready", h.getReadiness)
     router.Get("/health/startup", h.getStartup)
+    router.Get("/3rdparty/v1/health", h.getReadiness)
 }
🧹 Nitpick comments (1)
internal/sms-gateway/modules/db/health.go (1)

34-53: Consider removing redundant ObservedValue initialization.

The logic is correct, but ObservedValue: 0 on line 38 is overwritten on line 50 regardless of the ping result.

Apply this diff to simplify:

 func (h *health) ReadyProbe(ctx context.Context) (healthmod.Checks, error) {
 	pingCheck := healthmod.CheckDetail{
 		Description:   "Database ping",
 		ObservedUnit:  "failed pings",
-		ObservedValue: 0,
 		Status:        healthmod.StatusPass,
 	}
 
 	if err := h.db.PingContext(ctx); err != nil {
 		h.failedPings.Add(1)
 
 		pingCheck.Status = healthmod.StatusFail
 	} else {
 		h.failedPings.Store(0)
 	}
 
 	pingCheck.ObservedValue = int(h.failedPings.Load())
 
 	return healthmod.Checks{"ping": pingCheck}, nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d85ad8a and f9dec80.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (16)
  • api/requests.http (2 hunks)
  • build/package/Dockerfile (1 hunks)
  • deployments/helm-chart/templates/deployment.yaml (1 hunks)
  • deployments/helm-chart/values.yaml (1 hunks)
  • go.mod (1 hunks)
  • internal/sms-gateway/app.go (2 hunks)
  • internal/sms-gateway/handlers/health.go (1 hunks)
  • internal/sms-gateway/modules/db/health.go (1 hunks)
  • internal/sms-gateway/modules/db/module.go (1 hunks)
  • internal/sms-gateway/modules/health/cli.go (0 hunks)
  • internal/sms-gateway/modules/health/db.go (0 hunks)
  • internal/sms-gateway/modules/health/health.go (1 hunks)
  • internal/sms-gateway/modules/health/module.go (1 hunks)
  • internal/sms-gateway/modules/health/service.go (1 hunks)
  • internal/sms-gateway/modules/health/types.go (3 hunks)
  • internal/sms-gateway/modules/messages/metrics.go (1 hunks)
💤 Files with no reviewable changes (2)
  • internal/sms-gateway/modules/health/cli.go
  • internal/sms-gateway/modules/health/db.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • deployments/helm-chart/values.yaml
  • internal/sms-gateway/modules/messages/metrics.go
  • go.mod
  • internal/sms-gateway/modules/health/service.go
  • api/requests.http
🧰 Additional context used
🧬 Code graph analysis (6)
internal/sms-gateway/app.go (2)
internal/sms-gateway/modules/db/module.go (1)
  • Module (12-22)
internal/sms-gateway/modules/health/module.go (1)
  • Module (8-22)
internal/sms-gateway/modules/health/module.go (5)
internal/sms-gateway/app.go (1)
  • Module (36-60)
internal/sms-gateway/modules/db/module.go (1)
  • Module (12-22)
internal/sms-gateway/modules/health/health.go (1)
  • NewHealth (11-13)
internal/sms-gateway/modules/health/service.go (1)
  • NewService (24-30)
internal/sms-gateway/modules/health/types.go (1)
  • HealthProvider (66-72)
internal/sms-gateway/modules/health/health.go (1)
internal/sms-gateway/modules/health/types.go (6)
  • Checks (64-64)
  • CheckDetail (51-61)
  • Status (7-7)
  • StatusPass (11-11)
  • StatusWarn (12-12)
  • HealthProvider (66-72)
internal/sms-gateway/handlers/health.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/modules/health/service.go (1)
  • Service (18-22)
internal/sms-gateway/modules/health/types.go (5)
  • CheckResult (27-30)
  • Status (7-7)
  • StatusFail (13-13)
  • Checks (64-64)
  • CheckDetail (51-61)
internal/version/version.go (2)
  • AppVersion (9-9)
  • AppReleaseID (13-17)
internal/sms-gateway/modules/db/health.go (1)
internal/sms-gateway/modules/health/types.go (6)
  • Checks (64-64)
  • CheckDetail (51-61)
  • Status (7-7)
  • StatusPass (11-11)
  • StatusFail (13-13)
  • HealthProvider (66-72)
internal/sms-gateway/modules/db/module.go (2)
internal/sms-gateway/app.go (1)
  • Module (36-60)
internal/sms-gateway/modules/health/module.go (2)
  • Module (8-22)
  • AsHealthProvider (24-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: Lint
  • GitHub Check: Analyze (go)
🔇 Additional comments (13)
build/package/Dockerfile (1)

50-51: LGTM! Docker healthcheck correctly updated to liveness probe.

The HEALTHCHECK correctly targets the new /health/live endpoint, which is appropriate for Docker's container health monitoring.

deployments/helm-chart/templates/deployment.yaml (1)

94-119: LGTM! Kubernetes probes properly configured for all three health checks.

The probe configuration correctly implements the three-probe pattern:

  • Liveness probe (/health/live): Checks if the app is alive and should be restarted if failing
  • Readiness probe (/health/ready): Checks if the app can accept traffic (e.g., DB connected)
  • Startup probe (/health/startup): Allows up to 120 seconds for initial startup before other probes begin

The timing values are appropriate for a backend service.

internal/sms-gateway/modules/db/health.go (4)

17-21: LGTM! Clean constructor with proper initialization.


24-26: LGTM! Provider name is clear and appropriate.


29-31: LGTM! DB health correctly excluded from liveness probe.

The application should remain "alive" even if the database is temporarily unavailable. DB health affects readiness, not liveness.


56-58: LGTM! DB health correctly excluded from startup probe.

internal/sms-gateway/modules/health/types.go (1)

66-72: LGTM! HealthProvider interface properly separated into three probe semantics.

The interface correctly distinguishes between startup, readiness, and liveness concerns, aligning with Kubernetes probe patterns.

internal/sms-gateway/app.go (1)

36-60: LGTM! Module wiring correctly updated to use constructor pattern.

The refactoring from package-level module variables (appdb.Module, health.Module) to constructor functions (appdb.Module(), health.Module()) enables more flexible dependency injection and module initialization.

internal/sms-gateway/modules/db/module.go (1)

12-22: LGTM! DB module correctly refactored with health provider integration.

The module now properly exposes both the health provider and ID generator, with correct use of AsHealthProvider for health provider annotation.

internal/sms-gateway/modules/health/health.go (3)

11-13: LGTM! Clean constructor.


21-53: LGTM! Runtime health checks are appropriate for liveness monitoring.

The memory and goroutine checks correctly trigger warnings (not failures) at reasonable thresholds:

  • Memory > 1 GiB: allows detecting memory issues without killing healthy pods
  • Goroutines > 1000: helps identify goroutine leaks

The implementation correctly uses StatusWarn to enable alerting while keeping the pod running.


56-63: LGTM! System health correctly limited to liveness probe only.

Runtime metrics (memory, goroutines) are appropriate for liveness checks but don't affect startup or readiness, which is correct.

internal/sms-gateway/modules/health/module.go (1)

24-30: LGTM! AsHealthProvider helper is well-designed for provider registration.

The function correctly uses fx.Annotate to implement the HealthProvider interface and group providers for collection by the health service.

@capcom6 capcom6 added the deployed The PR is deployed on staging label Oct 29, 2025
@capcom6 capcom6 merged commit 11ed142 into master Oct 30, 2025
11 checks passed
@capcom6 capcom6 deleted the handlers/improve-health-handler branch October 30, 2025 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deployed The PR is deployed on staging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants