summaryrefslogtreecommitdiff
path: root/lib/grafana/time_window.rb
blob: 6cc757d77c553a92d20b46861000d32ae730b8d9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# frozen_string_literal: true

module Grafana
  # Allows for easy formatting and manipulations of timestamps
  # coming from a Grafana url
  class TimeWindow
    include ::Gitlab::Utils::StrongMemoize

    def initialize(from, to)
      @from = from
      @to = to
    end

    def formatted
      {
        start: window[:from].formatted,
        end: window[:to].formatted
      }
    end

    def in_milliseconds
      window.transform_values(&:to_ms)
    end

    private

    def window
      strong_memoize(:window) do
        specified_window
      rescue Timestamp::Error
        default_window
      end
    end

    def specified_window
      RangeWithDefaults.new(
        from: Timestamp.from_ms_since_epoch(@from),
        to: Timestamp.from_ms_since_epoch(@to)
      ).to_hash
    end

    def default_window
      RangeWithDefaults.new.to_hash
    end
  end

  # For incomplete time ranges, adds default parameters to
  # achieve a complete range. If both full range is provided,
  # range will be returned.
  class RangeWithDefaults
    DEFAULT_RANGE = 8.hours

    # @param from [Grafana::Timestamp, nil] Start of the expected range
    # @param to [Grafana::Timestamp, nil] End of the expected range
    def initialize(from: nil, to: nil)
      @from = from
      @to = to

      apply_defaults!
    end

    def to_hash
      { from: @from, to: @to }.compact
    end

    private

    def apply_defaults!
      @to ||= @from ? relative_end : Timestamp.new(Time.now)
      @from ||= relative_start
    end

    def relative_start
      Timestamp.new(DEFAULT_RANGE.before(@to.time))
    end

    def relative_end
      Timestamp.new(DEFAULT_RANGE.since(@from.time))
    end
  end

  # Offers a consistent API for timestamps originating from
  # Grafana or other sources, allowing for formatting of timestamps
  # as consumed by Grafana-related utilities
  class Timestamp
    Error = Class.new(StandardError)

    attr_accessor :time

    # @param timestamp [Time]
    def initialize(time)
      @time = time
    end

    # Formats a timestamp from Grafana for compatibility with
    # parsing in JS via `new Date(timestamp)`
    def formatted
      time.utc.strftime('%FT%TZ')
    end

    # Converts to milliseconds since epoch
    def to_ms
      time.to_i * 1000
    end

    class << self
      # @param time [String] Representing milliseconds since epoch.
      #                      This is what JS "decided" unix is.
      def from_ms_since_epoch(time)
        return if time.nil?

        raise Error, 'Expected milliseconds since epoch' unless ms_since_epoch?(time)

        new(cast_ms_to_time(time))
      end

      private

      def cast_ms_to_time(time)
        Time.at(time.to_i / 1000.0)
      end

      def ms_since_epoch?(time)
        ms = time.to_i

        ms.to_s == time && ms.bit_length < 64
      end
    end
  end
end