redis-time-series 0.4.0 → 0.5.0
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 +15 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +9 -1
- data/README.md +71 -16
- data/bin/setup +2 -1
- data/lib/ext/time_msec.rb +21 -0
- data/lib/redis-time-series.rb +3 -3
- data/lib/redis/time_series.rb +229 -78
- data/lib/redis/time_series/aggregation.rb +40 -5
- data/lib/redis/time_series/client.rb +77 -0
- data/lib/redis/time_series/errors.rb +9 -5
- data/lib/redis/time_series/info.rb +58 -8
- 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 +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60ddc8ba631c4016031490a9eb8c3e8659d6b38d3f21e3798a253ec3f63b2815
|
4
|
+
data.tar.gz: 7993209c75f9f23ed0fae6f918020f2a7a793575e6636e57c1bc225db3f3a63d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bd033ed2dfef155ba923ebee94089e41f507dcb8fcb63711a56585703d1603c32ff86b99067641b8a248587925d2bf489affd79822118e5fb42b05dbcf1c374
|
7
|
+
data.tar.gz: 553ffbea629efff4b1b436faf7542f35204cd9a0f47c2cf28dcd3ecc0261bd399eb19ac57a56985fc3fbeaef0e4b2e24786483bc385e06928fedcbad4f6709d0
|
data/.github/workflows/rspec.yml
CHANGED
@@ -16,13 +16,28 @@ jobs:
|
|
16
16
|
image: redislabs/redistimeseries:latest
|
17
17
|
ports:
|
18
18
|
- 6379:6379/tcp
|
19
|
+
env:
|
20
|
+
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
|
21
|
+
GIT_COMMIT_SHA: ${{ github.sha }}
|
22
|
+
GIT_BRANCH: ${{ github.head_ref }}
|
19
23
|
steps:
|
20
24
|
- uses: actions/checkout@v2
|
21
25
|
- name: Set up Ruby
|
22
26
|
uses: ruby/setup-ruby@v1
|
23
27
|
with:
|
24
28
|
ruby-version: 2.6
|
29
|
+
- name: Set up CodeClimate
|
30
|
+
run: |
|
31
|
+
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
32
|
+
chmod +x ./cc-test-reporter
|
33
|
+
./cc-test-reporter before-build
|
25
34
|
- name: Install dependencies
|
26
35
|
run: bundle install
|
27
36
|
- name: Run specs
|
28
37
|
run: bundle exec rake spec
|
38
|
+
- name: Upload coverage report
|
39
|
+
run: ./cc-test-reporter after-build -t simplecov coverage/.resultset.json
|
40
|
+
- uses: actions/upload-artifact@v2
|
41
|
+
with:
|
42
|
+
name: coverage
|
43
|
+
path: coverage/
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## 0.5.0
|
6
|
+
* Fix aggregations for TS.RANGE command (#34)
|
7
|
+
* Extract client handling into Client module (#32)
|
8
|
+
* Add `uncompressed` param to TS.ADD, TS.INCRBY, TS.DECRBY (#35)
|
9
|
+
* Add `Redis::TimeSeries::Rule` object (#38)
|
10
|
+
* Add [YARD documentation](https://rubydoc.info/gems/redis-time-series) (#40)
|
11
|
+
|
5
12
|
## 0.4.0
|
6
13
|
* Added [hash-based filter DSL](https://github.com/dzunk/redis-time-series/tree/7173c73588da50614c02f9c89bf2ecef77766a78#filter-dsl)
|
7
14
|
* Removed `Time#ts_msec` monkey-patch
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
redis-time-series (0.
|
4
|
+
redis-time-series (0.5.0)
|
5
5
|
redis (~> 4.0)
|
6
6
|
|
7
7
|
GEM
|
@@ -16,8 +16,10 @@ GEM
|
|
16
16
|
coderay (1.1.3)
|
17
17
|
concurrent-ruby (1.1.6)
|
18
18
|
diff-lcs (1.3)
|
19
|
+
docile (1.3.2)
|
19
20
|
i18n (1.8.3)
|
20
21
|
concurrent-ruby (~> 1.0)
|
22
|
+
json (2.3.1)
|
21
23
|
method_source (1.0.0)
|
22
24
|
minitest (5.14.1)
|
23
25
|
pry (0.13.1)
|
@@ -38,6 +40,11 @@ GEM
|
|
38
40
|
diff-lcs (>= 1.2.0, < 2.0)
|
39
41
|
rspec-support (~> 3.9.0)
|
40
42
|
rspec-support (3.9.3)
|
43
|
+
simplecov (0.17.1)
|
44
|
+
docile (~> 1.1)
|
45
|
+
json (>= 1.8, < 3)
|
46
|
+
simplecov-html (~> 0.10.0)
|
47
|
+
simplecov-html (0.10.2)
|
41
48
|
thread_safe (0.3.6)
|
42
49
|
tzinfo (1.2.7)
|
43
50
|
thread_safe (~> 0.1)
|
@@ -53,6 +60,7 @@ DEPENDENCIES
|
|
53
60
|
rake (~> 13.0)
|
54
61
|
redis-time-series!
|
55
62
|
rspec (~> 3.0)
|
63
|
+
simplecov (< 0.18)
|
56
64
|
|
57
65
|
BUNDLED WITH
|
58
66
|
1.17.2
|
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
[](https://github.com/dzunk/redis-time-series/actions?query=workflow%3ARSpec+branch%3Amaster)
|
2
2
|
[](https://badge.fury.io/rb/redis-time-series)
|
3
|
+
[](https://rubydoc.info/gems/redis-time-series)
|
4
|
+
[](https://codeclimate.com/github/dzunk/redis-time-series/maintainability)
|
5
|
+
[](https://codeclimate.com/github/dzunk/redis-time-series/test_coverage)
|
3
6
|
|
4
7
|
# RedisTimeSeries
|
5
8
|
|
@@ -73,6 +76,10 @@ Add a single value with a timestamp
|
|
73
76
|
```ruby
|
74
77
|
ts.add 1234, 3.minutes.ago # Used ActiveSupport here, but any Time object works fine
|
75
78
|
=> #<Redis::TimeSeries::Sample:0x00007fa6ce05f3f8 @time=2020-06-25 23:39:54 -0700, @value=0.1234e4>
|
79
|
+
|
80
|
+
# Optionally store data uncompressed
|
81
|
+
ts.add 5678, uncompressed: true
|
82
|
+
=> #<Redis::TimeSeries::Sample:0x00007f93f43cdf68 @time=2020-07-18 23:15:29 -0700, @value=0.5678e4>
|
76
83
|
```
|
77
84
|
Add multiple values with timestamps
|
78
85
|
```ruby
|
@@ -89,6 +96,10 @@ ts.increment # alias of incrby
|
|
89
96
|
=> 1593154255069
|
90
97
|
ts.decrement # alias of decrby
|
91
98
|
=> 1593154257344
|
99
|
+
|
100
|
+
# Optionally store data uncompressed
|
101
|
+
ts.incrby 4, uncompressed: true
|
102
|
+
=> 1595139299769
|
92
103
|
```
|
93
104
|
```ruby
|
94
105
|
ts.get
|
@@ -120,31 +131,53 @@ ts.get
|
|
120
131
|
```
|
121
132
|
Get a range of values
|
122
133
|
```ruby
|
123
|
-
|
134
|
+
# Time range as an argument
|
135
|
+
ts.range(10.minutes.ago..Time.current)
|
124
136
|
=> [#<Redis::TimeSeries::Sample:0x00007fa25f13fc28 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
137
|
+
#<Redis::TimeSeries::Sample:0x00007fa25f13db58 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
|
138
|
+
#<Redis::TimeSeries::Sample:0x00007fa25f13d900 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
|
139
|
+
#<Redis::TimeSeries::Sample:0x00007fa25f13d680 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
|
140
|
+
|
141
|
+
# Time range as keyword args
|
142
|
+
ts.range(from: 10.minutes.ago, to: Time.current)
|
129
143
|
=> [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
|
130
|
-
|
131
|
-
|
132
|
-
|
144
|
+
#<Redis::TimeSeries::Sample:0x00007fa25dc01d20 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
|
145
|
+
#<Redis::TimeSeries::Sample:0x00007fa25dc01b68 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
|
146
|
+
#<Redis::TimeSeries::Sample:0x00007fa25dc019b0 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
|
147
|
+
|
148
|
+
# Limit number of results with count argument
|
149
|
+
ts.range(10.minutes.ago..Time.current, count: 2)
|
150
|
+
=> [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
|
151
|
+
#<Redis::TimeSeries::Sample:0x00007fa25dc01d20 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>]
|
152
|
+
|
153
|
+
# Apply aggregations to the range
|
154
|
+
ts.range(from: 10.minutes.ago, to: Time.current, aggregation: [:avg, 10.minutes])
|
155
|
+
=> [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:00 -0700, @value=0.575e2>]
|
133
156
|
```
|
134
157
|
Get info about the series
|
135
158
|
```ruby
|
136
159
|
ts.info
|
137
160
|
=> #<struct Redis::TimeSeries::Info
|
161
|
+
series=
|
162
|
+
#<Redis::TimeSeries:0x00007ff46da9b578 @key="ts3", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>,
|
138
163
|
total_samples=3,
|
139
|
-
memory_usage=
|
140
|
-
first_timestamp=
|
141
|
-
last_timestamp=
|
164
|
+
memory_usage=4264,
|
165
|
+
first_timestamp=1595187993605,
|
166
|
+
last_timestamp=1595187993629,
|
142
167
|
retention_time=0,
|
143
168
|
chunk_count=1,
|
144
169
|
max_samples_per_chunk=256,
|
145
170
|
labels={"foo"=>"bar"},
|
146
171
|
source_key=nil,
|
147
|
-
rules=
|
172
|
+
rules=
|
173
|
+
[#<Redis::TimeSeries::Rule:0x00007ff46db30c68
|
174
|
+
@aggregation=#<Redis::TimeSeries::Aggregation:0x00007ff46db30c18 @duration=3600000, @type="avg">,
|
175
|
+
@destination_key="ts1",
|
176
|
+
@source=
|
177
|
+
#<Redis::TimeSeries:0x00007ff46da9b578
|
178
|
+
@key="ts3",
|
179
|
+
@redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>>]>
|
180
|
+
|
148
181
|
# Each info property is also a method on the time series object
|
149
182
|
ts.memory_usage
|
150
183
|
=> 4208
|
@@ -152,6 +185,7 @@ ts.labels
|
|
152
185
|
=> {"foo"=>"bar"}
|
153
186
|
ts.total_samples
|
154
187
|
=> 3
|
188
|
+
|
155
189
|
# Total samples also available as #count, #length, and #size
|
156
190
|
ts.count
|
157
191
|
=> 3
|
@@ -222,15 +256,19 @@ Redis::TimeSeries.where(person: Person.new('John'))
|
|
222
256
|
### Compaction Rules
|
223
257
|
Add a compaction rule to a series.
|
224
258
|
```ruby
|
225
|
-
#
|
259
|
+
# Destintation time series needs to be created before the rule is added.
|
226
260
|
other_ts = Redis::TimeSeries.create('other_ts')
|
227
261
|
|
228
|
-
#
|
229
|
-
ts.create_rule(dest: other_ts, aggregation: [:count,
|
262
|
+
# Aggregation buckets are measured in milliseconds
|
263
|
+
ts.create_rule(dest: other_ts, aggregation: [:count, 60000]) # 1 minute
|
230
264
|
|
231
|
-
# Can
|
265
|
+
# Can provide a string key instead of a time series object
|
232
266
|
ts.create_rule(dest: 'other_ts', aggregation: [:avg, 120000])
|
233
267
|
|
268
|
+
# If you're using Rails or ActiveSupport, you can provide an
|
269
|
+
# ActiveSupport::Duration instead of an integer
|
270
|
+
ts.create_rule(dest: other_ts, aggregation: [:avg, 2.minutes])
|
271
|
+
|
234
272
|
# Can also provide an Aggregation object instead of an array
|
235
273
|
agg = Redis::TimeSeries::Aggregation.new(:avg, 120000)
|
236
274
|
ts.create_rule(dest: other_ts, aggregation: agg)
|
@@ -238,9 +276,26 @@ ts.create_rule(dest: other_ts, aggregation: agg)
|
|
238
276
|
# Class-level method also available
|
239
277
|
Redis::TimeSeries.create_rule(source: ts, dest: other_ts, aggregation: ['std.p', 150000])
|
240
278
|
```
|
279
|
+
Get existing compaction rules
|
280
|
+
```ruby
|
281
|
+
ts.rules
|
282
|
+
=> [#<Redis::TimeSeries::Rule:0x00007ff46e91c728
|
283
|
+
@aggregation=#<Redis::TimeSeries::Aggregation:0x00007ff46e91c6d8 @duration=3600000, @type="avg">,
|
284
|
+
@destination_key="ts1",
|
285
|
+
@source=
|
286
|
+
#<Redis::TimeSeries:0x00007ff46da9b578 @key="ts3", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>>]
|
287
|
+
|
288
|
+
# Get properties of a rule too
|
289
|
+
ts.rules.first.aggregation
|
290
|
+
=> #<Redis::TimeSeries::Aggregation:0x00007ff46d146d38 @duration=3600000, @type="avg">
|
291
|
+
ts.rules.first.destination
|
292
|
+
=> #<Redis::TimeSeries:0x00007ff46d8a3d60 @key="ts1", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>
|
293
|
+
```
|
294
|
+
|
241
295
|
Remove an existing compaction rule
|
242
296
|
```ruby
|
243
297
|
ts.delete_rule(dest: 'other_ts')
|
298
|
+
ts.rules.first.delete
|
244
299
|
Redis::TimeSeries.delete_rule(source: ts, dest: 'other_ts')
|
245
300
|
```
|
246
301
|
|
data/bin/setup
CHANGED
@@ -3,10 +3,12 @@
|
|
3
3
|
system 'bundle install'
|
4
4
|
system 'docker pull redislabs/redistimeseries:latest'
|
5
5
|
container_id = `docker run -p 6379:6379 -dit --rm redislabs/redistimeseries`
|
6
|
+
at_exit { system "docker stop #{container_id}" }
|
6
7
|
|
7
8
|
require 'bundler/setup'
|
8
9
|
require 'active_support/core_ext/numeric/time'
|
9
10
|
require 'redis'
|
11
|
+
require 'redis-time-series'
|
10
12
|
|
11
13
|
Redis.current.flushall
|
12
14
|
ts1 = Redis::TimeSeries.create('ts1')
|
@@ -26,5 +28,4 @@ ts3.incrby 2
|
|
26
28
|
sleep 0.01
|
27
29
|
ts3.decrement
|
28
30
|
|
29
|
-
at_exit { system "docker stop #{container_id}" }
|
30
31
|
system "docker logs -f #{container_id}"
|
data/lib/ext/time_msec.rb
CHANGED
@@ -1,8 +1,29 @@
|
|
1
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
|
2
16
|
module TimeMsec
|
3
17
|
refine Time do
|
18
|
+
# TODO: convert to #to_msec
|
4
19
|
def ts_msec
|
5
20
|
(to_f * 1000.0).to_i
|
6
21
|
end
|
7
22
|
end
|
23
|
+
|
24
|
+
refine Time.singleton_class do
|
25
|
+
def from_msec(timestamp)
|
26
|
+
at(timestamp / 1000.0)
|
27
|
+
end
|
28
|
+
end
|
8
29
|
end
|
data/lib/redis-time-series.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
require 'forwardable'
|
3
3
|
require 'ext/time_msec'
|
4
|
+
require 'redis/time_series/client'
|
4
5
|
require 'redis/time_series/errors'
|
5
6
|
require 'redis/time_series/aggregation'
|
6
7
|
require 'redis/time_series/filters'
|
8
|
+
require 'redis/time_series/rule'
|
7
9
|
require 'redis/time_series/info'
|
8
10
|
require 'redis/time_series/sample'
|
9
11
|
require 'redis/time_series'
|
10
12
|
|
11
|
-
class RedisTimeSeries
|
12
|
-
VERSION = '0.4.0'
|
13
|
-
end
|
13
|
+
class RedisTimeSeries; end
|
data/lib/redis/time_series.rb
CHANGED
@@ -2,120 +2,233 @@
|
|
2
2
|
using TimeMsec
|
3
3
|
|
4
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>
|
5
24
|
class TimeSeries
|
25
|
+
extend Client
|
6
26
|
extend Forwardable
|
7
27
|
|
8
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
|
9
43
|
def create(key, **options)
|
10
|
-
new(key,
|
44
|
+
new(key, redis: options.fetch(:redis, redis)).create(**options)
|
11
45
|
end
|
12
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
|
13
62
|
def create_rule(source:, dest:, aggregation:)
|
14
|
-
|
15
|
-
source.is_a?(self) ? source.key : source.to_s,
|
16
|
-
dest.is_a?(self) ? dest.key : dest.to_s,
|
17
|
-
Aggregation.parse(aggregation).to_a
|
18
|
-
]
|
19
|
-
redis.call 'TS.CREATERULE', *args.flatten
|
63
|
+
cmd 'TS.CREATERULE', key_for(source), key_for(dest), Aggregation.parse(aggregation).to_a
|
20
64
|
end
|
21
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
|
22
73
|
def delete_rule(source:, dest:)
|
23
|
-
|
24
|
-
source.is_a?(self) ? source.key : source.to_s,
|
25
|
-
dest.is_a?(self) ? dest.key : dest.to_s
|
26
|
-
]
|
27
|
-
redis.call 'TS.DELETERULE', *args
|
74
|
+
cmd 'TS.DELETERULE', key_for(source), key_for(dest)
|
28
75
|
end
|
29
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
|
30
82
|
def destroy(key)
|
31
83
|
redis.del key
|
32
84
|
end
|
33
85
|
|
34
86
|
def madd(data)
|
35
87
|
data.reduce([]) do |memo, (key, value)|
|
36
|
-
|
37
|
-
# multiple timestamp => value pairs
|
38
|
-
value.each do |timestamp, nested_value|
|
39
|
-
timestamp = timestamp.ts_msec if timestamp.is_a? Time
|
40
|
-
memo << [key, timestamp, nested_value]
|
41
|
-
end
|
42
|
-
elsif value.is_a? Array
|
43
|
-
# single [timestamp, value]
|
44
|
-
key = key.ts_msec if key.is_a? Time
|
45
|
-
memo << [key, value]
|
46
|
-
else
|
47
|
-
# single value, no timestamp
|
48
|
-
memo << [key, '*', value]
|
49
|
-
end
|
88
|
+
memo << parse_madd_values(key, value)
|
50
89
|
memo
|
51
90
|
end.then do |args|
|
52
|
-
|
53
|
-
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|
|
54
92
|
result.is_a?(Redis::CommandError) ? result : Sample.new(result, args[idx][2])
|
55
93
|
end
|
56
94
|
end
|
57
95
|
end
|
58
96
|
|
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
|
59
113
|
def query_index(filter_value)
|
60
114
|
filters = Filters.new(filter_value)
|
61
115
|
filters.validate!
|
62
|
-
|
63
|
-
redis.call('TS.QUERYINDEX', *filters.to_a).map { |key| new(key) }
|
116
|
+
cmd('TS.QUERYINDEX', filters.to_a).map { |key| new(key) }
|
64
117
|
end
|
65
118
|
alias where query_index
|
66
119
|
|
67
|
-
|
68
|
-
|
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
|
69
124
|
end
|
70
125
|
|
71
|
-
def
|
72
|
-
|
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
|
73
139
|
end
|
74
140
|
end
|
75
141
|
|
76
|
-
|
142
|
+
# @return [String] the Redis key this time series is stored in
|
143
|
+
attr_reader :key
|
77
144
|
|
78
|
-
|
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)
|
79
148
|
@key = key
|
80
|
-
@redis =
|
81
|
-
@retention = options[:retention]
|
82
|
-
@uncompressed = options[:uncompressed] || false
|
149
|
+
@redis = redis
|
83
150
|
end
|
84
151
|
|
85
|
-
|
86
|
-
|
87
|
-
|
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)
|
88
162
|
Sample.new(ts, value)
|
89
163
|
end
|
90
164
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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?)
|
97
173
|
self
|
98
174
|
end
|
99
175
|
|
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
|
100
188
|
def create_rule(dest:, aggregation:)
|
101
189
|
self.class.create_rule(source: self, dest: dest, aggregation: aggregation)
|
102
190
|
end
|
103
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
|
104
200
|
def delete_rule(dest:)
|
105
201
|
self.class.delete_rule(source: self, dest: dest)
|
106
202
|
end
|
107
203
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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)
|
112
214
|
end
|
113
215
|
alias decrement decrby
|
114
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
|
115
222
|
def destroy
|
116
223
|
redis.del key
|
117
224
|
end
|
118
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
|
119
232
|
def get
|
120
233
|
cmd('TS.GET', key).then do |timestamp, value|
|
121
234
|
return unless value
|
@@ -123,28 +236,46 @@ class Redis
|
|
123
236
|
end
|
124
237
|
end
|
125
238
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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)
|
130
249
|
end
|
131
250
|
alias increment incrby
|
132
251
|
|
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
|
133
260
|
def info
|
134
|
-
cmd('TS.INFO', key)
|
261
|
+
Info.parse series: self, data: cmd('TS.INFO', key)
|
135
262
|
end
|
136
|
-
def_delegators :info, *Info.members
|
137
|
-
%i[count length size].each { |m| def_delegator :info, :total_samples, m }
|
263
|
+
def_delegators :info, *Info.members - [:series] + %i[count length size source]
|
138
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
|
139
271
|
def labels=(val)
|
140
|
-
cmd 'TS.ALTER', key, 'LABELS', val.to_a
|
272
|
+
cmd 'TS.ALTER', key, 'LABELS', val.to_a
|
141
273
|
end
|
142
274
|
|
143
275
|
def madd(*values)
|
144
276
|
if values.one? && values.first.is_a?(Hash)
|
145
277
|
# Hash of timestamp => value pairs
|
146
278
|
args = values.first.map do |ts, val|
|
147
|
-
ts = ts.ts_msec if ts.is_a? Time
|
148
279
|
[key, ts, val]
|
149
280
|
end.flatten
|
150
281
|
elsif values.one? && values.first.is_a?(Array)
|
@@ -164,30 +295,50 @@ class Redis
|
|
164
295
|
cmd 'TS.MADD', args
|
165
296
|
end
|
166
297
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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)
|
178
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) }
|
179
324
|
end
|
180
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
|
181
332
|
def retention=(val)
|
182
|
-
|
333
|
+
# TODO: this should also accept an ActiveSupport::Duration
|
183
334
|
cmd 'TS.ALTER', key, 'RETENTION', val.to_i
|
184
335
|
end
|
185
336
|
|
186
|
-
|
187
|
-
|
188
|
-
def
|
189
|
-
|
190
|
-
|
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
|
191
342
|
end
|
192
343
|
end
|
193
344
|
end
|
@@ -1,6 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
class Redis
|
3
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
|
4
11
|
class Aggregation
|
5
12
|
TYPES = %w[
|
6
13
|
avg
|
@@ -17,33 +24,61 @@ class Redis
|
|
17
24
|
var.s
|
18
25
|
]
|
19
26
|
|
20
|
-
|
21
|
-
|
27
|
+
# @return [String] the type of aggregation to apply
|
28
|
+
# @see TYPES
|
29
|
+
attr_reader :type
|
22
30
|
alias aggregation_type type
|
31
|
+
|
32
|
+
# @return [Integer] the time window to apply the aggregation over, in milliseconds
|
33
|
+
attr_reader :duration
|
23
34
|
alias time_bucket duration
|
24
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
|
25
41
|
def self.parse(agg)
|
42
|
+
return unless agg
|
26
43
|
return agg if agg.is_a?(self)
|
27
44
|
return new(agg.first, agg.last) if agg.is_a?(Array) && agg.size == 2
|
28
45
|
raise AggregationError, "Couldn't parse #{agg} into an aggregation rule!"
|
29
46
|
end
|
30
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
|
31
55
|
def initialize(type, duration)
|
32
|
-
|
56
|
+
type = type.to_s.downcase
|
57
|
+
unless TYPES.include? type
|
33
58
|
raise AggregationError, "#{type} is not a valid aggregation type!"
|
34
59
|
end
|
35
|
-
@type = type
|
36
|
-
|
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
|
37
66
|
end
|
38
67
|
|
68
|
+
# @api private
|
69
|
+
# @return [Array]
|
39
70
|
def to_a
|
40
71
|
['AGGREGATION', type, duration]
|
41
72
|
end
|
42
73
|
|
74
|
+
# @api private
|
75
|
+
# @return [String]
|
43
76
|
def to_s
|
44
77
|
to_a.join(' ')
|
45
78
|
end
|
46
79
|
|
80
|
+
# Compares aggregations based on type and duration.
|
81
|
+
# @return [Boolean] whether the given aggregations are equivalent
|
47
82
|
def ==(other)
|
48
83
|
parsed = self.class.parse(other)
|
49
84
|
type == parsed.type && duration == parsed.duration
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
using TimeMsec
|
3
|
+
|
4
|
+
class Redis
|
5
|
+
class TimeSeries
|
6
|
+
# The client module handles connection management for individual time series, and
|
7
|
+
# the parent {TimeSeries} class methods. You can enable or disable debugging, and set
|
8
|
+
# a default Redis client to use for time series objects.
|
9
|
+
module Client
|
10
|
+
def self.extended(base)
|
11
|
+
base.class_eval do
|
12
|
+
attr_reader :redis
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def cmd(name, *args)
|
17
|
+
self.class.send :cmd_with_redis, redis, name, *args
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check debug status. Defaults to on with +DEBUG=true+ environment variable.
|
23
|
+
# @return [Boolean] current debug status
|
24
|
+
def debug
|
25
|
+
@debug.nil? ? [true, 'true', 1].include?(ENV['DEBUG']) : @debug
|
26
|
+
end
|
27
|
+
|
28
|
+
# Enable or disable debug output for time series commands. Enabling debug will
|
29
|
+
# print commands to +STDOUT+ as they're executed.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# [1] pry(main)> @ts1.get
|
33
|
+
# => #<Redis::TimeSeries::Sample:0x00007fc82e9de150 @time=2020-07-19 15:01:13 -0700, @value=0.56e2>
|
34
|
+
# [2] pry(main)> Redis::TimeSeries.debug = true
|
35
|
+
# => true
|
36
|
+
# [3] pry(main)> @ts1.get
|
37
|
+
# DEBUG: TS.GET ts1
|
38
|
+
# => #<Redis::TimeSeries::Sample:0x00007fc82f11b7b0 @time=2020-07-19 15:01:13 -0700, @value=0.56e2>
|
39
|
+
#
|
40
|
+
# @return [Boolean] new debug status
|
41
|
+
def debug=(bool)
|
42
|
+
@debug = !!bool
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Redis] the current Redis client. Defaults to +Redis.current+
|
46
|
+
def redis
|
47
|
+
@redis ||= Redis.current
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set the default Redis client for time series objects.
|
51
|
+
# This may be useful if you already use a non-time-series Redis database, and want
|
52
|
+
# to use both at the same time.
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# # config/initializers/redis_time_series.rb
|
56
|
+
# Redis::TimeSeries.redis = Redis.new(url: 'redis://my-redis-server:6379/0')
|
57
|
+
#
|
58
|
+
# @param client [Redis] a Redis client
|
59
|
+
# @return [Redis]
|
60
|
+
def redis=(client)
|
61
|
+
@redis = client
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def cmd(name, *args)
|
67
|
+
cmd_with_redis redis, name, *args
|
68
|
+
end
|
69
|
+
|
70
|
+
def cmd_with_redis(redis, name, *args)
|
71
|
+
args = args.flatten.compact.map { |arg| arg.is_a?(Time) ? arg.ts_msec : arg }
|
72
|
+
puts "DEBUG: #{name} #{args.join(' ')}" if debug
|
73
|
+
redis.call name, args
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -1,13 +1,17 @@
|
|
1
1
|
class Redis
|
2
2
|
class TimeSeries
|
3
|
-
# Base error class for convenient
|
4
|
-
|
3
|
+
# Base error class for convenient +rescue+-ing.
|
4
|
+
#
|
5
|
+
# Descendant of +Redis::BaseError+, so you can rescue that and capture all
|
6
|
+
# time-series errors, as well as standard Redis command errors.
|
7
|
+
class Error < Redis::BaseError; end
|
5
8
|
|
6
|
-
#
|
7
|
-
#
|
9
|
+
# +FilterError+ is raised when a given set of filters is invalid (i.e. does not contain
|
10
|
+
# a equality comparison "foo=bar"), or the filter value is unparseable.
|
11
|
+
# @see Redis::TimeSeries::Filters
|
8
12
|
class FilterError < Error; end
|
9
13
|
|
10
|
-
#
|
14
|
+
# +AggregationError+ is raised when attempting to create an aggreation with
|
11
15
|
# an unknown type, or when calling a command with an invalid aggregation value.
|
12
16
|
# @see Redis::TimeSeries::Aggregation
|
13
17
|
class AggregationError < Error; end
|
@@ -1,29 +1,79 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
class Redis
|
3
3
|
class TimeSeries
|
4
|
+
# The Info struct wraps the result of the +TS.INFO+ command with method access.
|
5
|
+
# It also applies some limited parsing to the result values, mainly snakifying
|
6
|
+
# the property keys, and instantiating Rule objects if necessary.
|
7
|
+
#
|
8
|
+
# All properties of the struct are also available on a TimeSeries object itself
|
9
|
+
# via delegation.
|
10
|
+
#
|
11
|
+
# @!attribute [r] chunk_count
|
12
|
+
# @return [Integer] number of memory chunks used for the time-series
|
13
|
+
# @!attribute [r] first_timestamp
|
14
|
+
# @return [Integer] first timestamp present in the time-series (milliseconds since epoch)
|
15
|
+
# @!attribute [r] labels
|
16
|
+
# @return [Hash] a hash of label-value pairs that represent metadata labels of the time-series
|
17
|
+
# @!attribute [r] last_timestamp
|
18
|
+
# @return [Integer] last timestamp present in the time-series (milliseconds since epoch)
|
19
|
+
# @!attribute [r] max_samples_per_chunk
|
20
|
+
# @return [Integer] maximum number of samples per memory chunk
|
21
|
+
# @!attribute [r] memory_usage
|
22
|
+
# @return [Integer] total number of bytes allocated for the time-series
|
23
|
+
# @!attribute [r] retention_time
|
24
|
+
# @return [Integer] retention time, in milliseconds, for the time-series.
|
25
|
+
# A zero value means unlimited retention.
|
26
|
+
# @!attribute [r] rules
|
27
|
+
# @return [Array<Rule>] an array of configured compaction {Rule}s
|
28
|
+
# @!attribute [r] series
|
29
|
+
# @return [TimeSeries] the series this info is from
|
30
|
+
# @!attribute [r] source_key
|
31
|
+
# @return [String, nil] the key of the source series, if this series is the destination
|
32
|
+
# of a compaction rule
|
33
|
+
# @!attribute [r] total_samples
|
34
|
+
# @return [Integer] the total number of samples in the series
|
35
|
+
#
|
36
|
+
# @see TimeSeries#info
|
37
|
+
# @see https://oss.redislabs.com/redistimeseries/commands/#tsinfo
|
4
38
|
Info = Struct.new(
|
5
|
-
:
|
6
|
-
:memory_usage,
|
39
|
+
:chunk_count,
|
7
40
|
:first_timestamp,
|
41
|
+
:labels,
|
8
42
|
:last_timestamp,
|
9
|
-
:retention_time,
|
10
|
-
:chunk_count,
|
11
43
|
:max_samples_per_chunk,
|
12
|
-
:
|
13
|
-
:
|
44
|
+
:memory_usage,
|
45
|
+
:retention_time,
|
14
46
|
:rules,
|
47
|
+
:series,
|
48
|
+
:source_key,
|
49
|
+
:total_samples,
|
15
50
|
keyword_init: true
|
16
51
|
) do
|
17
|
-
|
18
|
-
|
52
|
+
# @api private
|
53
|
+
# @return [Info]
|
54
|
+
def self.parse(series:, data:)
|
55
|
+
data.each_slice(2).reduce({}) do |h, (key, value)|
|
19
56
|
# Convert camelCase info keys to snake_case
|
20
57
|
h[key.gsub(/(.)([A-Z])/,'\1_\2').downcase] = value
|
21
58
|
h
|
22
59
|
end.then do |parsed_hash|
|
60
|
+
parsed_hash['series'] = series
|
23
61
|
parsed_hash['labels'] = parsed_hash['labels'].to_h
|
62
|
+
parsed_hash['rules'] = parsed_hash['rules'].map { |d| Rule.new(source: series, data: d) }
|
24
63
|
new(parsed_hash)
|
25
64
|
end
|
26
65
|
end
|
66
|
+
|
67
|
+
alias count total_samples
|
68
|
+
alias length total_samples
|
69
|
+
alias size total_samples
|
70
|
+
|
71
|
+
# If this series is the destination of a compaction rule, returns the source series of the data.
|
72
|
+
# @return [TimeSeries, nil] the series referred to by {source_key}
|
73
|
+
def source
|
74
|
+
return unless source_key
|
75
|
+
@source ||= TimeSeries.new(source_key, redis: series.redis)
|
76
|
+
end
|
27
77
|
end
|
28
78
|
end
|
29
79
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Redis
|
3
|
+
class TimeSeries
|
4
|
+
# A compaction rule applies an aggregation from a source series to a destination series.
|
5
|
+
# As data is added to the source, it will be aggregated based on any configured rule(s) and
|
6
|
+
# distributed to the correct destination(s).
|
7
|
+
#
|
8
|
+
# Compaction rules are useful to retain data over long time periods without requiring exorbitant
|
9
|
+
# amounts of memory and storage. For example, if you're collecting data on a minute-by-minute basis,
|
10
|
+
# you may want to retain a week's worth of data at full fidelity, and a year's worth of data downsampled
|
11
|
+
# to hourly, which would require 60x less memory.
|
12
|
+
class Rule
|
13
|
+
# @return [Aggregation] the configured aggregation for this rule
|
14
|
+
attr_reader :aggregation
|
15
|
+
|
16
|
+
# @return [String] the Redis key of the destination series
|
17
|
+
attr_reader :destination_key
|
18
|
+
|
19
|
+
# @return [TimeSeries] the data source of this compaction rule
|
20
|
+
attr_reader :source
|
21
|
+
|
22
|
+
# Manually instantiating a rule does nothing, don't bother.
|
23
|
+
# @api private
|
24
|
+
# @see Info#rules
|
25
|
+
def initialize(source:, data:)
|
26
|
+
@source = source
|
27
|
+
@destination_key, duration, aggregation_type = data
|
28
|
+
@aggregation = Aggregation.new(aggregation_type, duration)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Delete this compaction rule.
|
32
|
+
# @return [String] the string "OK"
|
33
|
+
def delete
|
34
|
+
source.delete_rule(dest: destination_key)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [TimeSeries] the destination time series this rule refers to
|
38
|
+
def destination
|
39
|
+
@dest ||= TimeSeries.new(destination_key, redis: source.redis)
|
40
|
+
end
|
41
|
+
alias dest destination
|
42
|
+
|
43
|
+
# @return [String] the Redis key of the source series
|
44
|
+
def source_key
|
45
|
+
source.key
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -1,23 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
class Redis
|
3
3
|
class TimeSeries
|
4
|
+
# A sample is an immutable value object that represents a single data point within a time series.
|
4
5
|
class Sample
|
5
|
-
|
6
|
+
using TimeMsec
|
6
7
|
|
7
|
-
|
8
|
+
# @return [Time] the sample's timestamp
|
9
|
+
attr_reader :time
|
10
|
+
# @return [BigDecimal] the decimal value of the sample
|
11
|
+
attr_reader :value
|
8
12
|
|
13
|
+
# Samples are returned by time series query methods, there's no need to create one yourself.
|
14
|
+
# @api private
|
15
|
+
# @see TimeSeries#get
|
16
|
+
# @see TimeSeries#range
|
9
17
|
def initialize(timestamp, value)
|
10
|
-
@time = Time.
|
18
|
+
@time = Time.from_msec(timestamp)
|
11
19
|
@value = BigDecimal(value)
|
12
20
|
end
|
13
21
|
|
14
|
-
|
15
|
-
|
22
|
+
# @return [Integer] the millisecond value of the sample's timestamp
|
23
|
+
# @note
|
24
|
+
# We're wrapping the method provided by the {TimeMsec} refinement for convenience,
|
25
|
+
# otherwise it wouldn't be callable on {time} and devs would have to litter
|
26
|
+
# +using TimeMsec+ or +* 1000.0+ wherever they wanted the value.
|
27
|
+
def to_msec
|
28
|
+
time.ts_msec
|
16
29
|
end
|
17
30
|
|
31
|
+
# @return [Hash] a hash representation of the sample
|
32
|
+
# @example
|
33
|
+
# {:timestamp=>1595199272401, :value=>0.2e1}
|
18
34
|
def to_h
|
19
35
|
{
|
20
|
-
timestamp:
|
36
|
+
timestamp: to_msec,
|
21
37
|
value: value
|
22
38
|
}
|
23
39
|
end
|
data/redis-time-series.gemspec
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'redis
|
4
|
+
require 'redis/time_series/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = 'redis-time-series'
|
8
|
-
spec.version =
|
8
|
+
spec.version = Redis::TimeSeries::VERSION
|
9
9
|
spec.authors = ['Matt Duszynski']
|
10
|
-
spec.email = ['
|
10
|
+
spec.email = ['dzunk@hey.com']
|
11
11
|
|
12
12
|
spec.summary = %q{A Ruby adapter for the RedisTimeSeries module.}
|
13
13
|
# spec.description = %q{TODO: Write a longer description or delete this line.}
|
@@ -38,4 +38,5 @@ Gem::Specification.new do |spec|
|
|
38
38
|
spec.add_development_dependency 'pry', '~> 0.13'
|
39
39
|
spec.add_development_dependency 'rake', '~> 13.0'
|
40
40
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
41
|
+
spec.add_development_dependency 'simplecov', '< 0.18' # https://github.com/codeclimate/test-reporter/issues/413
|
41
42
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-time-series
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Duszynski
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-07-
|
11
|
+
date: 2020-07-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -94,9 +94,23 @@ dependencies:
|
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "<"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.18'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "<"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.18'
|
97
111
|
description:
|
98
112
|
email:
|
99
|
-
-
|
113
|
+
- dzunk@hey.com
|
100
114
|
executables: []
|
101
115
|
extensions: []
|
102
116
|
extra_rdoc_files: []
|
@@ -116,10 +130,13 @@ files:
|
|
116
130
|
- lib/redis-time-series.rb
|
117
131
|
- lib/redis/time_series.rb
|
118
132
|
- lib/redis/time_series/aggregation.rb
|
133
|
+
- lib/redis/time_series/client.rb
|
119
134
|
- lib/redis/time_series/errors.rb
|
120
135
|
- lib/redis/time_series/filters.rb
|
121
136
|
- lib/redis/time_series/info.rb
|
137
|
+
- lib/redis/time_series/rule.rb
|
122
138
|
- lib/redis/time_series/sample.rb
|
139
|
+
- lib/redis/time_series/version.rb
|
123
140
|
- redis-time-series.gemspec
|
124
141
|
homepage: https://github.com/dzunk/redis-time-series
|
125
142
|
licenses:
|