Module: Flightdeck::Metrics::TimeBucket

Defined in:
app/models/flightdeck/metrics/time_bucket.rb

Overview

The only adapter-specific SQL in Flightdeck.

Charts bucket timestamps into fixed-width windows. Doing that portably means converting a timestamp to a UTC epoch integer and dividing — never date_trunc, which does not exist everywhere and drags the server's timezone into the answer. Everything downstream is an integer number of seconds since the epoch, in UTC; the display timezone is applied at render time and nowhere else.

Defined Under Namespace

Classes: UnsupportedAdapter

Constant Summary collapse

IDENTIFIER =

Identifiers are built by Flightdeck, never by a request, but they are still validated before being interpolated so that can never change by accident.

/\A[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)?\z/

Class Method Summary collapse

Class Method Details

.adapter_nameObject



22
23
24
# File 'app/models/flightdeck/metrics/time_bucket.rb', line 22

def adapter_name
  SolidQueue::Record.connection.adapter_name.to_s
end

.bucket(column, seconds, family: self.family) ⇒ Object

The epoch second at which column's bucket starts.

SQLite gets integer division rather than FLOOR() on purpose: floor() is only available when SQLite is compiled with math functions, and integer division is exact for the post-1970 timestamps we deal with.



57
58
59
60
61
62
63
64
65
66
67
# File 'app/models/flightdeck/metrics/time_bucket.rb', line 57

def bucket(column, seconds, family: self.family)
  seconds = interval!(seconds)
  col = identifier!(column)

  case family
  when :postgresql then "FLOOR(EXTRACT(EPOCH FROM #{col}) / #{seconds}) * #{seconds}"
  when :mysql then "FLOOR(UNIX_TIMESTAMP(#{col}) / #{seconds}) * #{seconds}"
  when :sqlite then "(CAST(strftime('%s', #{col}) AS INTEGER) / #{seconds}) * #{seconds}"
  else raise UnsupportedAdapter, "unknown family #{family.inspect}"
  end
end

.elapsed(from_column, to_column, family: self.family) ⇒ Object

Seconds between two timestamp columns, for averaging in SQL.



72
73
74
# File 'app/models/flightdeck/metrics/time_bucket.rb', line 72

def elapsed(from_column, to_column, family: self.family)
  "(#{epoch(to_column, family: family)} - #{epoch(from_column, family: family)})"
end

.elapsed_sql(from_column, to_column) ⇒ Object



76
# File 'app/models/flightdeck/metrics/time_bucket.rb', line 76

def elapsed_sql(from_column, to_column) = Arel.sql(elapsed(from_column, to_column))

.epoch(column, family: self.family) ⇒ Object

Seconds since the UTC epoch, as a number the database can do arithmetic on.



41
42
43
44
45
46
47
48
49
50
# File 'app/models/flightdeck/metrics/time_bucket.rb', line 41

def epoch(column, family: self.family)
  col = identifier!(column)

  case family
  when :postgresql then "EXTRACT(EPOCH FROM #{col})"
  when :mysql then "UNIX_TIMESTAMP(#{col})"
  when :sqlite then "CAST(strftime('%s', #{col}) AS INTEGER)"
  else raise UnsupportedAdapter, "unknown family #{family.inspect}"
  end
end

.family(name = adapter_name) ⇒ Object

Normalises the adapter into the three SQL dialects we support.



27
28
29
30
31
32
33
34
35
36
37
# File 'app/models/flightdeck/metrics/time_bucket.rb', line 27

def family(name = adapter_name)
  case name.to_s.downcase
  when /postgres/ then :postgresql
  when /mysql|trilogy/ then :mysql
  when /sqlite/ then :sqlite
  else
    raise UnsupportedAdapter,
          "Flightdeck cannot bucket time on the #{name} adapter. " \
          "Supported adapters: PostgreSQL, MySQL/Trilogy, SQLite."
  end
end

.sql(column, seconds) ⇒ Object



69
# File 'app/models/flightdeck/metrics/time_bucket.rb', line 69

def sql(column, seconds) = Arel.sql(bucket(column, seconds))