RedisTimeSeries
A Ruby adapter for the RedisTimeSeries module.
This doesn't work with vanilla Redis, you need the time series module compiled and installed. Try it with Docker, and see the module setup guide for additional options.
docker run -p 6379:6379 -it --rm redislabs/redistimeseries
TL;DR
require 'redis-time-series'
ts = Redis::TimeSeries.new('foo')
ts.add 1234
=> #<Redis::TimeSeries::Sample:0x00007f8c0d2561d8 @time=2020-06-25 23:23:04 -0700, @value=0.1234e4>
ts.add 56
=> #<Redis::TimeSeries::Sample:0x00007f8c0d26c460 @time=2020-06-25 23:23:16 -0700, @value=0.56e2>
ts.add 78
=> #<Redis::TimeSeries::Sample:0x00007f8c0d276618 @time=2020-06-25 23:23:20 -0700, @value=0.78e2>
ts.range (Time.now.to_i - 100)..Time.now.to_i * 1000
=> [#<Redis::TimeSeries::Sample:0x00007f8c0d297200 @time=2020-06-25 23:23:04 -0700, @value=0.1234e4>,
#<Redis::TimeSeries::Sample:0x00007f8c0d297048 @time=2020-06-25 23:23:16 -0700, @value=0.56e2>,
#<Redis::TimeSeries::Sample:0x00007f8c0d296e90 @time=2020-06-25 23:23:20 -0700, @value=0.78e2>]
Installation
Add this line to your application's Gemfile:
gem 'redis-time-series'
And then execute:
$ bundle
Or install it yourself as:
$ gem install redis-time-series
Usage
Check out the Redis Time Series command documentation first. Should be able to do most of that.
Configuring
You can set the default Redis client for class-level calls operating on multiple series, as well as series created without specifying a client.
Redis::TimeSeries.redis = Redis.new(url: ENV['REDIS_URL'], timeout: 1)
Creating a Series
Create a series (issues TS.CREATE
command) and return a Redis::TimeSeries object for further use. Key param is required, all other arguments are optional.
ts = Redis::TimeSeries.create(
'your_ts_key',
labels: { foo: 'bar' },
retention: 600,
uncompressed: false,
redis: Redis.new(url: ENV['REDIS_URL'])
)
You can also call .new
instead of .create
to skip the TS.CREATE
command.
ts = Redis::TimeSeries.new('your_ts_key')
Adding Data to a Series
Add a single value
ts.add 1234
=> #<Redis::TimeSeries::Sample:0x00007f8c0ea7edc8 @time=2020-06-25 23:41:29 -0700, @value=0.1234e4>
Add a single value with a timestamp
ts.add 1234, 3.minutes.ago
=> #<Redis::TimeSeries::Sample:0x00007fa6ce05f3f8 @time=2020-06-25 23:39:54 -0700, @value=0.1234e4>
ts.add 5678, uncompressed: true
=> #<Redis::TimeSeries::Sample:0x00007f93f43cdf68 @time=2020-07-18 23:15:29 -0700, @value=0.5678e4>
Add multiple values with timestamps
ts.madd(2.minutes.ago => 12, 1.minute.ago => 34, Time.now => 56)
=> [1593153909466, 1593153969466, 1593154029466]
Increment or decrement the most recent value
ts.incrby 2
=> 1593154222877
ts.decrby 1
=> 1593154251392
ts.increment
=> 1593154255069
ts.decrement
=> 1593154257344
ts.incrby 4, uncompressed: true
=> 1595139299769
ts.get
=> #<Redis::TimeSeries::Sample:0x00007fa25f17ed88 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>
ts.increment
=> 1593154290736
ts.get
=> #<Redis::TimeSeries::Sample:0x00007fa25f199480 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>
Add values to multiple series
Redis::TimeSeries.madd(foo: 1234, bar: 5678)
=> [#<Redis::TimeSeries::Sample:0x00007ffb3aa32ae0 @time=2020-06-26 00:09:15 -0700, @value=0.1234e4>,
#<Redis::TimeSeries::Sample:0x00007ffb3aa326d0 @time=2020-06-26 00:09:15 -0700, @value=0.5678e4>]
Redis::TimeSeries.madd(foo: { 1.minute.ago => 1234 }, bar: { 1.minute.ago => 2345 })
=> [#<Redis::TimeSeries::Sample:0x00007fb102431f88 @time=2020-06-26 00:10:22 -0700, @value=0.1234e4>,
#<Redis::TimeSeries::Sample:0x00007fb102431d80 @time=2020-06-26 00:10:22 -0700, @value=0.2345e4>]
Querying a Series
Get the most recent value
ts.get
=> #<Redis::TimeSeries::Sample:0x00007fa25f1b78b8 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>
Get a range of values
ts.range(10.minutes.ago..Time.current)
=> [#<Redis::TimeSeries::Sample:0x00007fa25f13fc28 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
#<Redis::TimeSeries::Sample:0x00007fa25f13db58 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
#<Redis::TimeSeries::Sample:0x00007fa25f13d900 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
#<Redis::TimeSeries::Sample:0x00007fa25f13d680 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
ts.range(from: 10.minutes.ago, to: Time.current)
=> [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
#<Redis::TimeSeries::Sample:0x00007fa25dc01d20 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
#<Redis::TimeSeries::Sample:0x00007fa25dc01b68 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
#<Redis::TimeSeries::Sample:0x00007fa25dc019b0 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
ts.range(10.minutes.ago..Time.current, count: 2)
=> [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
#<Redis::TimeSeries::Sample:0x00007fa25dc01d20 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>]
ts.range(from: 10.minutes.ago, to: Time.current, aggregation: [:avg, 10.minutes])
=> [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:00 -0700, @value=0.575e2>]
Get info about the series
ts.info
=> #<struct Redis::TimeSeries::Info
series=
#<Redis::TimeSeries:0x00007ff46da9b578 @key="ts3", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>,
total_samples=3,
memory_usage=4264,
first_timestamp=1595187993605,
last_timestamp=1595187993629,
retention_time=0,
chunk_count=1,
max_samples_per_chunk=256,
labels={"foo"=>"bar"},
source_key=nil,
rules=
[#<Redis::TimeSeries::Rule:0x00007ff46db30c68
@aggregation=#<Redis::TimeSeries::Aggregation:0x00007ff46db30c18 @duration=3600000, @type="avg">,
@destination_key="ts1",
@source=
#<Redis::TimeSeries:0x00007ff46da9b578
@key="ts3",
@redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>>]>
ts.memory_usage
=> 4208
ts.labels
=> {"foo"=>"bar"}
ts.total_samples
=> 3
ts.count
=> 3
ts.length
=> 3
ts.size
=> 3
Find series matching specific label(s)
Redis::TimeSeries.query_index('foo=bar')
=> [#<Redis::TimeSeries:0x00007fc115ba1610
@key="ts3",
@redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>,
@retention=nil,
@uncompressed=false>]
Redis::TimeSeries.query_index('foo!=bar')
=> RuntimeError: Filtering requires at least one equality comparison
Redis::TimeSeries.where('foo=bar')
=> [#<Redis::TimeSeries:0x00007fb8981010c8
@key="ts3",
@redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>,
@retention=nil,
@uncompressed=false>]
Querying Multiple Series
Get all samples from matching series over a time range with mrange
[4] pry(main)> result = Redis::TimeSeries.mrange(1.minute.ago.., filter: { foo: 'bar' })
=> [#<struct Redis::TimeSeries::Multi::Result
series=
#<Redis::TimeSeries:0x00007f833e408ad0
@key="ts3",
@redis=#<Redis client v4.2.5 for redis://127.0.0.1:6379/0>>,
labels=[],
samples=
[#<Redis::TimeSeries::Sample:0x00007f833e408a58
@time=2021-06-17 20:58:33 3246391/4194304 -0700,
@value=0.1e1>,
#<Redis::TimeSeries::Sample:0x00007f833e408850
@time=2021-06-17 20:58:33 413139/524288 -0700,
@value=0.3e1>,
#<Redis::TimeSeries::Sample:0x00007f833e408670
@time=2021-06-17 20:58:33 1679819/2097152 -0700,
@value=0.2e1>]>]
[5] pry(main)> result.keys
=> ["ts3"]
[6] pry(main)> result['ts3'].values
=> [0.1e1, 0.3e1, 0.2e1]
Order them from newest to oldest with mrevrange
[8] pry(main)> Redis::TimeSeries.mrevrange(1.minute.ago.., filter: { foo: 'bar' }).first.values
=> [0.2e1, 0.3e1, 0.1e1]
Filter DSL
You can provide filter strings directly, per the time series documentation.
Redis::TimeSeries.where('foo=bar')
=> [#<Redis::TimeSeries:0x00007fb8981010c8...>]
There is also a hash-based syntax available, which may be more pleasant to work with.
Redis::TimeSeries.where(foo: 'bar')
=> [#<Redis::TimeSeries:0x00007fb89811dca0...>]
All six filter types are represented in hash format below.
{
foo: 'bar',
foo: { not: 'bar' },
foo: true,
foo: false,
foo: [1, 2],
foo: { not: [1, 2] }
}
Note the special use of true
and false
. If you're representing a boolean value with a label, rather than setting its value to "true" or "false" (which would be treated as strings in Redis anyway), you should add or remove the label from the series.
Values can be any object that responds to .to_s
:
class Person
def initialize(name)
@name = name
end
def to_s
@name
end
end
Redis::TimeSeries.where(person: Person.new('John'))
Compaction Rules
Add a compaction rule to a series.
other_ts = Redis::TimeSeries.create('other_ts')
ts.create_rule(dest: other_ts, aggregation: [:count, 60000])
ts.create_rule(dest: 'other_ts', aggregation: [:avg, 120000])
ts.create_rule(dest: other_ts, aggregation: [:avg, 2.minutes])
agg = Redis::TimeSeries::Aggregation.new(:avg, 120000)
ts.create_rule(dest: other_ts, aggregation: agg)
Redis::TimeSeries.create_rule(source: ts, dest: other_ts, aggregation: ['std.p', 150000])
Get existing compaction rules
ts.rules
=> [#<Redis::TimeSeries::Rule:0x00007ff46e91c728
@aggregation=#<Redis::TimeSeries::Aggregation:0x00007ff46e91c6d8 @duration=3600000, @type="avg">,
@destination_key="ts1",
@source=
#<Redis::TimeSeries:0x00007ff46da9b578 @key="ts3", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>>]
ts.rules.first.aggregation
=> #<Redis::TimeSeries::Aggregation:0x00007ff46d146d38 @duration=3600000, @type="avg">
ts.rules.first.destination
=> #<Redis::TimeSeries:0x00007ff46d8a3d60 @key="ts1", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>
Remove an existing compaction rule
ts.delete_rule(dest: 'other_ts')
ts.rules.first.delete
Redis::TimeSeries.delete_rule(source: ts, dest: 'other_ts')
TODO
Development
After checking out the repo, run bin/setup
. You need the docker
daemon installed and running. This script will:
- Install gem dependencies
- Pull the latest
redislabs/redistimeseries
image - Start a Redis server on port 6379
- Seed three time series with some sample data
- Attach to the running server and print logs to
STDOUT
With the above script running, or after starting a server manually, you can run bin/console
to interact with it. The three series are named ts1
, ts2
, and ts3
, and are available as instance variables in the console.
If you want to see the commands being executed, run the console with DEBUG=true bin/console
and it will output the raw command strings as they're executed.
[1] pry(main)> @ts1.increment
DEBUG: TS.INCRBY ts1 1
=> 1593159795467
[2] pry(main)> @ts1.get
DEBUG: TS.GET ts1
=> #<Redis::TimeSeries::Sample:0x00007f8e1a190cf8 @time=2020-06-26 01:23:15 -0700, @value=0.4e1>
Use rake spec
to run the test suite.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/dzunk/redis-time-series.
License
The gem is available as open source under the terms of the MIT License.