Class: StandardHealth::Aggregator

Inherits:
Object
  • Object
show all
Defined in:
lib/standard_health/aggregator.rb

Overview

Runs all registered checks and rolls them up into a single status.

Status semantics:

:ok           — every check returned :ok
:degraded     — at least one non-critical check failed, OR a check was
              skipped because the total budget ran out
:unavailable  — at least one critical check failed

The aggregator never raises. Each check is invoked through safe_run which catches StandardError so a buggy custom check cannot take down /ready. Instrumentation is held to the same bar — every emit is wrapped so a broken subscriber cannot become a new way for /ready to 500.

Class Method Summary collapse

Class Method Details

.call(checks: StandardHealth.config.checks, now: Time.now.utc) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/standard_health/aggregator.rb', line 27

def self.call(checks: StandardHealth.config.checks, now: Time.now.utc)
  started = monotonic
  budget = StandardHealth.config.total_check_budget

  check_rows = checks.map do |reg|
    if budget_exhausted?(budget, started)
      row = skipped_row(reg, budget)
      # Skips emit too. Without this a skip is invisible to the Metrics
      # notifier — the only trace would be a name inside ready.evaluated's
      # `failed[]`, with no per-check counter — so once a budget is
      # enabled you could not answer "how often is check X getting
      # skipped", which is exactly the question a budget creates.
      emit_check(row)
      row
    else
      safe_run(reg)
    end
  end

  duration_ms = ((monotonic - started) * 1000).round
  status = overall_status(check_rows)

  failing = check_rows.reject { |r| r[:status] == :ok }

  emit(
    "standard_health.ready.evaluated",
    status: status,
    duration_ms: duration_ms,
    failed: failing.map { |r| r[:name] },
    # The full failure detail rides on THIS event, not only on the
    # per-check one. Redaction removes the message from the HTTP body on
    # the promise that it still reaches logs and Sentry — but both of
    # those subscribers are driven by ready.evaluated (deliberately: it
    # is the transition-gated event, so they don't fire per poll). If the
    # message only existed on check.completed, redaction would delete the
    # last copy instead of relocating it.
    failures: failing.map do |r|
      {
        name: r[:name],
        critical: r[:critical],
        status: r[:status],
        error_class: r[:error_class],
        error_message: r[:error]
      }.compact
    end
  )

  {
    status: status,
    checks: check_rows,
    generated_at: now.iso8601
  }
end