redis-time-series 0.3.0 → 0.4.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/CHANGELOG.md +7 -0
- data/Gemfile.lock +1 -1
- data/README.md +73 -4
- data/lib/ext/time_msec.rb +8 -0
- data/lib/redis-time-series.rb +5 -3
- data/lib/redis/time_series.rb +35 -2
- data/lib/redis/time_series/aggregation.rb +53 -0
- data/lib/redis/time_series/errors.rb +15 -0
- data/lib/redis/time_series/{filter.rb → filters.rb} +59 -8
- metadata +6 -4
- data/lib/time/msec.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 58671cce77efea0c1b4f405e60dcd73cb3f01101fabdb0cb9bc8b11335392116
|
4
|
+
data.tar.gz: b9c7e56687b5eb7d5286a88990ca20c5d82f6857070ab953e6d74925d5cbefb6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 402de8f815ef0ff2dfe9426d6be2e68508093c3a35d7def4721f5bfe9ba3a9b52d8b375600302b989c24fc10b4d9c7bc43249a81715267112fa4776d8429ea18
|
7
|
+
data.tar.gz: 3c6a554173d5f9c4a92a77c65c8c8fed8035395b3066ff0cbf222e1983347645fbabf1e498516218a042c3a3c0ad7d157ab5e39b1e22971a73189a5e3aba9496
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## 0.4.0
|
6
|
+
* Added [hash-based filter DSL](https://github.com/dzunk/redis-time-series/tree/7173c73588da50614c02f9c89bf2ecef77766a78#filter-dsl)
|
7
|
+
* Removed `Time#ts_msec` monkey-patch
|
8
|
+
* Renamed `TimeSeries.queryindex` to `.query_index`
|
9
|
+
* Added `TS.CREATERULE` and `TS.DELETERULE` commands
|
10
|
+
* Renamed `InvalidFilters` to `FilterError`
|
11
|
+
|
5
12
|
## 0.3.0
|
6
13
|
* Added `TS.QUERYINDEX` command
|
7
14
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -162,23 +162,92 @@ ts.size
|
|
162
162
|
```
|
163
163
|
Find series matching specific label(s)
|
164
164
|
```ruby
|
165
|
-
Redis::TimeSeries.
|
165
|
+
Redis::TimeSeries.query_index('foo=bar')
|
166
166
|
=> [#<Redis::TimeSeries:0x00007fc115ba1610
|
167
167
|
@key="ts3",
|
168
168
|
@redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>,
|
169
169
|
@retention=nil,
|
170
170
|
@uncompressed=false>]
|
171
171
|
# Note that you need at least one "label equals value" filter
|
172
|
-
Redis::TimeSeries.
|
172
|
+
Redis::TimeSeries.query_index('foo!=bar')
|
173
173
|
=> RuntimeError: Filtering requires at least one equality comparison
|
174
|
+
# query_index is also aliased as .where for fluency
|
175
|
+
Redis::TimeSeries.where('foo=bar')
|
176
|
+
=> [#<Redis::TimeSeries:0x00007fb8981010c8
|
177
|
+
@key="ts3",
|
178
|
+
@redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>,
|
179
|
+
@retention=nil,
|
180
|
+
@uncompressed=false>]
|
181
|
+
```
|
182
|
+
### Filter DSL
|
183
|
+
You can provide filter strings directly, per the time series documentation.
|
184
|
+
```ruby
|
185
|
+
Redis::TimeSeries.where('foo=bar')
|
186
|
+
=> [#<Redis::TimeSeries:0x00007fb8981010c8...>]
|
187
|
+
```
|
188
|
+
There is also a hash-based syntax available, which may be more pleasant to work with.
|
189
|
+
```ruby
|
190
|
+
Redis::TimeSeries.where(foo: 'bar')
|
191
|
+
=> [#<Redis::TimeSeries:0x00007fb89811dca0...>]
|
192
|
+
```
|
193
|
+
All six filter types are represented in hash format below.
|
194
|
+
```ruby
|
195
|
+
{
|
196
|
+
foo: 'bar', # label=value (equality)
|
197
|
+
foo: { not: 'bar' }, # label!=value (inequality)
|
198
|
+
foo: true, # label= (presence)
|
199
|
+
foo: false, # label!= (absence)
|
200
|
+
foo: [1, 2], # label=(1,2) (any value)
|
201
|
+
foo: { not: [1, 2] } # label!=(1,2) (no values)
|
202
|
+
}
|
203
|
+
```
|
204
|
+
Note the special use of `true` and `false`. If you're representing a boolean value with a label, rather than setting its value to "true" or "false" (which would be treated as strings in Redis anyway), you should add or remove the label from the series.
|
205
|
+
|
206
|
+
Values can be any object that responds to `.to_s`:
|
207
|
+
```ruby
|
208
|
+
class Person
|
209
|
+
def initialize(name)
|
210
|
+
@name = name
|
211
|
+
end
|
212
|
+
|
213
|
+
def to_s
|
214
|
+
@name
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
Redis::TimeSeries.where(person: Person.new('John'))
|
219
|
+
#=> TS.QUERYINDEX person=John
|
220
|
+
```
|
221
|
+
|
222
|
+
### Compaction Rules
|
223
|
+
Add a compaction rule to a series.
|
224
|
+
```ruby
|
225
|
+
# `dest` needs to be created before the rule is added.
|
226
|
+
other_ts = Redis::TimeSeries.create('other_ts')
|
227
|
+
|
228
|
+
# Note: aggregation durations are measured in milliseconds
|
229
|
+
ts.create_rule(dest: other_ts, aggregation: [:count, 6000])
|
230
|
+
|
231
|
+
# Can also provide a string key instead of a time series object
|
232
|
+
ts.create_rule(dest: 'other_ts', aggregation: [:avg, 120000])
|
233
|
+
|
234
|
+
# Can also provide an Aggregation object instead of an array
|
235
|
+
agg = Redis::TimeSeries::Aggregation.new(:avg, 120000)
|
236
|
+
ts.create_rule(dest: other_ts, aggregation: agg)
|
237
|
+
|
238
|
+
# Class-level method also available
|
239
|
+
Redis::TimeSeries.create_rule(source: ts, dest: other_ts, aggregation: ['std.p', 150000])
|
240
|
+
```
|
241
|
+
Remove an existing compaction rule
|
242
|
+
```ruby
|
243
|
+
ts.delete_rule(dest: 'other_ts')
|
244
|
+
Redis::TimeSeries.delete_rule(source: ts, dest: 'other_ts')
|
174
245
|
```
|
175
246
|
|
176
247
|
|
177
248
|
### TODO
|
178
249
|
* `TS.REVRANGE`
|
179
250
|
* `TS.MRANGE`/`TS.MREVRANGE`
|
180
|
-
* Compaction rules
|
181
|
-
* Filters
|
182
251
|
* Probably a bunch more stuff
|
183
252
|
|
184
253
|
## Development
|
data/lib/redis-time-series.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
require 'forwardable'
|
3
|
-
require '
|
4
|
-
require 'redis/time_series/
|
3
|
+
require 'ext/time_msec'
|
4
|
+
require 'redis/time_series/errors'
|
5
|
+
require 'redis/time_series/aggregation'
|
6
|
+
require 'redis/time_series/filters'
|
5
7
|
require 'redis/time_series/info'
|
6
8
|
require 'redis/time_series/sample'
|
7
9
|
require 'redis/time_series'
|
8
10
|
|
9
11
|
class RedisTimeSeries
|
10
|
-
VERSION = '0.
|
12
|
+
VERSION = '0.4.0'
|
11
13
|
end
|
data/lib/redis/time_series.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
using TimeMsec
|
3
|
+
|
2
4
|
class Redis
|
3
5
|
class TimeSeries
|
4
6
|
extend Forwardable
|
@@ -8,6 +10,27 @@ class Redis
|
|
8
10
|
new(key, **options).create(labels: options[:labels])
|
9
11
|
end
|
10
12
|
|
13
|
+
def create_rule(source:, dest:, aggregation:)
|
14
|
+
args = [
|
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
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete_rule(source:, dest:)
|
23
|
+
args = [
|
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
|
28
|
+
end
|
29
|
+
|
30
|
+
def destroy(key)
|
31
|
+
redis.del key
|
32
|
+
end
|
33
|
+
|
11
34
|
def madd(data)
|
12
35
|
data.reduce([]) do |memo, (key, value)|
|
13
36
|
if value.is_a?(Hash) || (value.is_a?(Array) && value.first.is_a?(Array))
|
@@ -33,11 +56,13 @@ class Redis
|
|
33
56
|
end
|
34
57
|
end
|
35
58
|
|
36
|
-
def
|
37
|
-
filters =
|
59
|
+
def query_index(filter_value)
|
60
|
+
filters = Filters.new(filter_value)
|
38
61
|
filters.validate!
|
62
|
+
puts "DEBUG: TS.QUERYINDEX #{filters.to_a.join(' ')}" if ENV['DEBUG']
|
39
63
|
redis.call('TS.QUERYINDEX', *filters.to_a).map { |key| new(key) }
|
40
64
|
end
|
65
|
+
alias where query_index
|
41
66
|
|
42
67
|
def redis
|
43
68
|
@redis ||= Redis.current
|
@@ -72,6 +97,14 @@ class Redis
|
|
72
97
|
self
|
73
98
|
end
|
74
99
|
|
100
|
+
def create_rule(dest:, aggregation:)
|
101
|
+
self.class.create_rule(source: self, dest: dest, aggregation: aggregation)
|
102
|
+
end
|
103
|
+
|
104
|
+
def delete_rule(dest:)
|
105
|
+
self.class.delete_rule(source: self, dest: dest)
|
106
|
+
end
|
107
|
+
|
75
108
|
def decrby(value = 1, timestamp = nil)
|
76
109
|
args = [key, value]
|
77
110
|
args << timestamp if timestamp
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Redis
|
3
|
+
class TimeSeries
|
4
|
+
class Aggregation
|
5
|
+
TYPES = %w[
|
6
|
+
avg
|
7
|
+
count
|
8
|
+
first
|
9
|
+
last
|
10
|
+
max
|
11
|
+
min
|
12
|
+
range
|
13
|
+
std.p
|
14
|
+
std.s
|
15
|
+
sum
|
16
|
+
var.p
|
17
|
+
var.s
|
18
|
+
]
|
19
|
+
|
20
|
+
attr_reader :type, :duration
|
21
|
+
|
22
|
+
alias aggregation_type type
|
23
|
+
alias time_bucket duration
|
24
|
+
|
25
|
+
def self.parse(agg)
|
26
|
+
return agg if agg.is_a?(self)
|
27
|
+
return new(agg.first, agg.last) if agg.is_a?(Array) && agg.size == 2
|
28
|
+
raise AggregationError, "Couldn't parse #{agg} into an aggregation rule!"
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(type, duration)
|
32
|
+
unless TYPES.include? type.to_s
|
33
|
+
raise AggregationError, "#{type} is not a valid aggregation type!"
|
34
|
+
end
|
35
|
+
@type = type.to_s
|
36
|
+
@duration = duration.to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_a
|
40
|
+
['AGGREGATION', type, duration]
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
to_a.join(' ')
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
parsed = self.class.parse(other)
|
49
|
+
type == parsed.type && duration == parsed.duration
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Redis
|
2
|
+
class TimeSeries
|
3
|
+
# Base error class for convenient `rescue`ing
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# Invalid filter error is raised when attempting to filter without at least
|
7
|
+
# one equality comparison ("foo=bar")
|
8
|
+
class FilterError < Error; end
|
9
|
+
|
10
|
+
# Aggregation error is raised when attempting to create anaggreation with
|
11
|
+
# an unknown type, or when calling a command with an invalid aggregation value.
|
12
|
+
# @see Redis::TimeSeries::Aggregation
|
13
|
+
class AggregationError < Error; end
|
14
|
+
end
|
15
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
class Redis
|
3
3
|
class TimeSeries
|
4
|
-
class
|
4
|
+
class Filters
|
5
5
|
Equal = Struct.new(:label, :value) do
|
6
6
|
self::REGEX = /^[^!]+=[^(]+/
|
7
7
|
|
@@ -9,6 +9,10 @@ class Redis
|
|
9
9
|
new(*str.split('='))
|
10
10
|
end
|
11
11
|
|
12
|
+
def to_h
|
13
|
+
{ label => value }
|
14
|
+
end
|
15
|
+
|
12
16
|
def to_s
|
13
17
|
"#{label}=#{value}"
|
14
18
|
end
|
@@ -21,6 +25,10 @@ class Redis
|
|
21
25
|
new(*str.split('!='))
|
22
26
|
end
|
23
27
|
|
28
|
+
def to_h
|
29
|
+
{ label => { not: value } }
|
30
|
+
end
|
31
|
+
|
24
32
|
def to_s
|
25
33
|
"#{label}!=#{value}"
|
26
34
|
end
|
@@ -33,6 +41,10 @@ class Redis
|
|
33
41
|
new(str.delete('='))
|
34
42
|
end
|
35
43
|
|
44
|
+
def to_h
|
45
|
+
{ label => false }
|
46
|
+
end
|
47
|
+
|
36
48
|
def to_s
|
37
49
|
"#{label}="
|
38
50
|
end
|
@@ -45,6 +57,10 @@ class Redis
|
|
45
57
|
new(str.delete('!='))
|
46
58
|
end
|
47
59
|
|
60
|
+
def to_h
|
61
|
+
{ label => true }
|
62
|
+
end
|
63
|
+
|
48
64
|
def to_s
|
49
65
|
"#{label}!="
|
50
66
|
end
|
@@ -59,8 +75,12 @@ class Redis
|
|
59
75
|
new(label, values)
|
60
76
|
end
|
61
77
|
|
78
|
+
def to_h
|
79
|
+
{ label => values }
|
80
|
+
end
|
81
|
+
|
62
82
|
def to_s
|
63
|
-
"#{label}=(#{values.join(',')})"
|
83
|
+
"#{label}=(#{values.map(&:to_s).join(',')})"
|
64
84
|
end
|
65
85
|
end
|
66
86
|
|
@@ -73,14 +93,18 @@ class Redis
|
|
73
93
|
new(label, values)
|
74
94
|
end
|
75
95
|
|
96
|
+
def to_h
|
97
|
+
{ label => { not: values } }
|
98
|
+
end
|
99
|
+
|
76
100
|
def to_s
|
77
|
-
"#{label}!=(#{values.join(',')})"
|
101
|
+
"#{label}!=(#{values.map(&:to_s).join(',')})"
|
78
102
|
end
|
79
103
|
end
|
80
104
|
|
81
105
|
TYPES = [Equal, NotEqual, Absent, Present, AnyValue, NoValues]
|
82
106
|
TYPES.each do |type|
|
83
|
-
define_method "#{type.to_s.split('::').last.gsub(/(.)([A-Z])/,'\1_\2').downcase}
|
107
|
+
define_method "#{type.to_s.split('::').last.gsub(/(.)([A-Z])/,'\1_\2').downcase}" do
|
84
108
|
filters.select { |f| f.is_a? type }
|
85
109
|
end
|
86
110
|
end
|
@@ -88,12 +112,15 @@ class Redis
|
|
88
112
|
attr_reader :filters
|
89
113
|
|
90
114
|
def initialize(filters = nil)
|
91
|
-
filters =
|
92
|
-
|
115
|
+
@filters = case filters
|
116
|
+
when String then parse_string(filters)
|
117
|
+
when Hash then parse_hash(filters)
|
118
|
+
else []
|
119
|
+
end
|
93
120
|
end
|
94
121
|
|
95
122
|
def validate!
|
96
|
-
valid? || raise('Filtering requires at least one equality comparison')
|
123
|
+
valid? || raise(FilterError, 'Filtering requires at least one equality comparison')
|
97
124
|
end
|
98
125
|
|
99
126
|
def valid?
|
@@ -104,15 +131,39 @@ class Redis
|
|
104
131
|
filters.map(&:to_s)
|
105
132
|
end
|
106
133
|
|
134
|
+
def to_h
|
135
|
+
filters.reduce({}) { |h, filter| h.merge(filter.to_h) }
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_s
|
139
|
+
to_a.join(' ')
|
140
|
+
end
|
141
|
+
|
107
142
|
private
|
108
143
|
|
109
144
|
def parse_string(filter_string)
|
145
|
+
return unless filter_string.is_a? String
|
110
146
|
filter_string.split(' ').map do |str|
|
111
147
|
match = TYPES.find { |f| f::REGEX.match? str }
|
112
|
-
raise "Unable to parse '#{str}'" unless match
|
148
|
+
raise(FilterError, "Unable to parse '#{str}'") unless match
|
113
149
|
match.parse(str)
|
114
150
|
end
|
115
151
|
end
|
152
|
+
|
153
|
+
def parse_hash(filter_hash)
|
154
|
+
return unless filter_hash.is_a? Hash
|
155
|
+
filter_hash.map do |label, value|
|
156
|
+
case value
|
157
|
+
when TrueClass then Present.new(label)
|
158
|
+
when FalseClass then Absent.new(label)
|
159
|
+
when Array then AnyValue.new(label, value)
|
160
|
+
when Hash
|
161
|
+
raise(FilterError, "Invalid filter hash value #{value}") unless value.keys === [:not]
|
162
|
+
(v = value.values.first).is_a?(Array) ? NoValues.new(label, v) : NotEqual.new(label, v)
|
163
|
+
else Equal.new(label, value)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
116
167
|
end
|
117
168
|
end
|
118
169
|
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.4.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-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -112,12 +112,14 @@ files:
|
|
112
112
|
- Rakefile
|
113
113
|
- bin/console
|
114
114
|
- bin/setup
|
115
|
+
- lib/ext/time_msec.rb
|
115
116
|
- lib/redis-time-series.rb
|
116
117
|
- lib/redis/time_series.rb
|
117
|
-
- lib/redis/time_series/
|
118
|
+
- lib/redis/time_series/aggregation.rb
|
119
|
+
- lib/redis/time_series/errors.rb
|
120
|
+
- lib/redis/time_series/filters.rb
|
118
121
|
- lib/redis/time_series/info.rb
|
119
122
|
- lib/redis/time_series/sample.rb
|
120
|
-
- lib/time/msec.rb
|
121
123
|
- redis-time-series.gemspec
|
122
124
|
homepage: https://github.com/dzunk/redis-time-series
|
123
125
|
licenses:
|