Skip to main content

internal/platform/health/health.go

Source

Overview

What: Implements readiness probing against configured upstream services and returns structured reports.

Why: The gateway should only be considered ready when its upstream dependencies are reachable and responding successfully.

How: A Checker fans out HTTP GET probes to each configured upstream health endpoint, aggregates results, and returns a status of ready or degraded.

Contents

Imports

import block 1

internal/platform/health/health.go#L3
import (
"context"
"fmt"
"net/http"
"net/url"
"sync"
"time"
)

Types

type block 1

internal/platform/health/health.go#L13
type Upstream struct {
Name string
BaseURL string
HealthPath string
}

Upstream

What: Identifies an upstream dependency to probe.

Why: Readiness needs at least a name, base URL, and health path per dependency.

How: Provided by configuration and consumed by the runtime and checker.

type block 2

internal/platform/health/health.go#L20
type UpstreamReport struct {
Name string `json:"name"`
Healthy bool `json:"healthy"`
StatusCode int `json:"statusCode,omitempty"`
Error string `json:"error,omitempty"`
CheckedAt time.Time `json:"checkedAt"`
}

UpstreamReport

What: Outcome of probing a single upstream.

Why: Enables debugging readiness failures by surfacing status codes and errors per dependency.

How: Populated by probe and included in the aggregated Report.

type block 3

internal/platform/health/health.go#L29
type Report struct {
Status string `json:"status"`
CheckedAt time.Time `json:"checkedAt"`
Upstreams []UpstreamReport `json:"upstreams"`
}

Report

What: Aggregated readiness status plus per-upstream results.

Why: Callers need a structured way to understand which upstream is failing and when checks ran.

How: Returned by Readiness with status, checkedAt, and a slice of UpstreamReport.

type block 4

internal/platform/health/health.go#L36
type Checker struct {
client *http.Client
upstreams []Upstream
timeout time.Duration
userAgent string
}

Checker

What: Holds readiness probing dependencies and configuration.

Why: Allows the runtime to reuse a single configured checker across requests.

How: Stores the HTTP client, list of upstreams, timeout, and user-agent used by probes.

Functions and Methods

NewChecker

What: Constructs a Checker with an HTTP client, upstream list, per-probe timeout, and user-agent.

Why: Centralizes defaults and keeps runtime wiring simple.

How: Applies defaults when inputs are missing and stores the fields used by Readiness.

internal/platform/health/health.go#L44
func NewChecker(client *http.Client, upstreams []Upstream, timeout time.Duration, userAgent string) *Checker {
if client == nil {
client = http.DefaultClient
}
if timeout <= 0 {
timeout = 2 * time.Second
}
if userAgent == "" {
userAgent = "api-router-gateway/readyz"
}

return &Checker{
client: client,
upstreams: upstreams,
timeout: timeout,
userAgent: userAgent,
}
}

Walkthrough

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L45: if client == nil { client = http.DefaultClient }
    • What: Branches conditionally.
    • Why: Handles different execution paths based on runtime state.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L46: client = http.DefaultClient
        • What: Assigns client.
        • Why: Keeps intermediate state available for later steps in the function.
        • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L48: if timeout <= 0 { timeout = 2 * time.Second }
    • What: Branches conditionally.
    • Why: Handles different execution paths based on runtime state.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L49: timeout = 2 * time.Second
        • What: Assigns timeout.
        • Why: Keeps intermediate state available for later steps in the function.
        • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L51: if userAgent == "" { userAgent = "api-router-gateway/readyz" }
    • What: Branches conditionally.
    • Why: Handles different execution paths based on runtime state.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L52: userAgent = "api-router-gateway/readyz"
        • What: Assigns userAgent.
        • Why: Keeps intermediate state available for later steps in the function.
        • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L55: return &Checker{ client: client, upstreams: upstreams, timeout: timeout, userAgent: userAgent, }
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

(*Checker).Readiness

What: Probes all configured upstreams and returns an aggregated readiness report.

Why: Readiness must reflect the combined health of dependencies, not just process liveness.

How: Runs probes concurrently, collects UpstreamReport entries, and marks the overall status as degraded if any upstream is unhealthy.

internal/platform/health/health.go#L64
func (c *Checker) Readiness(ctx context.Context) Report {
if len(c.upstreams) == 0 {
return Report{Status: "ready", CheckedAt: time.Now().UTC()}
}

results := make([]UpstreamReport, len(c.upstreams))
var wg sync.WaitGroup

for idx, upstream := range c.upstreams {
wg.Add(1)
go func(i int, u Upstream) {
defer wg.Done()
results[i] = c.probe(ctx, u)
}(idx, upstream)
}

wg.Wait()

report := Report{
CheckedAt: time.Now().UTC(),
Upstreams: results,
}

report.Status = "ready"
for _, r := range results {
if !r.Healthy {
report.Status = "degraded"
break
}
}

return report
}

Walkthrough

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L65: if len(c.upstreams) == 0 { return Report{Status: "ready", CheckedAt: time.Now().UTC()} }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L66: return Report{Status: "ready", CheckedAt: time.Now().UTC()}
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L69: results := make([]UpstreamReport, len(c.upstreams))
    • What: Defines results.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L70: var wg sync.WaitGroup
    • What: Declares local names.
    • Why: Introduces variables or types used later in the function.
    • How: Executes a Go declaration statement inside the function body.
  • L72: for idx, upstream := range c.upstreams { wg.Add(1) go func(i int, u Upstream) { defer wg.Done() results[i] = c.probe(ctx, u) }(idx, upstrea…
    • What: Iterates over a collection.
    • Why: Processes multiple elements with the same logic.
    • How: Executes a for ... range loop.
    • Nested steps:
      • L73: wg.Add(1)
        • What: Calls wg.Add.
        • Why: Performs side effects or delegates work to a helper.
        • How: Executes the expression statement.
      • L74: go func(i int, u Upstream) { defer wg.Done() results[i] = c.probe(ctx, u) }(idx, upstream)
        • What: Starts a goroutine.
        • Why: Runs work concurrently.
        • How: Invokes the function call asynchronously using go.
        • Nested steps:
          • L74: func(i int, u Upstream) { defer wg.Done() results[i] = c.probe(ctx, u) }
            • What: Defines an inline function (closure).
            • Why: Encapsulates callback logic and may capture variables from the surrounding scope.
            • How: Declares a func literal and uses it as a value (for example, as an HTTP handler or callback).
            • Nested steps:
              • L75: defer wg.Done()
                • What: Defers a call for cleanup.
                • Why: Ensures the deferred action runs even on early returns.
                • How: Schedules the call to run when the surrounding function returns.
              • L76: results[i] = c.probe(ctx, u)
                • What: Assigns results[i].
                • Why: Keeps intermediate state available for later steps in the function.
                • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L80: wg.Wait()
    • What: Calls wg.Wait.
    • Why: Performs side effects or delegates work to a helper.
    • How: Executes the expression statement.
  • L82: report := Report{ CheckedAt: time.Now().UTC(), Upstreams: results, }
    • What: Defines report.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L87: report.Status = "ready"
    • What: Assigns report.Status.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L88: for _, r := range results { if !r.Healthy { report.Status = "degraded" break } }
    • What: Iterates over a collection.
    • Why: Processes multiple elements with the same logic.
    • How: Executes a for ... range loop.
    • Nested steps:
      • L89: if !r.Healthy { report.Status = "degraded" break }
        • What: Branches conditionally.
        • Why: Handles different execution paths based on runtime state.
        • How: Evaluates the condition and executes the matching branch.
        • Nested steps:
          • L90: report.Status = "degraded"
            • What: Assigns report.Status.
            • Why: Keeps intermediate state available for later steps in the function.
            • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
          • L91: break
            • What: Executes a statement.
            • Why: Advances the function logic.
            • How: Runs this statement as part of the function body.
  • L95: return report
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

(*Checker).probe

What: Probes a single upstream health endpoint and returns an UpstreamReport.

Why: Keeps per-upstream probing logic isolated and testable.

How: Builds the full URL from base + health path, creates a context with timeout, sends a GET request with a user-agent, and treats non-2xx responses as unhealthy with an error message.

internal/platform/health/health.go#L98
func (c *Checker) probe(ctx context.Context, upstream Upstream) UpstreamReport {
checkedAt := time.Now().UTC()
report := UpstreamReport{
Name: upstream.Name,
Healthy: false,
CheckedAt: checkedAt,
}

targetURL, err := url.JoinPath(upstream.BaseURL, upstream.HealthPath)
if err != nil {
report.Error = fmt.Sprintf("failed to build upstream url: %v", err)
return report
}

reqCtx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()

req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, targetURL, nil)
if err != nil {
report.Error = fmt.Sprintf("failed to create request: %v", err)
return report
}

req.Header.Set("User-Agent", c.userAgent)

resp, err := c.client.Do(req)
if err != nil {
select {
case <-reqCtx.Done():
report.Error = reqCtx.Err().Error()
default:
report.Error = err.Error()
}
return report
}
defer resp.Body.Close()

report.StatusCode = resp.StatusCode
report.Healthy = resp.StatusCode >= 200 && resp.StatusCode < 300
if !report.Healthy {
report.Error = fmt.Sprintf("health check failed with status %d", resp.StatusCode)
}

return report
}

Walkthrough

Expand walkthrough (27 steps)

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L99: checkedAt := time.Now().UTC()
    • What: Defines checkedAt.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L100: report := UpstreamReport{ Name: upstream.Name, Healthy: false, CheckedAt: checkedAt, }
    • What: Defines report.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L106: targetURL, err := url.JoinPath(upstream.BaseURL, upstream.HealthPath)
    • What: Defines targetURL, err.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L107: if err != nil { report.Error = fmt.Sprintf("failed to build upstream url: %v", err) return report }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L108: report.Error = fmt.Sprintf("failed to build upstream url: %v", err)
        • What: Assigns report.Error.
        • Why: Keeps intermediate state available for later steps in the function.
        • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
      • L109: return report
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L112: reqCtx, cancel := context.WithTimeout(ctx, c.timeout)
    • What: Defines reqCtx, cancel.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L113: defer cancel()
    • What: Defers a call for cleanup.
    • Why: Ensures the deferred action runs even on early returns.
    • How: Schedules the call to run when the surrounding function returns.
  • L115: req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, targetURL, nil)
    • What: Defines req, err.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L116: if err != nil { report.Error = fmt.Sprintf("failed to create request: %v", err) return report }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L117: report.Error = fmt.Sprintf("failed to create request: %v", err)
        • What: Assigns report.Error.
        • Why: Keeps intermediate state available for later steps in the function.
        • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
      • L118: return report
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L121: req.Header.Set("User-Agent", c.userAgent)
    • What: Calls req.Header.Set.
    • Why: Performs side effects or delegates work to a helper.
    • How: Executes the expression statement.
  • L123: resp, err := c.client.Do(req)
    • What: Defines resp, err.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L124: if err != nil { select { case <-reqCtx.Done(): report.Error = reqCtx.Err().Error() default: report.Error = err.Error() } return report }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L125: select { case <-reqCtx.Done(): report.Error = reqCtx.Err().Error() default: report.Error = err.Error() }
        • What: Selects among concurrent operations.
        • Why: Coordinates channel operations without blocking incorrectly.
        • How: Executes a select statement and runs one ready case.
        • Nested steps:
          • L126: case <-reqCtx.Done():
            • What: Selects a select-case branch.
            • Why: Coordinates concurrent operations without blocking incorrectly.
            • How: Runs this case body when its channel operation is ready (or runs default immediately).
            • Nested steps:
              • L127: report.Error = reqCtx.Err().Error()
                • What: Assigns report.Error.
                • Why: Keeps intermediate state available for later steps in the function.
                • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
          • L128: default:
            • What: Selects a select-case branch.
            • Why: Coordinates concurrent operations without blocking incorrectly.
            • How: Runs this case body when its channel operation is ready (or runs default immediately).
            • Nested steps:
              • L129: report.Error = err.Error()
                • What: Assigns report.Error.
                • Why: Keeps intermediate state available for later steps in the function.
                • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
      • L131: return report
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L133: defer resp.Body.Close()
    • What: Defers a call for cleanup.
    • Why: Ensures the deferred action runs even on early returns.
    • How: Schedules the call to run when the surrounding function returns.
  • L135: report.StatusCode = resp.StatusCode
    • What: Assigns report.StatusCode.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L136: report.Healthy = resp.StatusCode >= 200 && resp.StatusCode < 300
    • What: Assigns report.Healthy.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L137: if !report.Healthy { report.Error = fmt.Sprintf("health check failed with status %d", resp.StatusCode) }
    • What: Branches conditionally.
    • Why: Handles different execution paths based on runtime state.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L138: report.Error = fmt.Sprintf("health check failed with status %d", resp.StatusCode)
        • What: Assigns report.Error.
        • Why: Keeps intermediate state available for later steps in the function.
        • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L141: return report
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

Start Here

Guides

Neighboring source