Module: Textus::Doctor

Defined in:
lib/textus/doctor.rb,
lib/textus/doctor/check.rb,
lib/textus/doctor/check/hooks.rb,
lib/textus/doctor/check/schemas.rb,
lib/textus/doctor/check/audit_log.rb,
lib/textus/doctor/check/sentinels.rb,
lib/textus/doctor/check/templates.rb,
lib/textus/doctor/check/illegal_keys.rb,
lib/textus/doctor/check/manifest_files.rb,
lib/textus/doctor/check/schema_violations.rb,
lib/textus/doctor/check/unowned_schema_fields.rb

Overview

Health check for a Textus store. Returns a JSON-friendly Hash envelope with an ‘issues` array and a summary. Each issue is a Hash with `code`, `level`, `subject`, `message`, and optionally `fix`.

Defined Under Namespace

Classes: Check

Constant Summary collapse

LEVELS =
%w[error warning info].freeze
DOCTOR_CHECK_TIMEOUT_SECONDS =
2
CHECKS =
[
  Check::ManifestFiles,
  Check::Schemas,
  Check::Templates,
  Check::Hooks,
  Check::IllegalKeys,
  Check::Sentinels,
  Check::AuditLog,
  Check::UnownedSchemaFields,
  Check::SchemaViolations,
].freeze
ALL_CHECKS =
CHECKS.map(&:name_key).freeze

Class Method Summary collapse

Class Method Details

.fail_issue(name, code:, message:, fix:) ⇒ Object



76
77
78
79
80
81
82
83
84
# File 'lib/textus/doctor.rb', line 76

def fail_issue(name, code:, message:, fix:)
  {
    "code" => code,
    "level" => "error",
    "subject" => name.to_s,
    "message" => message,
    "fix" => fix,
  }
end

.run(store, checks: nil) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/textus/doctor.rb', line 27

def run(store, checks: nil)
  selected_keys = checks ? Array(checks).map(&:to_s) : ALL_CHECKS
  unknown = selected_keys - ALL_CHECKS
  unless unknown.empty?
    raise UsageError.new(
      "unknown doctor check: #{unknown.first}. Valid checks: #{ALL_CHECKS.join(", ")}",
    )
  end

  selected = CHECKS.select { |c| selected_keys.include?(c.name_key) }
  issues = selected.flat_map { |c| c.new(store).call }
  issues.concat(run_registered_checks(store))

  summary = LEVELS.to_h { |l| [l, issues.count { |i| i["level"] == l }] }
  {
    "protocol" => Textus::PROTOCOL,
    "ok" => summary["error"].zero?,
    "issues" => issues,
    "summary" => summary,
  }
end

.run_registered_checks(store) ⇒ Object



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
# File 'lib/textus/doctor.rb', line 49

def run_registered_checks(store)
  out = []
  view = Store::View.new(store)
  store.registry.rpc_names(:check).each do |name|
    callable = store.registry.rpc_callable(:check, name)
    begin
      result = Timeout.timeout(DOCTOR_CHECK_TIMEOUT_SECONDS) { callable.call(store: view) }
      if result.is_a?(Array)
        out.concat(result.map { |h| h.transform_keys(&:to_s) })
      else
        out << fail_issue(name, code: "doctor_check.bad_return",
                                message: "doctor_check '#{name}' returned #{result.class} (expected Array)",
                                fix: "return an array of issue hashes from the doctor_check block")
      end
    rescue Timeout::Error
      out << fail_issue(name, code: "doctor_check.timeout",
                              message: "doctor_check '#{name}' exceeded #{DOCTOR_CHECK_TIMEOUT_SECONDS}s",
                              fix: "shorten the check or split it into smaller checks")
    rescue StandardError => e
      out << fail_issue(name, code: "doctor_check.failed",
                              message: "#{e.class}: #{e.message}",
                              fix: "fix the doctor_check block in .textus/extensions/")
    end
  end
  out
end