Class: ActiveSanction::Sync::Report
- Inherits:
-
Object
- Object
- ActiveSanction::Sync::Report
- Extended by:
- T::Generic, T::Sig
- Includes:
- Enumerable
- Defined in:
- lib/active_sanction/sync/report.rb
Overview
What a whole sync run did, one Result per source.
report = ActiveSanction.sync!
report.failed? # => true
report[:un_consolidated].error # => "Net::ReadTimeout: ..."
report.updated.map(&:source) # => [:ofac_sdn]
puts report # => the table below
4 sources in 13.08s: 1 updated, 2 unchanged, 1 failed
ofac_sdn updated 19015 records just fetched 12.41s
ofac_consolidated unchanged 1203 records 2h old 0.28s
canada_sema unchanged 684 records 2h old 0.19s
un_consolidated failed 612 records 3d old 1.11s Net::ReadTimeout: execution expired
It is an object, not console output
This is the operational surface of a sync: it is what a host application
alerts on, what a scheduled job exits with, and what the instrumentation
hooks (#59) emit. So it serializes to a documented shape and .from_h
rebuilds it -- a summary that only existed as printed text would mean
every host that wants to notice a degrading source has to scrape a log.
Note what the table prints beside a failure: the record count and age of the snapshot that source is still being screened against. A failed sync keeps its previous snapshot, which is the right call and is only safe while the age of what is being screened against is visible.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
-
#duration ⇒ Float
readonly
Wall-clock seconds for the whole run, which is less than the sum of the per-source durations when sources ran in parallel.
- #results ⇒ Array<Result> readonly
-
#started_at ⇒ Time
readonly
When the run began, UTC.
Class Method Summary collapse
-
.from_h(hash) ⇒ T.attached_class
Rebuilds from #to_h output, accepting string keys so a report survives the trip through JSON.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
-
#[](source) ⇒ Result?
One source's result, or nil if the run did not cover it.
- #each(&block) ⇒ T.untyped
- #empty? ⇒ Boolean
-
#exit_code ⇒ Integer
What a scheduled job should exit with, so that cron mails somebody and CI goes red when a source is failing.
- #failed ⇒ Array<Result>
- #failed? ⇒ Boolean
- #failure_message ⇒ String
- #hash ⇒ Integer
- #initialize(results:, started_at: nil, duration: 0.0) ⇒ void constructor
- #inspect ⇒ String
-
#oldest_age ⇒ Integer?
The age of the stalest list this run left behind, in seconds.
-
#record_count ⇒ Integer
Records stored across every source the run covered, failures included: what is screenable now, rather than what was downloaded.
-
#records(result) ⇒ String
The sentence a failing run should put in front of a human: which sources failed, out of how many, and why.
- #size ⇒ Integer
- #sources ⇒ Array<Symbol>
-
#success! ⇒ T.self_type
For a caller that wants any failure to be fatal, in the manner of Fetcher::Result#success!.
- #success? ⇒ Boolean
- #summary ⇒ String
- #to_h ⇒ Hash{Symbol => T.untyped}
- #to_s ⇒ String
- #unchanged ⇒ Array<Result>
-
#unscreenable ⇒ Array<Result>
Sources that came out of this run with no snapshot stored at all, and so are not covered by screening.
- #updated ⇒ Array<Result>
- #width(&block) ⇒ Integer
Constructor Details
#initialize(results:, started_at: nil, duration: 0.0) ⇒ void
75 76 77 78 79 80 |
# File 'lib/active_sanction/sync/report.rb', line 75 def initialize(results:, started_at: nil, duration: 0.0) @results = T.let(results!(results), T::Array[Result]) @started_at = T.let(time!(started_at), Time) @duration = T.let(duration.to_f, Float) freeze end |
Instance Attribute Details
#duration ⇒ Float (readonly)
Wall-clock seconds for the whole run, which is less than the sum of the per-source durations when sources ran in parallel.
61 62 63 |
# File 'lib/active_sanction/sync/report.rb', line 61 def duration @duration end |
#results ⇒ Array<Result> (readonly)
52 53 54 |
# File 'lib/active_sanction/sync/report.rb', line 52 def results @results end |
#started_at ⇒ Time (readonly)
When the run began, UTC.
56 57 58 |
# File 'lib/active_sanction/sync/report.rb', line 56 def started_at @started_at end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
Rebuilds from #to_h output, accepting string keys so a report survives the trip through JSON.
66 67 68 69 70 71 72 |
# File 'lib/active_sanction/sync/report.rb', line 66 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Sync::Report attribute(s): #{unknown.join(", ")}" if unknown.any? T.unsafe(self).new(**attributes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
197 198 199 200 201 |
# File 'lib/active_sanction/sync/report.rb', line 197 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#[](source) ⇒ Result?
One source's result, or nil if the run did not cover it. A run that did not cover a source is not the same as one where it succeeded, which is why this does not raise: a caller asking about a source it did not sync is asking a question with an answer.
95 96 97 98 |
# File 'lib/active_sanction/sync/report.rb', line 95 def [](source) key = source.to_sym results.find { |result| result.source == key } end |
#each(&block) ⇒ T.untyped
83 84 85 86 87 88 |
# File 'lib/active_sanction/sync/report.rb', line 83 def each(&block) return enum_for(:each) unless block results.each(&block) self end |
#empty? ⇒ Boolean
129 |
# File 'lib/active_sanction/sync/report.rb', line 129 def empty? = results.empty? |
#exit_code ⇒ Integer
What a scheduled job should exit with, so that cron mails somebody and CI goes red when a source is failing. Deliberately here rather than left to each caller to derive: "one source failed" has to mean the same thing to every wrapper anyone writes around a sync.
exit ActiveSanction.sync!.exit_code
148 |
# File 'lib/active_sanction/sync/report.rb', line 148 def exit_code = failed? ? 1 : 0 |
#failed ⇒ Array<Result>
110 |
# File 'lib/active_sanction/sync/report.rb', line 110 def failed = results.select(&:failed?) |
#failed? ⇒ Boolean
120 |
# File 'lib/active_sanction/sync/report.rb', line 120 def failed? = results.any?(&:failed?) |
#failure_message ⇒ String
179 180 181 182 |
# File 'lib/active_sanction/sync/report.rb', line 179 def "#{failed.size} of #{size} source(s) failed to sync: " + failed.map { |result| "#{result.source} (#{result.error})" }.join("; ") end |
#hash ⇒ Integer
205 |
# File 'lib/active_sanction/sync/report.rb', line 205 def hash = [self.class, to_h].hash |
#inspect ⇒ String
208 |
# File 'lib/active_sanction/sync/report.rb', line 208 def inspect = "#<#{self.class} #{summary}>" |
#oldest_age ⇒ Integer?
The age of the stalest list this run left behind, in seconds. The one number to alert on if a host only wants one.
139 |
# File 'lib/active_sanction/sync/report.rb', line 139 def oldest_age = results.filter_map(&:age).max |
#record_count ⇒ Integer
Records stored across every source the run covered, failures included: what is screenable now, rather than what was downloaded.
134 |
# File 'lib/active_sanction/sync/report.rb', line 134 def record_count = results.sum { |result| result.record_count || 0 } |
#records(result) ⇒ String
The sentence a failing run should put in front of a human: which sources failed, out of how many, and why.
173 |
# File 'lib/active_sanction/sync/report.rb', line 173 def records(result) = result.record_count&.to_s || "-" |
#size ⇒ Integer
126 |
# File 'lib/active_sanction/sync/report.rb', line 126 def size = results.size |
#sources ⇒ Array<Symbol>
101 |
# File 'lib/active_sanction/sync/report.rb', line 101 def sources = results.map(&:source) |
#success! ⇒ T.self_type
For a caller that wants any failure to be fatal, in the manner of
Fetcher::Result#success!. Note that this is not what sync! does: the
run has already finished and every other source has already been
stored, so raising here reports a failure rather than causing one.
155 156 157 158 159 |
# File 'lib/active_sanction/sync/report.rb', line 155 def success! return self if success? raise Failed, self end |
#success? ⇒ Boolean
123 |
# File 'lib/active_sanction/sync/report.rb', line 123 def success? = !failed? |
#summary ⇒ String
185 186 187 188 189 190 191 |
# File 'lib/active_sanction/sync/report.rb', line 185 def summary counts = { updated: updated.size, unchanged: unchanged.size, failed: failed.size } .reject { |_status, count| count.zero? } .map { |status, count| "#{count} #{status}" } "#{size} #{size == 1 ? "source" : "sources"} in #{format("%.2f", duration)}s" \ "#{": #{counts.join(", ")}" unless counts.empty?}" end |
#to_h ⇒ Hash{Symbol => T.untyped}
162 163 164 165 166 167 168 |
# File 'lib/active_sanction/sync/report.rb', line 162 def to_h { results: results.map(&:to_h), started_at: started_at.iso8601, duration: duration } end |
#to_s ⇒ String
194 |
# File 'lib/active_sanction/sync/report.rb', line 194 def to_s = ([summary] + rows).join("\n") |
#unchanged ⇒ Array<Result>
107 |
# File 'lib/active_sanction/sync/report.rb', line 107 def unchanged = results.select(&:unchanged?) |
#unscreenable ⇒ Array<Result>
Sources that came out of this run with no snapshot stored at all, and
so are not covered by screening. Louder than failed and rarer: a
source that failed but kept its previous list is stale, one that has
nothing stored is missing.
117 |
# File 'lib/active_sanction/sync/report.rb', line 117 def unscreenable = results.reject(&:stored?) |
#updated ⇒ Array<Result>
104 |
# File 'lib/active_sanction/sync/report.rb', line 104 def updated = results.select(&:updated?) |
#width(&block) ⇒ Integer
176 |
# File 'lib/active_sanction/sync/report.rb', line 176 def width(&block) = results.map { |result| block.call(result).length }.max.to_i |