redis-time-series 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58671cce77efea0c1b4f405e60dcd73cb3f01101fabdb0cb9bc8b11335392116
4
- data.tar.gz: b9c7e56687b5eb7d5286a88990ca20c5d82f6857070ab953e6d74925d5cbefb6
3
+ metadata.gz: 60ddc8ba631c4016031490a9eb8c3e8659d6b38d3f21e3798a253ec3f63b2815
4
+ data.tar.gz: 7993209c75f9f23ed0fae6f918020f2a7a793575e6636e57c1bc225db3f3a63d
5
5
  SHA512:
6
- metadata.gz: 402de8f815ef0ff2dfe9426d6be2e68508093c3a35d7def4721f5bfe9ba3a9b52d8b375600302b989c24fc10b4d9c7bc43249a81715267112fa4776d8429ea18
7
- data.tar.gz: 3c6a554173d5f9c4a92a77c65c8c8fed8035395b3066ff0cbf222e1983347645fbabf1e498516218a042c3a3c0ad7d157ab5e39b1e22971a73189a5e3aba9496
6
+ metadata.gz: 1bd033ed2dfef155ba923ebee94089e41f507dcb8fcb63711a56585703d1603c32ff86b99067641b8a248587925d2bf489affd79822118e5fb42b05dbcf1c374
7
+ data.tar.gz: 553ffbea629efff4b1b436faf7542f35204cd9a0f47c2cf28dcd3ecc0261bd399eb19ac57a56985fc3fbeaef0e4b2e24786483bc385e06928fedcbad4f6709d0
@@ -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/
@@ -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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redis-time-series (0.4.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
  [![RSpec](https://github.com/dzunk/redis-time-series/workflows/RSpec/badge.svg)](https://github.com/dzunk/redis-time-series/actions?query=workflow%3ARSpec+branch%3Amaster)
2
2
  [![Gem Version](https://badge.fury.io/rb/redis-time-series.svg)](https://badge.fury.io/rb/redis-time-series)
3
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-brightgreen)](https://rubydoc.info/gems/redis-time-series)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/19a5925c20318508b4a4/maintainability)](https://codeclimate.com/github/dzunk/redis-time-series/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/19a5925c20318508b4a4/test_coverage)](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
- ts.range 10.minutes.ago..Time.current # Time range as an argument
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
- #<Redis::TimeSeries::Sample:0x00007fa25f13db58 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
126
- #<Redis::TimeSeries::Sample:0x00007fa25f13d900 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
127
- #<Redis::TimeSeries::Sample:0x00007fa25f13d680 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
128
- ts.range from: 10.minutes.ago, to: Time.current # Time range as keyword args
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
- #<Redis::TimeSeries::Sample:0x00007fa25dc01d20 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
131
- #<Redis::TimeSeries::Sample:0x00007fa25dc01b68 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
132
- #<Redis::TimeSeries::Sample:0x00007fa25dc019b0 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
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=4184,
140
- first_timestamp=1594060993011,
141
- last_timestamp=1594060993060,
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
- # `dest` needs to be created before the rule is added.
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
- # Note: aggregation durations are measured in milliseconds
229
- ts.create_rule(dest: other_ts, aggregation: [:count, 6000])
262
+ # Aggregation buckets are measured in milliseconds
263
+ ts.create_rule(dest: other_ts, aggregation: [:count, 60000]) # 1 minute
230
264
 
231
- # Can also provide a string key instead of a time series object
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}"
@@ -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
@@ -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
@@ -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, **options).create(labels: options[:labels])
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
- 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
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
- 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
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
- if value.is_a?(Hash) || (value.is_a?(Array) && value.first.is_a?(Array))
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
- puts "DEBUG: TS.MADD #{args.join(' ')}" if ENV['DEBUG']
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
- puts "DEBUG: TS.QUERYINDEX #{filters.to_a.join(' ')}" if ENV['DEBUG']
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
- def redis
68
- @redis ||= Redis.current
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 redis=(client)
72
- @redis = redis
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
- attr_reader :key, :redis, :retention, :uncompressed
142
+ # @return [String] the Redis key this time series is stored in
143
+ attr_reader :key
77
144
 
78
- def initialize(key, options = {})
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 = options[:redis] || self.class.redis
81
- @retention = options[:retention]
82
- @uncompressed = options[:uncompressed] || false
149
+ @redis = redis
83
150
  end
84
151
 
85
- def add(value, timestamp = '*')
86
- timestamp = timestamp.ts_msec if timestamp.is_a? Time
87
- ts = cmd 'TS.ADD', key, timestamp, value
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
- def create(labels: nil)
92
- args = [key]
93
- args << ['RETENTION', retention] if retention
94
- args << 'UNCOMPRESSED' if uncompressed
95
- args << ['LABELS', labels.to_a] if labels&.any?
96
- cmd 'TS.CREATE', args.flatten
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
- def decrby(value = 1, timestamp = nil)
109
- args = [key, value]
110
- args << timestamp if timestamp
111
- cmd 'TS.DECRBY', args
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
- def incrby(value = 1, timestamp = nil)
127
- args = [key, value]
128
- args << timestamp if timestamp
129
- cmd 'TS.INCRBY', args
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).then(&Info.method(:parse))
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.flatten
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
- def range(range, count: nil, agg: nil)
168
- if range.is_a? Hash
169
- args = range.fetch(:from), range.fetch(:to)
170
- elsif range.is_a? Range
171
- args = range.min, range.max
172
- end
173
- args.map! { |ts| (ts.to_f * 1000).to_i }
174
- args.append('COUNT', count) if count
175
- args.append('AGGREGATION', agg) if agg
176
- cmd('TS.RANGE', key, args).map do |ts, val|
177
- Sample.new(ts, val)
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
- @retention = val.to_i
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
- private
187
-
188
- def cmd(name, *args)
189
- puts "DEBUG: #{name} #{args.join(' ')}" if ENV['DEBUG']
190
- redis.call name, *args
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
- attr_reader :type, :duration
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
- unless TYPES.include? type.to_s
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.to_s
36
- @duration = duration.to_i
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 `rescue`ing
4
- class Error < StandardError; end
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
- # Invalid filter error is raised when attempting to filter without at least
7
- # one equality comparison ("foo=bar")
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
- # Aggregation error is raised when attempting to create anaggreation with
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
- :total_samples,
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
- :labels,
13
- :source_key,
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
- def self.parse(raw_array)
18
- raw_array.each_slice(2).reduce({}) do |h, (key, value)|
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
- TS_FACTOR = 1000.0
6
+ using TimeMsec
6
7
 
7
- attr_reader :time, :value
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.at(timestamp / TS_FACTOR)
18
+ @time = Time.from_msec(timestamp)
11
19
  @value = BigDecimal(value)
12
20
  end
13
21
 
14
- def ts_msec
15
- (time.to_f * TS_FACTOR).to_i
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: ts_msec,
36
+ timestamp: to_msec,
21
37
  value: value
22
38
  }
23
39
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ class Redis
3
+ class TimeSeries
4
+ VERSION = '0.5.0'
5
+ end
6
+ end
@@ -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-time-series'
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 = RedisTimeSeries::VERSION
8
+ spec.version = Redis::TimeSeries::VERSION
9
9
  spec.authors = ['Matt Duszynski']
10
- spec.email = ['mattduszynski@gmail.com']
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.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-18 00:00:00.000000000 Z
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
- - mattduszynski@gmail.com
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: