Class: ActiveSanction::Doctor::Report
- Inherits:
-
Object
- Object
- ActiveSanction::Doctor::Report
- Extended by:
- T::Generic, T::Sig
- Includes:
- Enumerable
- Defined in:
- lib/active_sanction/doctor/report.rb
Overview
What a whole diagnostic run found, one Diagnosis per source.
report = ActiveSanction.doctor
report.ok? # => false
report.findings # => [Finding, ...]
report[:ofac_sdn] # => Diagnosis
exit report.exit_code
puts report
2 sources in 18.42s: 1 with findings
ofac_sdn WARN 3 findings
warn remarks coverage 71.4% (was 97.3%): "Passport No." x 1,880 unrecognized
warn individuals with a date of birth 12% (was 61%) of 11,704
info unknown SDN_Type "syndicate"; treated as an organization (41 rows)
un_consolidated OK
It is an object, not console output
The same split Sync::Report makes, for the same reason. The human form is what a CLI verb (#36) prints; the serialized form is what a host application alerts on, what a nightly job keeps so that next week's run has a warning class to compare against, and what the instrumentation hooks (#59) emit. A diagnostic that only existed as printed text would mean every host that wants to notice a drifting source has to scrape a log, which is precisely the state this exists to end.
The exit code is a policy, and it is the caller's
exit_code is 1 when anything failed at error, because a list that
cannot be read is not a matter of taste. Whether a warn should also
stop a deployment is, so it is a parameter: exit_code(on: :warn) is
what a team that treats drift as a build failure passes.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
- #diagnoses ⇒ Array<Diagnosis> readonly
-
#duration ⇒ Float
readonly
Wall-clock seconds for the whole run.
-
#started_at ⇒ Time
readonly
When the run began, UTC.
Class Method Summary collapse
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
-
#[](source) ⇒ Diagnosis?
One source's diagnosis, or nil if the run did not cover it.
- #each(&block) ⇒ T.untyped
- #empty? ⇒ Boolean
- #errors ⇒ Array<Finding>
-
#exit_code(on: :error) ⇒ Integer
What a scheduled job should exit with.
-
#failed ⇒ Array<Diagnosis>
The sources that could not be diagnosed at all -- a publisher that is down, a payload that is not the format it should be.
- #failed? ⇒ Boolean
-
#findings ⇒ Array<Finding>
Every finding across every source, most serious first, and within a severity in the order the sources were diagnosed.
- #hash ⇒ Integer
- #infos ⇒ Array<Finding>
- #initialize(diagnoses:, started_at: nil, duration: 0.0) ⇒ void constructor
- #inspect ⇒ String
-
#ok? ⇒ Boolean
Nothing above
info, anywhere. -
#profiles ⇒ Hash{Symbol => Profile}
The profile of each source, keyed by source -- what a nightly job keeps so that the next run has last night's warning classes and free-text coverage to compare against, which a stored snapshot cannot supply.
-
#severity ⇒ Symbol?
The most serious severity anywhere in the run, or nil for a clean one.
- #size ⇒ Integer
- #sources ⇒ Array<Symbol>
- #summary ⇒ String
- #to_h ⇒ Hash{Symbol => T.untyped}
- #to_s ⇒ String
-
#unhealthy ⇒ Array<Diagnosis>
The sources with something worth reading about them.
- #warnings ⇒ Array<Finding>
Constructor Details
#initialize(diagnoses:, started_at: nil, duration: 0.0) ⇒ void
80 81 82 83 84 85 |
# File 'lib/active_sanction/doctor/report.rb', line 80 def initialize(diagnoses:, started_at: nil, duration: 0.0) @diagnoses = T.let(diagnoses!(diagnoses), T::Array[Diagnosis]) @started_at = T.let(time!(started_at), Time) @duration = T.let(duration.to_f, Float) freeze end |
Instance Attribute Details
#diagnoses ⇒ Array<Diagnosis> (readonly)
60 61 62 |
# File 'lib/active_sanction/doctor/report.rb', line 60 def diagnoses @diagnoses end |
#duration ⇒ Float (readonly)
Wall-clock seconds for the whole run.
68 69 70 |
# File 'lib/active_sanction/doctor/report.rb', line 68 def duration @duration end |
#started_at ⇒ Time (readonly)
When the run began, UTC.
64 65 66 |
# File 'lib/active_sanction/doctor/report.rb', line 64 def started_at @started_at end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
71 72 73 74 75 76 77 |
# File 'lib/active_sanction/doctor/report.rb', line 71 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Doctor::Report attribute(s): #{unknown.join(", ")}" if unknown.any? T.unsafe(self).new(**attributes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
192 193 194 195 196 |
# File 'lib/active_sanction/doctor/report.rb', line 192 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#[](source) ⇒ Diagnosis?
One source's diagnosis, or nil if the run did not cover it.
97 98 99 100 |
# File 'lib/active_sanction/doctor/report.rb', line 97 def [](source) key = source.to_sym diagnoses.find { |diagnosis| diagnosis.source == key } end |
#each(&block) ⇒ T.untyped
88 89 90 91 92 93 |
# File 'lib/active_sanction/doctor/report.rb', line 88 def each(&block) return enum_for(:each) unless block diagnoses.each(&block) self end |
#empty? ⇒ Boolean
151 |
# File 'lib/active_sanction/doctor/report.rb', line 151 def empty? = diagnoses.empty? |
#errors ⇒ Array<Finding>
114 |
# File 'lib/active_sanction/doctor/report.rb', line 114 def errors = findings.select(&:error?) |
#exit_code(on: :error) ⇒ Integer
What a scheduled job should exit with. 1 on any error by default, and
on: :warn for a caller that wants drift to stop a build too. See the
class comment.
157 158 159 160 |
# File 'lib/active_sanction/doctor/report.rb', line 157 def exit_code(on: :error) level = on.to_sym findings.any? { |finding| finding.at_least?(level) } ? 1 : 0 end |
#failed ⇒ Array<Diagnosis>
The sources that could not be diagnosed at all -- a publisher that is down, a payload that is not the format it should be. Louder than a finding, and a different question: nothing here knows whether those lists have drifted.
136 |
# File 'lib/active_sanction/doctor/report.rb', line 136 def failed = diagnoses.select(&:failed?) |
#failed? ⇒ Boolean
139 |
# File 'lib/active_sanction/doctor/report.rb', line 139 def failed? = diagnoses.any?(&:failed?) |
#findings ⇒ Array<Finding>
Every finding across every source, most serious first, and within a severity in the order the sources were diagnosed.
108 109 110 111 |
# File 'lib/active_sanction/doctor/report.rb', line 108 def findings diagnoses.flat_map(&:findings) .sort_by.with_index { |finding, at| [-Finding::SEVERITIES.index(finding.severity).to_i, at] } end |
#hash ⇒ Integer
200 |
# File 'lib/active_sanction/doctor/report.rb', line 200 def hash = [self.class, to_h].hash |
#infos ⇒ Array<Finding>
120 |
# File 'lib/active_sanction/doctor/report.rb', line 120 def infos = findings.select(&:info?) |
#inspect ⇒ String
203 |
# File 'lib/active_sanction/doctor/report.rb', line 203 def inspect = "#<#{self.class} #{summary}>" |
#ok? ⇒ Boolean
Nothing above info, anywhere. What a nightly job alerts on when it
only wants one question answered.
125 |
# File 'lib/active_sanction/doctor/report.rb', line 125 def ok? = diagnoses.all?(&:ok?) |
#profiles ⇒ Hash{Symbol => Profile}
The profile of each source, keyed by source -- what a nightly job keeps so that the next run has last night's warning classes and free-text coverage to compare against, which a stored snapshot cannot supply. See Doctor#baseline.
172 173 174 175 176 177 |
# File 'lib/active_sanction/doctor/report.rb', line 172 def profiles diagnoses.each_with_object({}) do |diagnosis, all| profile = diagnosis.profile all[diagnosis.source] = profile unless profile.nil? end end |
#severity ⇒ Symbol?
The most serious severity anywhere in the run, or nil for a clean one.
143 144 145 |
# File 'lib/active_sanction/doctor/report.rb', line 143 def severity Finding::SEVERITIES.reverse.find { |level| diagnoses.any? { |one| one.severity == level } } end |
#size ⇒ Integer
148 |
# File 'lib/active_sanction/doctor/report.rb', line 148 def size = diagnoses.size |
#sources ⇒ Array<Symbol>
103 |
# File 'lib/active_sanction/doctor/report.rb', line 103 def sources = diagnoses.map(&:source) |
#summary ⇒ String
180 181 182 183 184 185 186 |
# File 'lib/active_sanction/doctor/report.rb', line 180 def summary counts = { "with findings" => unhealthy.size, "unreadable" => failed.size } .reject { |_label, count| count.zero? } .map { |label, count| "#{count} #{label}" } "#{size} #{size == 1 ? "source" : "sources"} in #{format("%.2f", duration)}s" \ "#{": #{counts.empty? ? "all healthy" : counts.join(", ")}"}" end |
#to_h ⇒ Hash{Symbol => T.untyped}
163 164 165 |
# File 'lib/active_sanction/doctor/report.rb', line 163 def to_h { diagnoses: diagnoses.map(&:to_h), started_at: started_at.iso8601, duration: duration } end |
#to_s ⇒ String
189 |
# File 'lib/active_sanction/doctor/report.rb', line 189 def to_s = ([summary] + diagnoses.flat_map { |diagnosis| diagnosis.lines(width) }).join("\n") |
#unhealthy ⇒ Array<Diagnosis>
The sources with something worth reading about them.
129 |
# File 'lib/active_sanction/doctor/report.rb', line 129 def unhealthy = diagnoses.reject(&:ok?) |
#warnings ⇒ Array<Finding>
117 |
# File 'lib/active_sanction/doctor/report.rb', line 117 def warnings = findings.select(&:warn?) |