redis-time-series 0.1.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +17 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile.lock +10 -2
- data/README.md +185 -31
- data/bin/console +3 -9
- data/bin/setup +29 -6
- data/lib/ext/time_msec.rb +29 -0
- data/lib/redis-time-series.rb +10 -5
- data/lib/redis/time_series.rb +257 -70
- data/lib/redis/time_series/aggregation.rb +88 -0
- data/lib/redis/time_series/client.rb +77 -0
- data/lib/redis/time_series/errors.rb +19 -0
- data/lib/redis/time_series/filters.rb +169 -0
- data/lib/redis/time_series/info.rb +81 -0
- data/lib/redis/time_series/rule.rb +49 -0
- data/lib/redis/time_series/sample.rb +22 -6
- data/lib/redis/time_series/version.rb +6 -0
- data/redis-time-series.gemspec +4 -3
- metadata +25 -4
- data/lib/time/msec.rb +0 -7
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The +TimeMsec+ module is a refinement for the +Time+ class that makes it easier
|
4
|
+
# to work with millisecond timestamps.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Time.now.to_i # 1595194259
|
8
|
+
# Time.now.ts_msec # NoMethodError
|
9
|
+
#
|
10
|
+
# using TimeMsec
|
11
|
+
#
|
12
|
+
# Time.now.to_i # 1595194259
|
13
|
+
# Time.now.ts_msec # 1595194259000
|
14
|
+
#
|
15
|
+
# Time.from_msec(1595194259000) # 2020-07-19 14:30:59 -0700
|
16
|
+
module TimeMsec
|
17
|
+
refine Time do
|
18
|
+
# TODO: convert to #to_msec
|
19
|
+
def ts_msec
|
20
|
+
(to_f * 1000.0).to_i
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
refine Time.singleton_class do
|
25
|
+
def from_msec(timestamp)
|
26
|
+
at(timestamp / 1000.0)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/redis-time-series.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
require 'bigdecimal'
|
2
|
-
require '
|
3
|
-
require '
|
2
|
+
require 'forwardable'
|
3
|
+
require 'ext/time_msec'
|
4
|
+
require 'redis/time_series/client'
|
5
|
+
require 'redis/time_series/errors'
|
6
|
+
require 'redis/time_series/aggregation'
|
7
|
+
require 'redis/time_series/filters'
|
8
|
+
require 'redis/time_series/rule'
|
9
|
+
require 'redis/time_series/info'
|
4
10
|
require 'redis/time_series/sample'
|
11
|
+
require 'redis/time_series'
|
5
12
|
|
6
|
-
class RedisTimeSeries
|
7
|
-
VERSION = '0.1.1'
|
8
|
-
end
|
13
|
+
class RedisTimeSeries; end
|
data/lib/redis/time_series.rb
CHANGED
@@ -1,82 +1,234 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
using TimeMsec
|
3
|
+
|
2
4
|
class Redis
|
5
|
+
# The +Redis::TimeSeries+ class is an interface for working with time-series data in
|
6
|
+
# Redis, using the {https://oss.redislabs.com/redistimeseries RedisTimeSeries} module.
|
7
|
+
#
|
8
|
+
# You can't use this gem with vanilla Redis, the time series module must be compiled
|
9
|
+
# and loaded. The easiest way to do this is by running the provided Docker container.
|
10
|
+
# Refer to the {https://oss.redislabs.com/redistimeseries/#setup setup guide} for more info.
|
11
|
+
#
|
12
|
+
# +docker run -p 6379:6379 -it --rm redislabs/redistimeseries+
|
13
|
+
#
|
14
|
+
# Once you're up and running, you can create a new time series and start recording data.
|
15
|
+
# Many commands are documented below, but you should refer to the
|
16
|
+
# {https://oss.redislabs.com/redistimeseries/commands command documentation} for the most
|
17
|
+
# authoritative and up-to-date reference.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# ts = Redis::TimeSeries.create('time_series_example')
|
21
|
+
# ts.add(12345)
|
22
|
+
# ts.get
|
23
|
+
# #=> #<Redis::TimeSeries::Sample:0x00007ff00d942e60 @time=2020-07-19 16:52:48 -0700, @value=0.12345e5>
|
3
24
|
class TimeSeries
|
25
|
+
extend Client
|
26
|
+
extend Forwardable
|
27
|
+
|
4
28
|
class << self
|
29
|
+
# Create a new time series.
|
30
|
+
#
|
31
|
+
# @param key [String] the Redis key to store time series data in
|
32
|
+
# @option options [Hash] :labels
|
33
|
+
# A hash of label-value pairs to apply to this series.
|
34
|
+
# @option options [Redis] :redis (self.class.redis) a different Redis client to use
|
35
|
+
# @option options [Integer] :retention
|
36
|
+
# Maximum age for samples compared to last event time (in milliseconds).
|
37
|
+
# With no value, the series will not be trimmed.
|
38
|
+
# @option options [Boolean] :uncompressed
|
39
|
+
# When true, series data will be stored in an uncompressed format.
|
40
|
+
#
|
41
|
+
# @return [Redis::TimeSeries] the created time series
|
42
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tscreate
|
5
43
|
def create(key, **options)
|
6
|
-
new(key,
|
44
|
+
new(key, redis: options.fetch(:redis, redis)).create(**options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a compaction rule for a series. Note that both source and destination series
|
48
|
+
# must exist before the rule can be created.
|
49
|
+
#
|
50
|
+
# @param source [String, TimeSeries] the source series (or key) to apply the rule to
|
51
|
+
# @param dest [String, TimeSeries] the destination series (or key) to aggregate the data
|
52
|
+
# @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
|
53
|
+
# The aggregation to apply. Can be an {Aggregation} object, or an array of
|
54
|
+
# aggregation_type and duration +[:avg, 120000]+
|
55
|
+
#
|
56
|
+
# @return [String] the string "OK"
|
57
|
+
# @raise [Redis::TimeSeries::AggregationError] if the given aggregation params are invalid
|
58
|
+
# @raise [Redis::CommandError] if the compaction rule cannot be applied to either series
|
59
|
+
#
|
60
|
+
# @see TimeSeries#create_rule
|
61
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tscreaterule
|
62
|
+
def create_rule(source:, dest:, aggregation:)
|
63
|
+
cmd 'TS.CREATERULE', key_for(source), key_for(dest), Aggregation.parse(aggregation).to_a
|
64
|
+
end
|
65
|
+
|
66
|
+
# Delete an existing compaction rule.
|
67
|
+
#
|
68
|
+
# @param source [String, TimeSeries] the source series (or key) to remove the rule from
|
69
|
+
# @param dest [String, TimeSeries] the destination series (or key) the rule applies to
|
70
|
+
#
|
71
|
+
# @return [String] the string "OK"
|
72
|
+
# @raise [Redis::CommandError] if the compaction rule does not exist
|
73
|
+
def delete_rule(source:, dest:)
|
74
|
+
cmd 'TS.DELETERULE', key_for(source), key_for(dest)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Delete all data and remove a time series from Redis.
|
78
|
+
#
|
79
|
+
# @param key [String] the key to remove
|
80
|
+
# @return [1] if the series existed
|
81
|
+
# @return [0] if the series did not exist
|
82
|
+
def destroy(key)
|
83
|
+
redis.del key
|
7
84
|
end
|
8
85
|
|
9
86
|
def madd(data)
|
10
87
|
data.reduce([]) do |memo, (key, value)|
|
11
|
-
|
12
|
-
# multiple timestamp => value pairs
|
13
|
-
value.each do |timestamp, nested_value|
|
14
|
-
timestamp = timestamp.ts_msec if timestamp.is_a? Time
|
15
|
-
memo << [key, timestamp, nested_value]
|
16
|
-
end
|
17
|
-
elsif value.is_a? Array
|
18
|
-
# single [timestamp, value]
|
19
|
-
key = key.ts_msec if key.is_a? Time
|
20
|
-
memo << [key, value]
|
21
|
-
else
|
22
|
-
# single value, no timestamp
|
23
|
-
memo << [key, '*', value]
|
24
|
-
end
|
88
|
+
memo << parse_madd_values(key, value)
|
25
89
|
memo
|
26
90
|
end.then do |args|
|
27
|
-
|
28
|
-
redis.call('TS.MADD', args.flatten).each_with_index.map do |result, idx|
|
91
|
+
cmd('TS.MADD', args).each_with_index.map do |result, idx|
|
29
92
|
result.is_a?(Redis::CommandError) ? result : Sample.new(result, args[idx][2])
|
30
93
|
end
|
31
94
|
end
|
32
95
|
end
|
33
96
|
|
34
|
-
|
35
|
-
|
97
|
+
# Search for a time series matching the provided filters. Refer to the {Filters} documentation
|
98
|
+
# for more details on how to filter.
|
99
|
+
#
|
100
|
+
# @example Using a filter string
|
101
|
+
# Redis::TimeSeries.query_index('foo=bar')
|
102
|
+
# #=> [#<Redis::TimeSeries:0x00007ff00e222788 @key="ts3", @redis=#<Redis...>>]
|
103
|
+
# @example Using the .where alias with hash DSL
|
104
|
+
# Redis::TimeSeries.where(foo: 'bar')
|
105
|
+
# #=> [#<Redis::TimeSeries:0x00007ff00e2a1d30 @key="ts3", @redis=#<Redis...>>]
|
106
|
+
#
|
107
|
+
# @param filter_value [Hash, String] a set of filters to query with
|
108
|
+
# @return [Array<TimeSeries>] an array of series that matched the given filters
|
109
|
+
#
|
110
|
+
# @see Filters
|
111
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsqueryindex
|
112
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#filtering
|
113
|
+
def query_index(filter_value)
|
114
|
+
filters = Filters.new(filter_value)
|
115
|
+
filters.validate!
|
116
|
+
cmd('TS.QUERYINDEX', filters.to_a).map { |key| new(key) }
|
117
|
+
end
|
118
|
+
alias where query_index
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def key_for(series_or_string)
|
123
|
+
series_or_string.is_a?(self) ? series_or_string.key : series_or_string.to_s
|
36
124
|
end
|
37
125
|
|
38
|
-
def
|
39
|
-
|
126
|
+
def parse_madd_values(key, raw)
|
127
|
+
if raw.is_a?(Hash) || (raw.is_a?(Array) && raw.first.is_a?(Array))
|
128
|
+
# multiple timestamp => value pairs
|
129
|
+
raw.map do |timestamp, value|
|
130
|
+
[key, timestamp, value]
|
131
|
+
end
|
132
|
+
elsif raw.is_a? Array
|
133
|
+
# single [timestamp, value]
|
134
|
+
[key, raw.first, raw.last]
|
135
|
+
else
|
136
|
+
# single value, no timestamp
|
137
|
+
[key, '*', raw]
|
138
|
+
end
|
40
139
|
end
|
41
140
|
end
|
42
141
|
|
43
|
-
|
142
|
+
# @return [String] the Redis key this time series is stored in
|
143
|
+
attr_reader :key
|
44
144
|
|
45
|
-
|
145
|
+
# @param key [String] the Redis key to store the time series in
|
146
|
+
# @param redis [Redis] an optional Redis client
|
147
|
+
def initialize(key, redis: self.class.redis)
|
46
148
|
@key = key
|
47
|
-
|
48
|
-
@labels = options[:labels] || []
|
49
|
-
@redis = options[:redis] || self.class.redis
|
50
|
-
@retention = options[:retention]
|
51
|
-
@uncompressed = options[:uncompressed] || false
|
149
|
+
@redis = redis
|
52
150
|
end
|
53
151
|
|
54
|
-
|
55
|
-
|
56
|
-
|
152
|
+
# Add a value to the series.
|
153
|
+
#
|
154
|
+
# @param value [Numeric] the value to add
|
155
|
+
# @param timestamp [Time, Numeric] the +Time+, or integer timestamp in milliseconds, to add the value
|
156
|
+
# @param uncompressed [Boolean] if true, stores data in an uncompressed format
|
157
|
+
#
|
158
|
+
# @return [Sample] the value that was added
|
159
|
+
# @raise [Redis::CommandError] if the value being added is older than the latest timestamp in the series
|
160
|
+
def add(value, timestamp = '*', uncompressed: nil)
|
161
|
+
ts = cmd 'TS.ADD', key, timestamp, value, ('UNCOMPRESSED' if uncompressed)
|
57
162
|
Sample.new(ts, value)
|
58
163
|
end
|
59
164
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
165
|
+
# Issues a TS.CREATE command for the current series.
|
166
|
+
# You should use class method {Redis::TimeSeries.create} instead.
|
167
|
+
# @api private
|
168
|
+
def create(retention: nil, uncompressed: nil, labels: nil)
|
169
|
+
cmd 'TS.CREATE', key,
|
170
|
+
(['RETENTION', retention] if retention),
|
171
|
+
('UNCOMPRESSED' if uncompressed),
|
172
|
+
(['LABELS', labels.to_a] if labels&.any?)
|
66
173
|
self
|
67
174
|
end
|
68
175
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
176
|
+
# Create a compaction rule for this series.
|
177
|
+
#
|
178
|
+
# @param dest [String, TimeSeries] the destination series (or key) to aggregate the data
|
179
|
+
# @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
|
180
|
+
# The aggregation to apply. Can be an {Aggregation} object, or an array of
|
181
|
+
# aggregation_type and duration +[:avg, 120000]+
|
182
|
+
#
|
183
|
+
# @return [String] the string "OK"
|
184
|
+
# @raise [Redis::TimeSeries::AggregationError] if the given aggregation params are invalid
|
185
|
+
# @raise [Redis::CommandError] if the compaction rule cannot be applied to either series
|
186
|
+
#
|
187
|
+
# @see TimeSeries.create_rule
|
188
|
+
def create_rule(dest:, aggregation:)
|
189
|
+
self.class.create_rule(source: self, dest: dest, aggregation: aggregation)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Delete an existing compaction rule.
|
193
|
+
#
|
194
|
+
# @param dest [String, TimeSeries] the destination series (or key) the rule applies to
|
195
|
+
#
|
196
|
+
# @return [String] the string "OK"
|
197
|
+
# @raise [Redis::CommandError] if the compaction rule does not exist
|
198
|
+
#
|
199
|
+
# @see TimeSeries.delete_rule
|
200
|
+
def delete_rule(dest:)
|
201
|
+
self.class.delete_rule(source: self, dest: dest)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Decrement the current value of the series.
|
205
|
+
#
|
206
|
+
# @param value [Integer] the amount to decrement by
|
207
|
+
# @param timestamp [Time, Integer] the Time or integer millisecond timestamp to save the new value at
|
208
|
+
# @param uncompressed [Boolean] if true, stores data in an uncompressed format
|
209
|
+
#
|
210
|
+
# @return [Integer] the timestamp the value was stored at
|
211
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
|
212
|
+
def decrby(value = 1, timestamp = nil, uncompressed: nil)
|
213
|
+
cmd 'TS.DECRBY', key, value, (timestamp if timestamp), ('UNCOMPRESSED' if uncompressed)
|
73
214
|
end
|
74
215
|
alias decrement decrby
|
75
216
|
|
217
|
+
|
218
|
+
# Delete all data and remove this time series from Redis.
|
219
|
+
#
|
220
|
+
# @return [1] if the series existed
|
221
|
+
# @return [0] if the series did not exist
|
76
222
|
def destroy
|
77
223
|
redis.del key
|
78
224
|
end
|
79
225
|
|
226
|
+
# Get the most recent sample for this series.
|
227
|
+
#
|
228
|
+
# @return [Sample] the most recent sample for this series
|
229
|
+
# @return [nil] if there are no samples in the series
|
230
|
+
#
|
231
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsget
|
80
232
|
def get
|
81
233
|
cmd('TS.GET', key).then do |timestamp, value|
|
82
234
|
return unless value
|
@@ -84,31 +236,46 @@ class Redis
|
|
84
236
|
end
|
85
237
|
end
|
86
238
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
239
|
+
# Increment the current value of the series.
|
240
|
+
#
|
241
|
+
# @param value [Integer] the amount to increment by
|
242
|
+
# @param timestamp [Time, Integer] the Time or integer millisecond timestamp to save the new value at
|
243
|
+
# @param uncompressed [Boolean] if true, stores data in an uncompressed format
|
244
|
+
#
|
245
|
+
# @return [Integer] the timestamp the value was stored at
|
246
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
|
247
|
+
def incrby(value = 1, timestamp = nil, uncompressed: nil)
|
248
|
+
cmd 'TS.INCRBY', key, value, (timestamp if timestamp), ('UNCOMPRESSED' if uncompressed)
|
91
249
|
end
|
92
250
|
alias increment incrby
|
93
251
|
|
94
|
-
#
|
252
|
+
# Get information about the series.
|
253
|
+
# Note that all properties of {Info} are also available on the series itself
|
254
|
+
# via delegation.
|
255
|
+
#
|
256
|
+
# @return [Info] an info object about the current series
|
257
|
+
#
|
258
|
+
# @see Info
|
259
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsinfo
|
95
260
|
def info
|
96
|
-
cmd('TS.INFO', key)
|
97
|
-
h[key.gsub(/(.)([A-Z])/,'\1_\2').downcase] = value
|
98
|
-
h
|
99
|
-
end
|
261
|
+
Info.parse series: self, data: cmd('TS.INFO', key)
|
100
262
|
end
|
263
|
+
def_delegators :info, *Info.members - [:series] + %i[count length size source]
|
101
264
|
|
265
|
+
# Assign labels to the series using +TS.ALTER+
|
266
|
+
#
|
267
|
+
# @param val [Hash] a hash of label-value pairs
|
268
|
+
# @return [Hash] the assigned labels
|
269
|
+
#
|
270
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsalter
|
102
271
|
def labels=(val)
|
103
|
-
|
104
|
-
cmd 'TS.ALTER', key, 'LABELS', labels.to_a.flatten
|
272
|
+
cmd 'TS.ALTER', key, 'LABELS', val.to_a
|
105
273
|
end
|
106
274
|
|
107
275
|
def madd(*values)
|
108
276
|
if values.one? && values.first.is_a?(Hash)
|
109
277
|
# Hash of timestamp => value pairs
|
110
278
|
args = values.first.map do |ts, val|
|
111
|
-
ts = ts.ts_msec if ts.is_a? Time
|
112
279
|
[key, ts, val]
|
113
280
|
end.flatten
|
114
281
|
elsif values.one? && values.first.is_a?(Array)
|
@@ -128,30 +295,50 @@ class Redis
|
|
128
295
|
cmd 'TS.MADD', args
|
129
296
|
end
|
130
297
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
298
|
+
# Get a range of values from the series
|
299
|
+
#
|
300
|
+
# @param range [Hash, Range] a time range, or hash of +from+ and +to+ values
|
301
|
+
# @param count [Integer] the maximum number of results to return
|
302
|
+
# @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
|
303
|
+
# The aggregation to apply. Can be an {Aggregation} object, or an array of
|
304
|
+
# aggregation_type and duration +[:avg, 120000]+
|
305
|
+
#
|
306
|
+
# @return [Array<Sample>] an array of samples matching the range query
|
307
|
+
#
|
308
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange
|
309
|
+
def range(range, count: nil, aggregation: nil)
|
310
|
+
if range.is_a?(Hash)
|
311
|
+
# This is to support from: and to: passed in as hash keys
|
312
|
+
# `range` will swallow all parameters if they're all hash syntax
|
313
|
+
count = range.delete(:count)
|
314
|
+
aggregation = range.delete(:aggregation)
|
315
|
+
range = range.fetch(:from)..range.fetch(:to)
|
142
316
|
end
|
317
|
+
cmd('TS.RANGE',
|
318
|
+
key,
|
319
|
+
range.min,
|
320
|
+
range.max,
|
321
|
+
(['COUNT', count] if count),
|
322
|
+
Aggregation.parse(aggregation)&.to_a
|
323
|
+
).map { |ts, val| Sample.new(ts, val) }
|
143
324
|
end
|
144
325
|
|
326
|
+
# Set data retention time for the series using +TS.ALTER+
|
327
|
+
#
|
328
|
+
# @param val [Integer] the number of milliseconds data should be retained. +0+ means retain forever.
|
329
|
+
# @return [Integer] the retention value of the series
|
330
|
+
#
|
331
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsalter
|
145
332
|
def retention=(val)
|
146
|
-
|
333
|
+
# TODO: this should also accept an ActiveSupport::Duration
|
147
334
|
cmd 'TS.ALTER', key, 'RETENTION', val.to_i
|
148
335
|
end
|
149
336
|
|
150
|
-
|
151
|
-
|
152
|
-
def
|
153
|
-
|
154
|
-
|
337
|
+
# Compare series based on Redis key and configured client.
|
338
|
+
# @return [Boolean] whether the two TimeSeries objects refer to the same series
|
339
|
+
def ==(other)
|
340
|
+
return false unless other.is_a?(self.class)
|
341
|
+
key == other.key && redis == other.redis
|
155
342
|
end
|
156
343
|
end
|
157
344
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Redis
|
3
|
+
class TimeSeries
|
4
|
+
# An aggregation is a combination of a mathematical function, and a time window over
|
5
|
+
# which to apply that function. In RedisTimeSeries, aggregations are used to downsample
|
6
|
+
# data from a source series to a destination series, using compaction rules.
|
7
|
+
#
|
8
|
+
# @see Redis::TimeSeries#create_rule
|
9
|
+
# @see Redis::TimeSeries::Rule
|
10
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#aggregation-compaction-downsampling
|
11
|
+
class Aggregation
|
12
|
+
TYPES = %w[
|
13
|
+
avg
|
14
|
+
count
|
15
|
+
first
|
16
|
+
last
|
17
|
+
max
|
18
|
+
min
|
19
|
+
range
|
20
|
+
std.p
|
21
|
+
std.s
|
22
|
+
sum
|
23
|
+
var.p
|
24
|
+
var.s
|
25
|
+
]
|
26
|
+
|
27
|
+
# @return [String] the type of aggregation to apply
|
28
|
+
# @see TYPES
|
29
|
+
attr_reader :type
|
30
|
+
alias aggregation_type type
|
31
|
+
|
32
|
+
# @return [Integer] the time window to apply the aggregation over, in milliseconds
|
33
|
+
attr_reader :duration
|
34
|
+
alias time_bucket duration
|
35
|
+
|
36
|
+
# Parse a method argument into an aggregation.
|
37
|
+
#
|
38
|
+
# @param agg [Array, Aggregation] an aggregation object, or an array of type and duration +[:avg, 60000]+
|
39
|
+
# @return [Aggregation] the parsed aggregation, or the original argument if already an aggregation
|
40
|
+
# @raise [AggregationError] when given an unparseable value
|
41
|
+
def self.parse(agg)
|
42
|
+
return unless agg
|
43
|
+
return agg if agg.is_a?(self)
|
44
|
+
return new(agg.first, agg.last) if agg.is_a?(Array) && agg.size == 2
|
45
|
+
raise AggregationError, "Couldn't parse #{agg} into an aggregation rule!"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create a new Aggregation given a type and duration.
|
49
|
+
# @param type [String, Symbol] one of the valid aggregation {TYPES}
|
50
|
+
# @param duration [Integer, ActiveSupport::Duration]
|
51
|
+
# A time window to apply this aggregation over.
|
52
|
+
# If you're using ActiveSupport, duration objects (e.g. +10.minutes+) will be automatically coerced.
|
53
|
+
# @return [Aggregation]
|
54
|
+
# @raise [AggregationError] if the given aggregation type is not valid
|
55
|
+
def initialize(type, duration)
|
56
|
+
type = type.to_s.downcase
|
57
|
+
unless TYPES.include? type
|
58
|
+
raise AggregationError, "#{type} is not a valid aggregation type!"
|
59
|
+
end
|
60
|
+
@type = type
|
61
|
+
if defined?(ActiveSupport::Duration) && duration.is_a?(ActiveSupport::Duration)
|
62
|
+
@duration = duration.in_milliseconds
|
63
|
+
else
|
64
|
+
@duration = duration.to_i
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
# @return [Array]
|
70
|
+
def to_a
|
71
|
+
['AGGREGATION', type, duration]
|
72
|
+
end
|
73
|
+
|
74
|
+
# @api private
|
75
|
+
# @return [String]
|
76
|
+
def to_s
|
77
|
+
to_a.join(' ')
|
78
|
+
end
|
79
|
+
|
80
|
+
# Compares aggregations based on type and duration.
|
81
|
+
# @return [Boolean] whether the given aggregations are equivalent
|
82
|
+
def ==(other)
|
83
|
+
parsed = self.class.parse(other)
|
84
|
+
type == parsed.type && duration == parsed.duration
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|