Class: Flightdeck::Metrics::Series
- Inherits:
-
Object
- Object
- Flightdeck::Metrics::Series
- Defined in:
- app/models/flightdeck/metrics/series.rb
Overview
Time series for the Overview charts, computed live from the Solid Queue tables — no rollup tables, no background aggregation.
Every series is gap-filled in Ruby so a chart always has a complete axis: a bucket with no rows is a real zero (or a real "no data"), not a missing point that would make the chart lie about its own shape.
Defined Under Namespace
Constant Summary collapse
- WINDOWS =
{ "1h" => Window.new(key: "1h", label: "1H", duration: 1.hour, bucket_seconds: 5.minutes.to_i, subtitle: "per 5 minutes", tick_every: 3, tick_format: "%H:%M"), "24h" => Window.new(key: "24h", label: "24H", duration: 24.hours, bucket_seconds: 1.hour.to_i, subtitle: "per hour", tick_every: 4, tick_format: "%H:%M"), "7d" => Window.new(key: "7d", label: "7D", duration: 7.days, bucket_seconds: 6.hours.to_i, subtitle: "per 6 hours", tick_every: 4, tick_format: "%-m/%-d") }.freeze
- DEFAULT_WINDOW =
"24h"- SPARKLINE_HOURS =
16- SPARKLINE_BUCKET =
1.hour.to_i
Instance Attribute Summary collapse
-
#now ⇒ Object
readonly
Returns the value of attribute now.
-
#window ⇒ Object
readonly
Returns the value of attribute window.
Class Method Summary collapse
- .align_to(epoch, seconds) ⇒ Object
-
.queue_sparklines(now: Time.current, hours: SPARKLINE_HOURS) ⇒ Object
One query for every queue's hourly finished count, fanned out in Ruby.
- .window_for(key) ⇒ Object
Instance Method Summary collapse
-
#buckets ⇒ Object
Bucket start times, aligned to absolute epoch multiples so that the same bucket has the same boundaries in every process and every request.
-
#completion_time ⇒ Object
Average wall-clock time from enqueue to finish, per bucket.
- #empty? ⇒ Boolean
- #ends_at ⇒ Object
-
#initialize(window: DEFAULT_WINDOW, now: Time.current) ⇒ Series
constructor
A new instance of Series.
- #retention ⇒ Object
- #starts_at ⇒ Object
-
#throughput ⇒ Object
Jobs finished (succeeded) and executions failed, per bucket.
- #total_failed ⇒ Object
- #total_succeeded ⇒ Object
-
#truncated_by_retention? ⇒ Boolean
Finished-job retention shorter than the window means the oldest buckets are missing rows that really did happen.
Constructor Details
#initialize(window: DEFAULT_WINDOW, now: Time.current) ⇒ Series
Returns a new instance of Series.
41 42 43 44 |
# File 'app/models/flightdeck/metrics/series.rb', line 41 def initialize(window: DEFAULT_WINDOW, now: Time.current) @window = window.is_a?(Window) ? window : self.class.window_for(window) @now = now.utc end |
Instance Attribute Details
#now ⇒ Object (readonly)
Returns the value of attribute now.
35 36 37 |
# File 'app/models/flightdeck/metrics/series.rb', line 35 def now @now end |
#window ⇒ Object (readonly)
Returns the value of attribute window.
35 36 37 |
# File 'app/models/flightdeck/metrics/series.rb', line 35 def window @window end |
Class Method Details
.align_to(epoch, seconds) ⇒ Object
143 |
# File 'app/models/flightdeck/metrics/series.rb', line 143 def self.align_to(epoch, seconds) = (epoch / seconds) * seconds |
.queue_sparklines(now: Time.current, hours: SPARKLINE_HOURS) ⇒ Object
One query for every queue's hourly finished count, fanned out in Ruby. Used by the queue cards and the Overview mini-table.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'app/models/flightdeck/metrics/series.rb', line 119 def self.queue_sparklines(now: Time.current, hours: SPARKLINE_HOURS) Flightdeck::Cache.fetch("sparklines", hours, align_to(now.to_i, SPARKLINE_BUCKET), expires_in: Flightdeck.config.chart_cache_ttl) do last = align_to(now.to_i, SPARKLINE_BUCKET) slots = Array.new(hours) { |i| last - ((hours - 1 - i) * SPARKLINE_BUCKET) } counts = SolidQueue::Job .where(finished_at: Time.at(slots.first).utc..) .group(:queue_name) .group(TimeBucket.sql("#{SolidQueue::Job.table_name}.finished_at", SPARKLINE_BUCKET)) .count fanned = Hash.new { |hash, key| hash[key] = Array.new(hours, 0) } counts.each do |(queue_name, bucket), count| index = slots.index(bucket.to_i) next unless index fanned[queue_name][index] = count end fanned.default = nil fanned end end |
.window_for(key) ⇒ Object
37 38 39 |
# File 'app/models/flightdeck/metrics/series.rb', line 37 def self.window_for(key) WINDOWS.fetch(key.to_s, WINDOWS.fetch(DEFAULT_WINDOW)) end |
Instance Method Details
#buckets ⇒ Object
Bucket start times, aligned to absolute epoch multiples so that the same bucket has the same boundaries in every process and every request.
48 49 50 51 52 53 54 |
# File 'app/models/flightdeck/metrics/series.rb', line 48 def buckets @buckets ||= begin last = align(@now.to_i) count = window.bucket_count Array.new(count) { |i| last - ((count - 1 - i) * window.bucket_seconds) } end end |
#completion_time ⇒ Object
Average wall-clock time from enqueue to finish, per bucket.
This is time to completion, not time to start: Solid Queue deletes the claimed_executions row when a job finishes, so the moment a job actually started is not recoverable after the fact and no honest historical time-to-start series can be built from these tables.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'app/models/flightdeck/metrics/series.rb', line 83 def completion_time cached(:completion_time) do averages = SolidQueue::Job .where(finished_at: starts_at..) .group(TimeBucket.sql("#{jobs_table}.finished_at", window.bucket_seconds)) .average(TimeBucket.elapsed_sql("#{jobs_table}.created_at", "#{jobs_table}.finished_at")) .transform_keys(&:to_i) buckets.map do |bucket| value = averages[bucket] Point.new(at: Time.at(bucket).utc, seconds: value&.to_f) end end end |
#empty? ⇒ Boolean
101 |
# File 'app/models/flightdeck/metrics/series.rb', line 101 def empty? = throughput.all? { |point| point.total.zero? } |
#ends_at ⇒ Object
57 |
# File 'app/models/flightdeck/metrics/series.rb', line 57 def ends_at = Time.at(buckets.last + window.bucket_seconds).utc |
#retention ⇒ Object
110 111 112 113 114 115 |
# File 'app/models/flightdeck/metrics/series.rb', line 110 def retention return nil unless SolidQueue.respond_to?(:clear_finished_jobs_after) return nil unless SolidQueue.preserve_finished_jobs? SolidQueue.clear_finished_jobs_after end |
#starts_at ⇒ Object
56 |
# File 'app/models/flightdeck/metrics/series.rb', line 56 def starts_at = Time.at(buckets.first).utc |
#throughput ⇒ Object
Jobs finished (succeeded) and executions failed, per bucket.
A job that failed has no finished_at — Solid Queue leaves it null and records a failed_executions row — so the two series never double count.
63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'app/models/flightdeck/metrics/series.rb', line 63 def throughput cached(:throughput) do succeeded = grouped_count(SolidQueue::Job.where(finished_at: starts_at..), :finished_at) failed = grouped_count(SolidQueue::FailedExecution.where(created_at: starts_at..), :created_at, table: SolidQueue::FailedExecution.table_name) buckets.map do |bucket| Point.new(at: Time.at(bucket).utc, succeeded: succeeded.fetch(bucket, 0), failed: failed.fetch(bucket, 0)) end end end |
#total_failed ⇒ Object
99 |
# File 'app/models/flightdeck/metrics/series.rb', line 99 def total_failed = throughput.sum(&:failed) |
#total_succeeded ⇒ Object
98 |
# File 'app/models/flightdeck/metrics/series.rb', line 98 def total_succeeded = throughput.sum(&:succeeded) |
#truncated_by_retention? ⇒ Boolean
Finished-job retention shorter than the window means the oldest buckets are missing rows that really did happen. The chart says so rather than showing a decline that is an artefact of purging.
106 107 108 |
# File 'app/models/flightdeck/metrics/series.rb', line 106 def truncated_by_retention? retention.present? && retention < window.duration end |