redistat 0.3.0 → 0.4.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.
- 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 [](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
|
|