rhcf-timeseries 0.0.6 → 1.0.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.
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'redis'
3
+ require 'rhcf/timeseries/manager'
4
+
5
+ describe Rhcf::Timeseries::RedisGetStrategy do
6
+ it_behaves_like 'a valid strategy'
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'redis'
3
+ require 'rhcf/timeseries/manager'
4
+
5
+ describe Rhcf::Timeseries::RedisHgetallStrategy do
6
+ it_behaves_like 'a valid strategy'
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'redis'
3
+ require 'rhcf/timeseries/manager'
4
+
5
+ describe Rhcf::Timeseries::RedisMgetLuaStrategy do
6
+ it_behaves_like 'a valid strategy'
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'redis'
3
+ require 'rhcf/timeseries/manager'
4
+
5
+ describe Rhcf::Timeseries::RedisMgetStrategy do
6
+ it_behaves_like 'a valid strategy'
7
+ end
data/spec/spec_helper.rb CHANGED
@@ -4,8 +4,7 @@
4
4
  # loaded once.
5
5
  #
6
6
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
- #require 'simplecov'
8
- #SimpleCov.start_with?
7
+ Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
9
8
  require 'timecop'
10
9
  RSpec.configure do |config|
11
10
  config.run_all_when_everything_filtered = true
@@ -0,0 +1,14 @@
1
+ require 'database_cleaner'
2
+ RSpec.configure do |config|
3
+
4
+ config.before(:suite) do
5
+ DatabaseCleaner.strategy = :truncation
6
+ DatabaseCleaner.clean_with(:truncation)
7
+ end
8
+
9
+ config.around(:each) do |example|
10
+ DatabaseCleaner.cleaning do
11
+ example.run
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,2 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
@@ -0,0 +1,119 @@
1
+ require 'redis'
2
+ require 'rhcf/timeseries/manager'
3
+ RSpec.shared_examples 'a valid strategy' do
4
+ let(:redis){Redis.new}
5
+ let(:manager) { Rhcf::Timeseries::Manager.new(connection: redis, strategy: described_class) }
6
+ let(:start_time){ Time.parse("2000-01-01 00:00:00") }
7
+
8
+ before do
9
+ Timecop.travel(start_time)
10
+ manager.store("views/product/15", {"web/firefox/3" => 1})
11
+
12
+ Timecop.travel(15.minutes) #00:00:15
13
+ manager.store("views/product/13", {"web/firefox/3" => 1}, Time.now)
14
+ manager.store("views/product/13", {"web/firefox/3" => 1}, Time.now)
15
+ manager.store("views/product/13", {"web/firefox/3" => 0}, Time.now)
16
+
17
+ Timecop.travel(15.minutes) #00:00:30
18
+ manager.store("views/product/15", {"web/ie/6" => 3})
19
+
20
+ Timecop.travel(15.minutes) #00:00:45
21
+ manager.store("views/product/15", {"web/ie/6" => 2})
22
+
23
+ Timecop.travel(15.minutes) #00:00:00
24
+ manager.store("views/product/11", {"web/ie/5" => 2})
25
+
26
+ Timecop.travel(15.minutes) #00:00:15
27
+ manager.store("views/product/11", {"web/chrome/11"=> 4})
28
+
29
+ Timecop.travel(15.minutes) #00:00:30
30
+ manager.store("views/product/11", {"web/chrome/11"=> 2})
31
+ end
32
+
33
+ it "is similar to redistat" do
34
+
35
+ expect(manager.find("views/product", start_time, start_time + 55.minutes).total(:ever)).to eq({
36
+ "web" => 16.0,
37
+ "web/chrome" => 6.0,
38
+ "web/chrome/11" => 6.0,
39
+ "web/firefox" => 3.0,
40
+ "web/firefox/3" => 3.0,
41
+ "web/ie" => 7.0,
42
+ "web/ie/5" => 2.0,
43
+ "web/ie/6" => 5.0
44
+ })
45
+
46
+ expect(manager.find("views/product", start_time, start_time + 55.minutes).total(:year)).to eq({
47
+ "web" => 16.0,
48
+ "web/chrome" => 6.0,
49
+ "web/chrome/11" => 6.0,
50
+ "web/firefox" => 3.0,
51
+ "web/firefox/3" => 3.0,
52
+ "web/ie" => 7.0,
53
+ "web/ie/5" => 2.0,
54
+ "web/ie/6" => 5.0
55
+ })
56
+
57
+ expect( manager.find("views/product", start_time, start_time + 55.minutes).total ).to eq({
58
+ 'web' => 8,
59
+ 'web/firefox' => 3,
60
+ 'web/firefox/3' => 3,
61
+ 'web/ie' => 5,
62
+ 'web/ie/6' => 5,
63
+ })
64
+
65
+ expect(manager.find("views/product/15", start_time, start_time + 55.minutes).points(:minute)).to eq([
66
+ {:moment=>"2000-01-01T00:00", :values=>{"web/firefox"=>1, "web/firefox/3"=>1, "web"=>1}},
67
+ {:moment=>"2000-01-01T00:30", :values=>{"web"=>3, "web/ie/6"=>3, "web/ie"=>3}},
68
+ {:moment=>"2000-01-01T00:45", :values=>{"web"=>2, "web/ie/6"=>2, "web/ie"=>2}}
69
+ ])
70
+
71
+ expect(manager.find("views/product/13", start_time, start_time + 55.minutes).points(:minute)).to eq([
72
+ {:moment=>"2000-01-01T00:15", :values=>{"web/firefox"=>2, "web/firefox/3"=>2, "web"=>2}},
73
+ ])
74
+
75
+ expect(manager.find("views/product", start_time, start_time + 55.minutes).points(:minute)).to eq([
76
+ {:moment=>"2000-01-01T00:00", :values=>{"web/firefox"=>1, "web/firefox/3"=>1, "web"=>1}},
77
+ {:moment=>"2000-01-01T00:15", :values=>{"web/firefox"=>2, "web/firefox/3"=>2, "web"=>2}},
78
+ {:moment=>"2000-01-01T00:30", :values=>{"web"=>3, "web/ie/6"=>3, "web/ie"=>3}},
79
+ {:moment=>"2000-01-01T00:45", :values=>{"web"=>2, "web/ie/6"=>2, "web/ie"=>2}}
80
+ ])
81
+
82
+ expect(manager.find("views", start_time, start_time + 55.minutes).points(:minute)).to eq([
83
+ {:moment=>"2000-01-01T00:00", :values=>{"web/firefox"=>1, "web/firefox/3"=>1, "web"=>1}},
84
+ {:moment=>"2000-01-01T00:15", :values=>{"web/firefox"=>2, "web/firefox/3"=>2, "web"=>2}},
85
+ {:moment=>"2000-01-01T00:30", :values=>{"web"=>3, "web/ie/6"=>3, "web/ie"=>3}},
86
+ {:moment=>"2000-01-01T00:45", :values=>{"web"=>2, "web/ie/6"=>2, "web/ie"=>2}}
87
+ ])
88
+
89
+ expect(manager.find("views", start_time).points(:hour)).to eq([
90
+ {
91
+ :moment=>"2000-01-01T00",
92
+ :values=> {
93
+ "web/ie"=>5.0,
94
+ "web"=>8.0,
95
+ "web/firefox"=>3.0,
96
+ "web/ie/6"=>5.0,
97
+ "web/firefox/3"=>3.0}
98
+ },{
99
+ :moment=>"2000-01-01T01",
100
+ :values=>{
101
+ "web/ie"=>2.0,
102
+ "web/chrome"=>6.0,
103
+ "web/chrome/11"=>6.0,
104
+ "web"=>8.0,
105
+ "web/ie/5"=>2.0
106
+ }
107
+ }
108
+ ])
109
+ end
110
+
111
+ let(:filter) { Rhcf::Timeseries::Filter.new([:source, :browser], browser: 'firefox.*' )}
112
+ it "can find with filter" do
113
+
114
+ expect(manager.find("views", start_time, start_time + 55.minutes, filter).points(:minute)).to eq([
115
+ {:moment=>"2000-01-01T00:00", :values=>{"web/firefox"=>1, "web/firefox/3"=>1}},
116
+ {:moment=>"2000-01-01T00:15", :values=>{"web/firefox"=>2, "web/firefox/3"=>2}},
117
+ ])
118
+ end
119
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rhcf-timeseries
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Romeu Fonseca
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-10 00:00:00.000000000 Z
11
+ date: 2015-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: timecop
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,20 @@ dependencies:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: database_cleaner
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
97
125
  description: Gem to allow your system to keep record of time series on rhcf
98
126
  email:
99
127
  - romeu.hcf@gmail.com
@@ -110,14 +138,25 @@ files:
110
138
  - LICENSE.txt
111
139
  - README.md
112
140
  - Rakefile
141
+ - lib/rhcf/extensions/fixnum.rb
113
142
  - lib/rhcf/timeseries.rb
114
- - lib/rhcf/timeseries/redis.rb
143
+ - lib/rhcf/timeseries/constants.rb
144
+ - lib/rhcf/timeseries/manager.rb
145
+ - lib/rhcf/timeseries/query.rb
146
+ - lib/rhcf/timeseries/redis_strategies.rb
115
147
  - lib/rhcf/timeseries/version.rb
116
148
  - rhcf-timeseries.gemspec
117
149
  - spec/lib/rhcf/timeseries/TODO
118
- - spec/lib/rhcf/timeseries/redis_spec.rb
119
- - spec/lib/rhcf/timeseries/result_spec.rb
150
+ - spec/lib/rhcf/timeseries/manager_spec.rb
151
+ - spec/lib/rhcf/timeseries/query_spec.rb
152
+ - spec/lib/rhcf/timeseries/redis_get_strategy_spec.rb
153
+ - spec/lib/rhcf/timeseries/redis_hgetall_strategy_spec.rb
154
+ - spec/lib/rhcf/timeseries/redis_mget_lua_strategy_spec.rb
155
+ - spec/lib/rhcf/timeseries/redis_mget_strategy_spec.rb
120
156
  - spec/spec_helper.rb
157
+ - spec/support/database_cleaner.rb
158
+ - spec/support/simplecov.rb
159
+ - spec/support/valid_strategy_spec.rb
121
160
  homepage: ''
122
161
  licenses:
123
162
  - MIT
@@ -144,6 +183,13 @@ specification_version: 4
144
183
  summary: Redistat inspired redis time series.
145
184
  test_files:
146
185
  - spec/lib/rhcf/timeseries/TODO
147
- - spec/lib/rhcf/timeseries/redis_spec.rb
148
- - spec/lib/rhcf/timeseries/result_spec.rb
186
+ - spec/lib/rhcf/timeseries/manager_spec.rb
187
+ - spec/lib/rhcf/timeseries/query_spec.rb
188
+ - spec/lib/rhcf/timeseries/redis_get_strategy_spec.rb
189
+ - spec/lib/rhcf/timeseries/redis_hgetall_strategy_spec.rb
190
+ - spec/lib/rhcf/timeseries/redis_mget_lua_strategy_spec.rb
191
+ - spec/lib/rhcf/timeseries/redis_mget_strategy_spec.rb
149
192
  - spec/spec_helper.rb
193
+ - spec/support/database_cleaner.rb
194
+ - spec/support/simplecov.rb
195
+ - spec/support/valid_strategy_spec.rb
@@ -1,269 +0,0 @@
1
- require 'date'
2
-
3
- class Fixnum
4
- def minutes
5
- self * 60
6
- end
7
-
8
- def hours
9
- self.minutes * 60
10
- end
11
-
12
- def days
13
- self.hours * 24
14
- end
15
-
16
- def seconds
17
- self
18
- end
19
-
20
- def weeks
21
- self.days * 7
22
- end
23
-
24
- def years
25
- self.days * 365
26
- end
27
-
28
- alias_method :day, :days
29
- alias_method :week, :weeks
30
- alias_method :hour, :hours
31
- alias_method :second, :seconds
32
- alias_method :minute, :minutes
33
- alias_method :year, :years
34
- end
35
-
36
- class NilLogger
37
- def log(*args)
38
-
39
- end
40
-
41
- alias_method :warn, :log
42
- alias_method :debug, :log
43
- alias_method :info, :log
44
- alias_method :error, :log
45
- end
46
-
47
- module Rhcf
48
- module Timeseries
49
-
50
- class Result
51
- def initialize(subject, from, to, series)
52
- if from > to
53
- fail ArgumentError, "Argument 'from' can not be bigger then 'to'"
54
- end
55
- @series = series
56
- @subject = subject
57
- @from = from
58
- @to = to
59
- end
60
-
61
- def total(resolution_id=nil)
62
- accumulator={}
63
- points(resolution_id || better_resolution[:id]) do |data|
64
- data[:values].each do |key, value|
65
- accumulator[key]||=0
66
- accumulator[key]+=value
67
- end
68
- end
69
- accumulator
70
- end
71
-
72
- def points(resolution_id)
73
- list =[]
74
-
75
- point_range(resolution_id) do |point|
76
- values = {}
77
-
78
- @series.events_for_subject_on(@subject, point, resolution_id).each do |event|
79
- value = @series.get('point', @subject, event, resolution_id, point)
80
- values[event] = value.to_i
81
- end
82
-
83
- next if values.empty?
84
- data = {moment: point, values: values }
85
- if block_given?
86
- yield data
87
- else
88
- list << data
89
- end
90
- end
91
- list unless block_given?
92
- end
93
-
94
- def point_range(resolution_id)
95
- resolution = @series.resolution(resolution_id)
96
- span = resolution[:span]
97
- ptr = @from.dup
98
- while ptr < @to
99
- point = @series.resolution_value_at(ptr, resolution_id)
100
- yield point
101
- ptr += span.to_i
102
- end
103
- rescue FloatDomainError
104
- # OK
105
- end
106
-
107
- def better_resolution
108
- span = @to.to_time - @from.to_time
109
-
110
- resolutions = @series.resolutions.sort_by{|h| h[:span]}.reverse
111
- 5.downto(1) do |div|
112
- res = resolutions.find{|r| r[:span] < span / div }
113
- return res if res
114
- end
115
- return nil
116
- end
117
- end
118
-
119
-
120
- class Redis
121
-
122
- RESOLUTIONS_MAP={
123
- :ever => {span:Float::INFINITY, formatter: "ever", ttl: (2 * 366).days},
124
- :year => {span: 365.days,formatter: "%Y", ttl: (2 * 366).days},
125
- :week => {span: 1.week, formatter: "%Y-CW%w", ttl: 90.days},
126
- :month => {span: 30.days, formatter: "%Y-%m", ttl: 366.days},
127
- :day => {span: 1.day, formatter: "%Y-%m-%d", ttl: 30.days},
128
- :hour => {span: 1.hour, formatter: "%Y-%m-%dT%H", ttl: 24.hours},
129
- :minute => {span: 1.minute, formatter: "%Y-%m-%dT%H:%M", ttl: 120.minutes},
130
- :second => {span: 1, formatter: "%Y-%m-%dT%H:%M:%S", ttl: 1.hour},
131
- :"5seconds" => {span: 5.seconds, formatter: ->(time){ [time.strftime("%Y-%m-%dT%H:%M:") , time.to_i % 60/5, '*',5].join('') }, ttl: 1.hour},
132
- :"5minutes" => {span: 5.minutes, formatter: ->(time){ [time.strftime("%Y-%m-%dT%H:") , (time.to_i/60) % 60/5, '*',5].join('') }, ttl: 3.hour},
133
- :"15minutes" => {span: 15.minutes, formatter: ->(time){ [time.strftime("%Y-%m-%dT%H:") , (time.to_i/60) % 60/15, '*',15].join('') }, ttl: 24.hours}
134
-
135
- }
136
- DEFAULT_RESOLUTIONS = RESOLUTIONS_MAP.keys
137
- NAMESPACE_SEPARATOR = '|'
138
-
139
- attr_reader :logger
140
-
141
- def initialize(logger, redis, options = {})
142
- @resolution_ids = options[:resolutions] || DEFAULT_RESOLUTIONS
143
- @prefix = options[:prefix] || self.class.name
144
- @logger = logger || NilLogger.new
145
- @connection_to_use = redis
146
- end
147
-
148
- def on_connection(conn)
149
- old_connection = @connection_to_use
150
- @connection_to_use = conn
151
- yield self
152
- @connection_to_use = old_connection
153
- end
154
-
155
- def redis_connection_to_use
156
- @connection_to_use || fail("No redis connection given")
157
- end
158
-
159
- def store(subject, event_point_hash, moment = Time.now, descend_subject = true, descend_event = true)
160
- resolutions = resolutions_of(moment)
161
-
162
- descend(subject, descend_subject) do |subject_path|
163
- event_point_hash.each do |event, point_value|
164
- descend(event, descend_event) do |event_path|
165
- resolutions.each do |res|
166
- resolution_name, resolution_value = *res
167
- store_point_value(subject_path, event_path, resolution_name, resolution_value, point_value)
168
- end
169
- end
170
- end
171
- end
172
- end
173
-
174
- def resolutions_of(moment)
175
- @resolution_ids.collect do |res_id|
176
- [res_id, resolution_value_at(moment, res_id)]
177
- end
178
- end
179
-
180
- def resolution_value_at(moment, res_id)
181
- res_config = RESOLUTIONS_MAP[res_id]
182
- if res_config.nil?
183
- fail "No resolution config for id: #{res_id.class}:#{res_id}"
184
- end
185
-
186
- time_resolution_formater = res_config[:formatter]
187
- case time_resolution_formater
188
- when String
189
- moment.strftime(time_resolution_formater)
190
- when Proc
191
- time_resolution_formater.call(moment)
192
- else
193
- fail ArgumentError, "Unexpected moment formater type #{time_resolution_formater.class}"
194
- end
195
- end
196
-
197
- def descend(path, do_descend = true , &block)
198
- return if path.empty? or path == "."
199
- block.call(path)
200
- descend(File.dirname(path), do_descend, &block) if do_descend
201
- end
202
-
203
- def store_point_event( resolution_name, resolution_value, subject_path, event_path)
204
- key = [@prefix, 'event_set', resolution_name, resolution_value, subject_path].join(NAMESPACE_SEPARATOR)
205
- logger.debug("EVENTSET SADD #{key} -> #{event_path}")
206
- redis_connection_to_use.sadd(key, event_path)
207
- end
208
-
209
- def store_point_value( subject_path, event_path, resolution_name, resolution_value, point_value)
210
- store_point_event(resolution_name, resolution_value, subject_path, event_path)
211
-
212
- key = [@prefix, 'point' ,subject_path, event_path, resolution_name, resolution_value].join(NAMESPACE_SEPARATOR)
213
- logger.debug("SETTING KEY #{key}")
214
- redis_connection_to_use.incrby(key, point_value)
215
- redis_connection_to_use.expire(key, RESOLUTIONS_MAP[resolution_name][:ttl])
216
- end
217
-
218
- def find(subject, from, to = Time.now)
219
- Rhcf::Timeseries::Result.new(subject, from, to, self)
220
- end
221
-
222
- def flush!
223
- every_key{|a_key| delete_key(a_key)}
224
- end
225
-
226
- def every_key(pattern=nil, &block)
227
- pattern = [@prefix, pattern,'*'].compact.join(NAMESPACE_SEPARATOR)
228
- redis_connection_to_use.keys(pattern).each do |key|
229
- yield key
230
- end
231
- end
232
-
233
- def delete_key(a_key)
234
- logger.debug("DELETING KEY #{a_key}")
235
- redis_connection_to_use.del(a_key)
236
- end
237
-
238
- def keys(*a_key)
239
- fail "GIVEUP"
240
- a_key = [@prefix, a_key].flatten.join(NAMESPACE_SEPARATOR)
241
- logger.debug("FINDING KEY #{a_key}")
242
- redis_connection_to_use.keys(a_key).collect{|k| k.split(NAMESPACE_SEPARATOR)[1,1000].join(NAMESPACE_SEPARATOR) }
243
- end
244
-
245
- def get(*a_key)
246
- a_key = [@prefix, a_key].flatten.join(NAMESPACE_SEPARATOR)
247
- logger.debug("GETTING KEY #{a_key}")
248
- redis_connection_to_use.get(a_key)
249
- end
250
-
251
-
252
- def resolution(id)
253
- res = RESOLUTIONS_MAP[id]
254
- fail ArgumentError, "Invalid resolution name #{id} for this time series" if res.nil?
255
- res.merge(:id => id)
256
- end
257
-
258
- def resolutions
259
- @_resolutions ||= @resolution_ids.map { |id| resolution(id) }
260
- end
261
-
262
- def events_for_subject_on(subject, point, resolution_id)
263
- key = [@prefix, 'event_set', resolution_id, point, subject].join(NAMESPACE_SEPARATOR)
264
- logger.debug("EVENTSET SMEMBERS #{key}")
265
- redis_connection_to_use.smembers(key)
266
- end
267
- end
268
- end
269
- end