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 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: