internal/platform/health/health.go
Source
- Package:
health - File:
internal/platform/health/health.go - GitHub: https://github.com/theroutercompany/api_router/blob/main/internal/platform/health/health.go
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
import (
"context"
"fmt"
"net/http"
"net/url"
"sync"
"time"
)
Types
type block 1
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
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
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
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.
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.
- L46:
- 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.
- L49:
- 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.
- L52:
- 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
returnstatement (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.
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
returnstatement (possibly returning values).
- L66:
- 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 ... rangeloop. - 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
funcliteral 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.
- L75:
- L74:
- L73:
- 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 ... rangeloop. - 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.
- L90:
- L89:
- 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
returnstatement (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.
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
returnstatement (possibly returning values).
- L108:
- 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
returnstatement (possibly returning values).
- L117:
- 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
selectstatement 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.
- L127:
- 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.
- L129:
- L126:
- 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
returnstatement (possibly returning values).
- L125:
- 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.
- L138:
- 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
returnstatement (possibly returning values).