redistat 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +8 -0
- data/README.md +219 -97
- data/lib/redistat.rb +13 -13
- data/lib/redistat/buffer.rb +27 -24
- data/lib/redistat/collection.rb +5 -5
- data/lib/redistat/connection.rb +23 -18
- data/lib/redistat/core_ext.rb +1 -1
- data/lib/redistat/core_ext/bignum.rb +2 -2
- data/lib/redistat/core_ext/date.rb +2 -2
- data/lib/redistat/core_ext/fixnum.rb +2 -2
- data/lib/redistat/core_ext/hash.rb +4 -4
- data/lib/redistat/date.rb +11 -11
- data/lib/redistat/event.rb +18 -18
- data/lib/redistat/finder.rb +39 -39
- data/lib/redistat/finder/date_set.rb +4 -4
- data/lib/redistat/key.rb +16 -16
- data/lib/redistat/label.rb +14 -14
- data/lib/redistat/mixins/database.rb +1 -1
- data/lib/redistat/mixins/date_helper.rb +1 -1
- data/lib/redistat/mixins/options.rb +8 -8
- data/lib/redistat/mixins/synchronize.rb +12 -11
- data/lib/redistat/model.rb +25 -17
- data/lib/redistat/result.rb +4 -4
- data/lib/redistat/scope.rb +5 -5
- data/lib/redistat/summary.rb +33 -26
- data/lib/redistat/version.rb +1 -1
- data/redistat.gemspec +4 -3
- data/spec/buffer_spec.rb +27 -25
- data/spec/collection_spec.rb +4 -4
- data/spec/connection_spec.rb +12 -12
- data/spec/core_ext/hash_spec.rb +5 -5
- data/spec/database_spec.rb +3 -3
- data/spec/date_spec.rb +15 -15
- data/spec/event_spec.rb +8 -8
- data/spec/finder/date_set_spec.rb +134 -134
- data/spec/finder_spec.rb +36 -36
- data/spec/key_spec.rb +19 -19
- data/spec/label_spec.rb +10 -10
- data/spec/model_helper.rb +10 -9
- data/spec/model_spec.rb +38 -41
- data/spec/options_spec.rb +9 -9
- data/spec/result_spec.rb +4 -4
- data/spec/scope_spec.rb +6 -6
- data/spec/spec_helper.rb +6 -0
- data/spec/summary_spec.rb +31 -24
- data/spec/synchronize_spec.rb +118 -57
- data/spec/thread_safety_spec.rb +6 -6
- metadata +88 -126
- data/.rvmrc +0 -1
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,164 +1,259 @@
|
|
1
|
-
# Redistat
|
1
|
+
# Redistat [![Build Status](https://secure.travis-ci.org/jimeh/redistat.png)](http://travis-ci.org/jimeh/redistat)
|
2
2
|
|
3
3
|
A Redis-backed statistics storage and querying library written in Ruby.
|
4
4
|
|
5
|
-
Redistat was originally created to replace a small hacked together statistics
|
5
|
+
Redistat was originally created to replace a small hacked together statistics
|
6
|
+
collection solution which was MySQL-based. When I started I had a short list
|
7
|
+
of requirements:
|
6
8
|
|
7
9
|
* Store and increment/decrement integer values (counters, etc)
|
8
10
|
* Up to the second statistics available at all times
|
9
11
|
* Screamingly fast
|
10
12
|
|
11
|
-
Redis fits perfectly with all of these requirements. It has atomic operations
|
13
|
+
Redis fits perfectly with all of these requirements. It has atomic operations
|
14
|
+
like increment, and it's lightning fast, meaning if the data is structured
|
15
|
+
well, the initial stats reporting call will store data in a format that's
|
16
|
+
instantly retrievable just as fast.
|
12
17
|
|
13
|
-
## Installation
|
18
|
+
## Installation
|
14
19
|
|
15
20
|
gem install redistat
|
16
21
|
|
17
|
-
If you are using Ruby 1.8.x, it's recommended you also install the
|
22
|
+
If you are using Ruby 1.8.x, it's recommended you also install the
|
23
|
+
`SystemTimer` gem, as the Redis gem will otherwise complain.
|
18
24
|
|
19
|
-
## Usage (Crash Course)
|
25
|
+
## Usage (Crash Course)
|
20
26
|
|
21
27
|
view\_stats.rb:
|
22
28
|
|
23
|
-
|
24
|
-
|
25
|
-
class ViewStats
|
26
|
-
include Redistat::Model
|
27
|
-
end
|
28
|
-
|
29
|
-
# if using Redistat in multiple threads set this
|
30
|
-
# somewhere in the beginning of the execution stack
|
31
|
-
Redistat.thread_safe = true
|
29
|
+
```ruby
|
30
|
+
require 'redistat'
|
32
31
|
|
32
|
+
class ViewStats
|
33
|
+
include Redistat::Model
|
34
|
+
end
|
33
35
|
|
34
|
-
|
36
|
+
# if using Redistat in multiple threads set this
|
37
|
+
# somewhere in the beginning of the execution stack
|
38
|
+
Redistat.thread_safe = true
|
39
|
+
```
|
40
|
+
|
41
|
+
|
42
|
+
### Simple Example
|
35
43
|
|
36
44
|
Store:
|
37
45
|
|
38
|
-
|
39
|
-
|
46
|
+
```ruby
|
47
|
+
ViewStats.store('hello', {:world => 4})
|
48
|
+
ViewStats.store('hello', {:world => 2}, 2.hours.ago)
|
49
|
+
```
|
40
50
|
|
41
51
|
Fetch:
|
42
52
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
53
|
+
```ruby
|
54
|
+
ViewStats.find('hello', 1.hour.ago, 1.hour.from_now).all
|
55
|
+
#=> [{'world' => 4}]
|
56
|
+
ViewStats.find('hello', 1.hour.ago, 1.hour.from_now).total
|
57
|
+
#=> {'world' => 4}
|
58
|
+
ViewStats.find('hello', 3.hour.ago, 1.hour.from_now).total
|
59
|
+
#=> {'world' => 6}
|
60
|
+
```
|
49
61
|
|
50
62
|
|
51
|
-
### Advanced Example
|
63
|
+
### Advanced Example
|
52
64
|
|
53
65
|
Store page view on product #44 from Chrome 11:
|
54
66
|
|
55
|
-
|
56
|
-
|
67
|
+
```ruby
|
68
|
+
ViewStats.store('views/product/44', {'count/chrome/11' => 1})
|
69
|
+
```
|
70
|
+
|
57
71
|
Fetch product #44 stats:
|
58
72
|
|
59
|
-
|
60
|
-
|
73
|
+
```ruby
|
74
|
+
ViewStats.find('views/product/44', 23.hours.ago, 1.hour.from_now).total
|
75
|
+
#=> { 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
|
76
|
+
```
|
61
77
|
|
62
78
|
Store a page view on product #32 from Firefox 3:
|
63
79
|
|
64
|
-
|
80
|
+
```ruby
|
81
|
+
ViewStats.store('views/product/32', {'count/firefox/3' => 1})
|
82
|
+
```
|
65
83
|
|
66
84
|
Fetch product #32 stats:
|
67
|
-
|
68
|
-
|
69
|
-
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
ViewStats.find('views/product/32', 23.hours.ago, 1.hour.from_now).total
|
88
|
+
#=> { 'count' => 1, 'count/firefox' => 1, 'count/firefox/3' => 1 }
|
89
|
+
```
|
70
90
|
|
71
91
|
Fetch stats for all products:
|
72
92
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
93
|
+
```ruby
|
94
|
+
ViewStats.find('views/product', 23.hours.ago, 1.hour.from_now).total
|
95
|
+
#=> { 'count' => 2,
|
96
|
+
# 'count/chrome' => 1,
|
97
|
+
# 'count/chrome/11' => 1,
|
98
|
+
# 'count/firefox' => 1,
|
99
|
+
# 'count/firefox/3' => 1 }
|
100
|
+
```
|
79
101
|
|
80
102
|
Store a 404 error view:
|
81
103
|
|
82
|
-
|
104
|
+
```ruby
|
105
|
+
ViewStats.store('views/error/404', {'count/chrome/9' => 1})
|
106
|
+
```
|
83
107
|
|
84
108
|
Fetch stats for all views across the board:
|
85
109
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
110
|
+
```ruby
|
111
|
+
ViewStats.find('views', 23.hours.ago, 1.hour.from_now).total
|
112
|
+
#=> { 'count' => 3,
|
113
|
+
# 'count/chrome' => 2,
|
114
|
+
# 'count/chrome/9' => 1,
|
115
|
+
# 'count/chrome/11' => 1,
|
116
|
+
# 'count/firefox' => 1,
|
117
|
+
# 'count/firefox/3' => 1 }
|
118
|
+
```
|
93
119
|
|
94
120
|
Fetch list of products known to Redistat:
|
95
121
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
122
|
+
```ruby
|
123
|
+
finder = ViewStats.find('views/product', 23.hours.ago, 1.hour.from_now)
|
124
|
+
finder.children.map { |child| child.label.me }
|
125
|
+
#=> [ "32", "44" ]
|
126
|
+
finder.children.map { |child| child.label.to_s }
|
127
|
+
#=> [ "views/products/32", "views/products/44" ]
|
128
|
+
finder.children.map { |child| child.total }
|
129
|
+
#=> [ { "count" => 1, "count/firefox" => 1, "count/firefox/3" => 1 },
|
130
|
+
# { "count" => 1, "count/chrome" => 1, "count/chrome/11" => 1 } ]
|
131
|
+
```
|
132
|
+
|
133
|
+
|
134
|
+
## Terminology
|
135
|
+
|
136
|
+
### Scope
|
104
137
|
|
138
|
+
A type of global-namespace for storing data. When using the `Redistat::Model`
|
139
|
+
wrapper, the scope is automatically set to the class name. In the examples
|
140
|
+
above, the scope is `ViewStats`. Can be overridden by calling the `#scope`
|
141
|
+
class method on your model class.
|
105
142
|
|
106
|
-
|
143
|
+
### Label
|
107
144
|
|
108
|
-
|
145
|
+
Identifier string to separate different types and groups of statistics from
|
146
|
+
each other. The first argument of the `#store`, `#find`, and `#fetch` methods
|
147
|
+
is the label that you're storing to, or fetching from.
|
109
148
|
|
110
|
-
|
149
|
+
Labels support multiple grouping levels by splitting the label string with `/`
|
150
|
+
and storing the same stats for each level. For example, when storing data to a
|
151
|
+
label called `views/product/44`, the data is stored for the label you specify,
|
152
|
+
and also for `views/product` and `views`.
|
111
153
|
|
112
|
-
|
154
|
+
A word of caution: Don't use a crazy number of group levels. As two levels
|
155
|
+
causes twice as many `hincrby` calls to Redis as not using the grouping
|
156
|
+
feature. Hence using 10 grouping levels, causes 10 times as many write calls
|
157
|
+
to Redis.
|
113
158
|
|
114
|
-
|
159
|
+
### Input Statistics Data
|
115
160
|
|
116
|
-
|
161
|
+
You provide Redistat with the data you want to store using a Ruby Hash. This
|
162
|
+
data is then stored in a corresponding Redis hash with identical key/field
|
163
|
+
names.
|
117
164
|
|
118
|
-
|
165
|
+
Key names in the hash also support grouping features similar to those
|
166
|
+
available for Labels. Again, the more levels you use, the more write calls to
|
167
|
+
Redis, so avoid using 10-15 levels.
|
119
168
|
|
120
|
-
###
|
169
|
+
### Depth (Storage Accuracy)
|
121
170
|
|
122
|
-
|
171
|
+
Define how accurately data should be stored, and how accurately it's looked up
|
172
|
+
when fetching it again. By default Redistat uses a depth value of `:hour`,
|
173
|
+
which means it's impossible to separate two events which were stored at 10:18
|
174
|
+
and 10:23. In Redis they are both stored within a date key of `2011031610`.
|
123
175
|
|
124
|
-
|
176
|
+
You can set depth within your model using the `#depth` class method. Available
|
177
|
+
depths are: `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec`
|
125
178
|
|
126
|
-
###
|
179
|
+
### Time Ranges
|
127
180
|
|
128
|
-
|
181
|
+
When you fetch data, you need to specify a start and an end time. The
|
182
|
+
selection behavior can seem a bit weird at first when, but makes sense when
|
183
|
+
you understand how Redistat works internally.
|
129
184
|
|
130
|
-
|
185
|
+
For example, if we are using a Depth value of `:hour`, and we trigger a fetch
|
186
|
+
call starting at `1.hour.ago` (13:34), till `Time.now` (14:34), only stats
|
187
|
+
from 13:00:00 till 13:59:59 are returned, as they were all stored within the
|
188
|
+
key for the 13th hour. If both 13:00 and 14:00 was returned, you would get
|
189
|
+
results from two whole hours. Hence if you want up to the second data, use an
|
190
|
+
end time of `1.hour.from_now`.
|
131
191
|
|
132
|
-
###
|
192
|
+
### The Finder Object
|
133
193
|
|
134
|
-
|
194
|
+
Calling the `#find` method on a Redistat model class returns a
|
195
|
+
`Redistat::Finder` object. The finder is a lazy-loaded gateway to your
|
196
|
+
data. Meaning you can create a new finder, and modify instantiated finder's
|
197
|
+
label, scope, dates, and more. It does not call Redis and fetch the data until
|
198
|
+
you call `#total`, `#all`, `#map`, `#each`, or `#each_with_index` on the
|
199
|
+
finder.
|
135
200
|
|
136
|
-
|
201
|
+
This section does need further expanding as there's a lot to cover when it
|
202
|
+
comes to the finder.
|
137
203
|
|
138
|
-
### The Finder Object ###
|
139
204
|
|
140
|
-
|
205
|
+
## Key Expiry
|
141
206
|
|
142
|
-
|
207
|
+
Support for expiring keys from Redis is available, allowing you too keep
|
208
|
+
varying levels of details for X period of time. This allows you easily keep
|
209
|
+
things nice and tidy by only storing varying levels detailed stats only for as
|
210
|
+
long as you need.
|
143
211
|
|
212
|
+
In the below example we define how long Redis keys for varying depths are
|
213
|
+
stored. Second by second stats are available for 10 minutes, minute by minute
|
214
|
+
stats for 6 hours, hourly stats for 3 months, daily stats for 2 years, and
|
215
|
+
yearly stats are retained forever.
|
144
216
|
|
217
|
+
```ruby
|
218
|
+
class ViewStats
|
219
|
+
include Redistat::Model
|
145
220
|
|
146
|
-
|
221
|
+
depth :sec
|
147
222
|
|
148
|
-
|
223
|
+
expire \
|
224
|
+
:sec => 10.minutes.to_i,
|
225
|
+
:min => 6.hours.to_i,
|
226
|
+
:hour => 3.months.to_i,
|
227
|
+
:day => 2.years.to_i
|
228
|
+
end
|
229
|
+
```
|
149
230
|
|
150
|
-
|
231
|
+
Keep in mind that when storing stats for a custom date in the past for
|
232
|
+
example, the expiry time for the keys will be relative to now. The values you
|
233
|
+
specify are simply passed to the `Redis#expire` method.
|
234
|
+
|
235
|
+
|
236
|
+
## Internals
|
237
|
+
|
238
|
+
### Storing / Writing
|
239
|
+
|
240
|
+
Redistat stores all data into a Redis hash keys. The Redis key name the used
|
241
|
+
consists of three parts. The scope, label, and datetime:
|
151
242
|
|
152
243
|
{scope}/{label}:{datetime}
|
153
244
|
|
154
245
|
For example, this...
|
155
246
|
|
156
|
-
|
247
|
+
```ruby
|
248
|
+
ViewStats.store('views/product/44', {'count/chrome/11' => 1})
|
249
|
+
```
|
157
250
|
|
158
251
|
...would store the follow hash of data...
|
159
252
|
|
160
|
-
|
161
|
-
|
253
|
+
```ruby
|
254
|
+
{ 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
|
255
|
+
```
|
256
|
+
|
162
257
|
...to all 12 of these Redis hash keys...
|
163
258
|
|
164
259
|
ViewStats/views:2011
|
@@ -174,54 +269,81 @@ For example, this...
|
|
174
269
|
ViewStats/views/product/44:20110315
|
175
270
|
ViewStats/views/product/44:2011031510
|
176
271
|
|
177
|
-
...by creating the Redis key, and/or hash field if needed, otherwise it simply
|
272
|
+
...by creating the Redis key, and/or hash field if needed, otherwise it simply
|
273
|
+
increments the already existing data.
|
178
274
|
|
179
|
-
It would also create the following Redis sets to keep track of which child
|
275
|
+
It would also create the following Redis sets to keep track of which child
|
276
|
+
labels are available:
|
180
277
|
|
181
278
|
ViewStats.label_index:
|
182
279
|
ViewStats.label_index:views
|
183
280
|
ViewStats.label_index:views/product
|
184
281
|
|
185
|
-
It should now be more obvious to you why you should think about how you use
|
282
|
+
It should now be more obvious to you why you should think about how you use
|
283
|
+
the grouping capabilities so you don't go crazy and use 10-15 levels. Storing
|
284
|
+
is done through Redis' `hincrby` call, which only supports a single key/field
|
285
|
+
combo. Meaning the above example would call `hincrby` a total of 36 times to
|
286
|
+
store the data, and `sadd` a total of 3 times to ensure the label index is
|
287
|
+
accurate. 39 calls is however not a problem for Redis, most calls happen in
|
288
|
+
less than 0.15ms (0.00015 seconds) on my local machine.
|
186
289
|
|
187
290
|
|
188
|
-
### Fetching / Reading
|
291
|
+
### Fetching / Reading
|
189
292
|
|
190
|
-
By default when fetching statistics, Redistat will figure out how to do the
|
293
|
+
By default when fetching statistics, Redistat will figure out how to do the
|
294
|
+
least number of reads from Redis. First it checks how long range you're
|
295
|
+
fetching. If whole days, months or years for example fit within the start and
|
296
|
+
end dates specified, it will fetch the one key for the day/month/year in
|
297
|
+
question. It further drills down to the smaller units.
|
191
298
|
|
192
|
-
It is also intelligent enough to not fetch each day from 3-31 of a month,
|
299
|
+
It is also intelligent enough to not fetch each day from 3-31 of a month,
|
300
|
+
instead it would fetch the data for the whole month and the first two days,
|
301
|
+
which are then removed from the summary of the whole month. This means three
|
302
|
+
calls to `hgetall` instead of 29 if each whole day was fetched.
|
193
303
|
|
194
|
-
### Buffer
|
304
|
+
### Buffer
|
195
305
|
|
196
|
-
The buffer is a new, still semi-beta, feature aimed to reduce the number of
|
306
|
+
The buffer is a new, still semi-beta, feature aimed to reduce the number of
|
307
|
+
Redis `hincrby` that Redistat sends. This should only really be useful when
|
308
|
+
you're hitting north of 30,000 Redis requests per second, if your Redis server
|
309
|
+
has limited resources, or against my recommendation you've opted to use 10,
|
310
|
+
20, or more label grouping levels.
|
197
311
|
|
198
|
-
Buffering tries to fold together multiple `store` calls into as few as
|
312
|
+
Buffering tries to fold together multiple `store` calls into as few as
|
313
|
+
possible by merging the statistics hashes from all calls and groups them based
|
314
|
+
on scope, label, date depth, and more. You configure the the buffer by setting
|
315
|
+
`Redistat.buffer_size` to an integer higher than 1. This basically tells
|
316
|
+
Redistat how many `store` calls to buffer in memory before writing all data to
|
317
|
+
Redis.
|
199
318
|
|
200
319
|
|
201
|
-
## Todo
|
320
|
+
## Todo
|
202
321
|
|
203
322
|
* More details in Readme.
|
204
323
|
* Documentation.
|
205
324
|
* Anything else that becomes apparent after real-world use.
|
206
325
|
|
207
326
|
|
208
|
-
## Credits
|
327
|
+
## Credits
|
328
|
+
|
329
|
+
[Global Personals](http://globalpersonals.co.uk/) deserves a thank
|
330
|
+
you. Currently the primary user of Redistat, they've allowed me to spend some
|
331
|
+
company time to further develop the project.
|
209
332
|
|
210
|
-
[Global Personals](http://globalpersonals.co.uk/) deserves a thank you. Currently the primary user of Redistat, they've allowed me to spend some company time to further develop the project.
|
211
333
|
|
334
|
+
## Note on Patches/Pull Requests
|
212
335
|
|
213
|
-
## Note on Patches/Pull Requests ##
|
214
|
-
|
215
336
|
* Fork the project.
|
216
337
|
* Make your feature addition or bug fix.
|
217
338
|
* Add tests for it. This is important so I don't break it in a
|
218
339
|
future version unintentionally.
|
219
|
-
* Commit, do not mess with rakefile, version, or history.
|
220
|
-
|
340
|
+
* Commit, do not mess with rakefile, version, or history. (if you want to
|
341
|
+
have your own version, that is fine but bump version in a commit by itself I
|
342
|
+
can ignore when I pull)
|
221
343
|
* Send me a pull request. Bonus points for topic branches.
|
222
344
|
|
223
345
|
|
224
|
-
## License and Copyright
|
346
|
+
## License and Copyright
|
225
347
|
|
226
348
|
Copyright (c) 2011 Jim Myhrberg.
|
227
349
|
|
data/lib/redistat.rb
CHANGED
@@ -39,58 +39,58 @@ require 'redistat/core_ext'
|
|
39
39
|
|
40
40
|
|
41
41
|
module Redistat
|
42
|
-
|
42
|
+
|
43
43
|
KEY_NEXT_ID = ".next_id"
|
44
44
|
KEY_EVENT = ".event:"
|
45
45
|
KEY_LABELS = "Redistat.labels:" # used for reverse label hash lookup
|
46
46
|
KEY_EVENT_IDS = ".event_ids"
|
47
47
|
LABEL_INDEX = ".label_index:"
|
48
48
|
GROUP_SEPARATOR = "/"
|
49
|
-
|
49
|
+
|
50
50
|
class InvalidOptions < ArgumentError; end
|
51
51
|
class RedisServerIsTooOld < Exception; end
|
52
|
-
|
52
|
+
|
53
53
|
class << self
|
54
|
-
|
54
|
+
|
55
55
|
def buffer
|
56
56
|
Buffer.instance
|
57
57
|
end
|
58
|
-
|
58
|
+
|
59
59
|
def buffer_size
|
60
60
|
buffer.size
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
def buffer_size=(size)
|
64
64
|
buffer.size = size
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
def thread_safe
|
68
68
|
Synchronize.thread_safe
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def thread_safe=(value)
|
72
72
|
Synchronize.thread_safe = value
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
def connection(ref = nil)
|
76
76
|
Connection.get(ref)
|
77
77
|
end
|
78
78
|
alias :redis :connection
|
79
|
-
|
79
|
+
|
80
80
|
def connection=(connection)
|
81
81
|
Connection.add(connection)
|
82
82
|
end
|
83
83
|
alias :redis= :connection=
|
84
|
-
|
84
|
+
|
85
85
|
def connect(options)
|
86
86
|
Connection.create(options)
|
87
87
|
end
|
88
|
-
|
88
|
+
|
89
89
|
def flush
|
90
90
|
puts "WARNING: Redistat.flush is deprecated. Use Redistat.redis.flushdb instead."
|
91
91
|
connection.flushdb
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|