rhcf-timeseries 0.0.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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