redistat 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Redistat
1
+ # Redistat #
2
2
 
3
3
  A Redis-backed statistics storage and querying library written in Ruby.
4
4
 
@@ -10,68 +10,197 @@ Redistat was originally created to replace a small hacked together statistics co
10
10
 
11
11
  Redis fits perfectly with all of these requirements. It has atomic operations like increment, and it's lightning fast, meaning if the data is structured well, the initial stats reporting call will store data in a format that's instantly retrievable just as fast.
12
12
 
13
- ## Installation
13
+ ## Installation ##
14
14
 
15
15
  gem install redistat
16
16
 
17
17
  If you are using Ruby 1.8.x, it's recommended you also install the `SystemTimer` gem, as the Redis gem will otherwise complain.
18
18
 
19
- ## Usage
19
+ ## Usage (Crash Course) ##
20
20
 
21
- The simplest way to use Redistat is through the model wrapper.
21
+ view\_stats.rb:
22
22
 
23
- class VisitorStats
23
+ require 'redistat'
24
+
25
+ class ViewStats
24
26
  include Redistat::Model
25
27
  end
26
28
 
27
- Before any of you Rails-purists start complaining about the model name being plural, I want to point out that it makes sense with Redistat, cause a model doesn't exactly return a specific row or object. But I'm getting ahead of myself.
28
29
 
29
- To store statistics we essentially tell Redistat that an event has occurred with a label of X, and statistics of Y. So let's say we want to store a page view event on the `/about` page on a site:
30
+ ### Simple Example ###
30
31
 
31
- VisitorStats.store('/about', {:views => 1})
32
+ Store:
32
33
 
33
- In the above case "`/about`" is the label under which the stats are grouped, and the statistics associated with the event is simply a normal Ruby hash, except all values need to be integers, or Redis' increment calls won't work.
34
+ ViewStats.store('hello', {:world => 4})
35
+ ViewStats.store('hello', {:world => 2}, 2.hours.ago)
34
36
 
35
- To later retrieve statistics, we use the `fetch` method:
37
+ Fetch:
36
38
 
37
- stats = VisitorStats.fetch('/about', 2.hour.ago, Time.now)
38
- # stats => [{:views => 1}]
39
- # stats.total => {:views => 1}
39
+ ViewStats.find('hello', 1.hour.ago, 1.hour.from_now).all
40
+ #=> [{'world' => 4}]
41
+ ViewStats.find('hello', 1.hour.ago, 1.hour.from_now).total
42
+ #=> {'world' => 4}
43
+ ViewStats.find('hello', 3.hour.ago, 1.hour.from_now).total
44
+ #=> {'world' => 6}
40
45
 
41
- The fetch method requires 3 arguments, a label, a start time, and an end time. Fetch returns a `Redistat::Collection` object, which is normal Ruby Array with a couple of added methods, like total shown above.
42
46
 
43
- For more detailed usage, please check spec files, and source code till I have time to write up a complete readme.
47
+ ### Advanced Example ###
44
48
 
49
+ Store page view on product #44 from Chrome 11:
45
50
 
46
- ## Some Technical Details
51
+ ViewStats.store('views/product/44', {'count/chrome/11' => 1})
52
+
53
+ Fetch product #44 stats:
47
54
 
48
- To give a brief look into how Redistat works internally to store statistics, I'm going to use the examples above. The store method accepts a Ruby Hash with statistics to store. Redistat stores all statistics as hashes in Redis. It stores summaries of the stats for the specific time when it happened and all it's parent time groups (second, minute, hour, day, month, year). The default depth Redistat goes to is hour, unless the `depth` option is passed to `store` or `fetch`.
55
+ ViewStats.find('views/product/44', 23.hours.ago, 1.hour.from_now).total
56
+ #=> { 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
49
57
 
50
- In short, the above call to `store` creates the following keys in Redis:
58
+ Store a page view on product #32 from Firefox 3:
51
59
 
52
- VisitorStats//about:2010
53
- VisitorStats//about:201011
54
- VisitorStats//about:20101124
55
- VisitorStats//about:2010112401
60
+ ViewStats.store('views/product/32', {'count/firefox/3' => 1})
56
61
 
57
- Each of these keys in Redis are a hash, containing the sums of each statistic point reported for the time frame the key represents. In this case there's two slashes, cause the label we used was “`/about`”, and the scope (class name when used through the model wrapper) and the label are separated with a slash.
62
+ Fetch product #32 stats:
63
+
64
+ ViewStats.find('views/product/32', 23.hours.ago, 1.hour.from_now).total
65
+ #=> { 'count' => 1, 'count/firefox' => 1, 'count/firefox/3' => 1 }
58
66
 
59
- When retrieving statistics for a given date range, Redistat figures out how to do the least number of calls to Redis to fetch all relevant data. For example, if you want the sum of stats from the 4th till the last of November, the full month of November would first be fetched, then the first 3 days of November would be fetched and removed from the full month stats.
67
+ Fetch stats for all products:
60
68
 
69
+ ViewStats.find('views/product', 23.hours.ago, 1.hour.from_now).total
70
+ #=> { 'count' => 2,
71
+ # 'count/chrome' => 1,
72
+ # 'count/chrome/11' => 1,
73
+ # 'count/firefox' => 1,
74
+ # 'count/firefox/3' => 1 }
61
75
 
62
- ## Todo
76
+ Store a 404 error view:
63
77
 
64
- * Proper/complete readme.
78
+ ViewStats.store('views/error/404', {'count/chrome/9' => 1})
79
+
80
+ Fetch stats for all views across the board:
81
+
82
+ ViewStats.find('views', 23.hours.ago, 1.hour.from_now).total
83
+ #=> { 'count' => 3,
84
+ # 'count/chrome' => 2,
85
+ # 'count/chrome/9' => 1,
86
+ # 'count/chrome/11' => 1,
87
+ # 'count/firefox' => 1,
88
+ # 'count/firefox/3' => 1 }
89
+
90
+ Fetch list of products known to Redistat:
91
+
92
+ finder = ViewStats.find('views/product', 23.hours.ago, 1.hour.from_now)
93
+ finder.children.map { |child| child.label.me }
94
+ #=> [ "32", "44" ]
95
+ finder.children.map { |child| child.label.to_s }
96
+ #=> [ "views/products/32", "views/products/44" ]
97
+ finder.children.map { |child| child.total }
98
+ #=> [ { "count" => 1, "count/firefox" => 1, "count/firefox/3" => 1 },
99
+ # { "count" => 1, "count/chrome" => 1, "count/chrome/11" => 1 } ]
100
+
101
+
102
+ ## Terminology ##
103
+
104
+ ### Scope ###
105
+
106
+ A type of global-namespace for storing data. When using the `Redistat::Model` wrapper, the scope is automatically set to the class name. In the examples above, the scope is `ViewStats`. Can be overridden by calling the `#scope` class method on your model class.
107
+
108
+ ### Label ###
109
+
110
+ Identifier string to separate different types and groups of statistics from each other. The first argument of the `#store`, `#find`, and `#fetch` methods is the label that you're storing to, or fetching from.
111
+
112
+ Labels support multiple grouping levels by splitting the label string with `/` and storing the same stats for each level. For example, when storing data to a label called `views/product/44`, the data is stored for the label you specify, and also for `views/product` and `views`.
113
+
114
+ A word of caution: Don't use a crazy number of group levels. As two levels causes twice as many `hincrby` calls to Redis as not using the grouping feature. Hence using 10 grouping levels, causes 10 times as many write calls to Redis.
115
+
116
+ ### Input Statistics Data ###
117
+
118
+ You provide Redistat with the data you want to store using a Ruby Hash. This data is then stored in a corresponding Redis hash with identical key/field names.
119
+
120
+ Key names in the hash also support grouping features similar to those available for Labels. Again, the more levels you use, the more write calls to Redis, so avoid using 10-15 levels.
121
+
122
+ ### Depth (Storage Accuracy) ###
123
+
124
+ Define how accurately data should be stored, and how accurately it's looked up when fetching it again. By default Redistat uses a depth value of `:hour`, which means it's impossible to separate two events which were stored at 10:18 and 10:23. In Redis they are both stored within a date key of `2011031610`.
125
+
126
+ You can set depth within your model using the `#depth` class method. Available depths are: `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec`
127
+
128
+ ### Time Ranges ###
129
+
130
+ When you fetch data, you need to specify a start and an end time. The selection behavior can seem a bit weird at first when, but makes sense when you understand how Redistat works internally.
131
+
132
+ For example, if we are using a Depth value of `:hour`, and we trigger a fetch call starting at `1.hour.ago` (13:34), till `Time.now` (14:34), only stats from 13:00:00 till 13:59:59 are returned, as they were all stored within the key for the 13th hour. If both 13:00 and 14:00 was returned, you would get results from two hole hours. Hence if you want up to the second data, use an end time of `1.hour.from_now`.
133
+
134
+ ### The Finder Object ###
135
+
136
+ Calling the `#find` method on a Redistat model class returns a `Redistat::Finder` object. The finder is a lazy-loaded gateway to your data. Meaning you can create a new finder, and modify instantiated finder's label, scope, dates, and more. It does not call Redis and fetch the data until you call `#total`, `#all`, `#map`, `#each`, or `#each_with_index` on the finder.
137
+
138
+ This section does need further expanding as there's a lot to cover when it comes to the finder.
139
+
140
+
141
+
142
+ ## Internals ##
143
+
144
+ ### Storing / Writing ###
145
+
146
+ Redistat stores all data into a Redis hash keys. The Redis key name the used consists of three parts. The scope, label, and datetime:
147
+
148
+ {scope}/{label}:{datetime}
149
+
150
+ For example, this...
151
+
152
+ ViewStats.store('views/product/44', {'count/chrome/11' => 1})
153
+
154
+ ...would store the follow hash of data...
155
+
156
+ { 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
157
+
158
+ ...to all 12 of these Redis hash keys...
159
+
160
+ ViewStats/views:2011
161
+ ViewStats/views:201103
162
+ ViewStats/views:20110315
163
+ ViewStats/views:2011031510
164
+ ViewStats/views/product:2011
165
+ ViewStats/views/product:201103
166
+ ViewStats/views/product:20110315
167
+ ViewStats/views/product:2011031510
168
+ ViewStats/views/product/44:2011
169
+ ViewStats/views/product/44:201103
170
+ ViewStats/views/product/44:20110315
171
+ ViewStats/views/product/44:2011031510
172
+
173
+ ...by creating the Redis key, and/or hash field if needed, otherwise it simply increments the already existing data.
174
+
175
+ It would also create the following Redis sets to keep track of which child labels are available:
176
+
177
+ ViewStats.label_index:
178
+ ViewStats.label_index:views
179
+ ViewStats.label_index:views/product
180
+
181
+ It should now be more obvious to you why you should think about how you use the grouping capabilities so you don't go crazy and use 10-15 levels. Storing is done through Redis' `hincrby` call, which only supports a single key/field combo. Meaning the above example would call `hincrby` a total of 36 times to store the data, and `sadd` a total of 3 times to ensure the label index is accurate. 39 calls is however not a problem for Redis, most calls happen in less than 0.15ms (0.00015 seconds) on my local machine.
182
+
183
+
184
+ ### Fetching / Reading ###
185
+
186
+ By default when fetching statistics, Redistat will figure out how to do the least number of reads from Redis. First it checks how long range you're fetching. If whole days, months or years for example fit within the start and end dates specified, it will fetch the one key for the day/month/year in question. It further drills down to the smaller units.
187
+
188
+ It is also intelligent enough to not fetch each day from 3-31 of a month, instead it would fetch the data for the whole month and the first two days, which are then removed from the summary of the whole month. This means three calls to `hgetall` instead of 29 if each whole day was fetched.
189
+
190
+
191
+ ## Todo ##
192
+
193
+ * More details in Readme.
65
194
  * Documentation.
66
195
  * Anything else that becomes apparent after real-world use.
67
196
 
68
197
 
69
- ## Credits
198
+ ## Credits ##
70
199
 
71
200
  [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.
72
201
 
73
202
 
74
- ## Note on Patches/Pull Requests
203
+ ## Note on Patches/Pull Requests ##
75
204
 
76
205
  * Fork the project.
77
206
  * Make your feature addition or bug fix.
@@ -82,7 +211,7 @@ When retrieving statistics for a given date range, Redistat figures out how to d
82
211
  * Send me a pull request. Bonus points for topic branches.
83
212
 
84
213
 
85
- ## License and Copyright
214
+ ## License and Copyright ##
86
215
 
87
216
  Copyright (c) 2011 Jim Myhrberg.
88
217
 
@@ -39,12 +39,12 @@ module Redistat
39
39
  #
40
40
 
41
41
  option_accessor :depth
42
- option_accessor :class_name
42
+ option_accessor :scope
43
43
  option_accessor :store_event
44
44
  option_accessor :hashed_label
45
45
  option_accessor :label_indexing
46
46
 
47
- alias :scope :class_name
47
+ alias :class_name :scope
48
48
 
49
49
  def connect_to(opts = {})
50
50
  Connection.create(opts.merge(:ref => name))
@@ -62,7 +62,7 @@ module Redistat
62
62
  alias :redis :connection
63
63
 
64
64
  def name
65
- options[:class_name] || (@name ||= self.to_s)
65
+ options[:scope] || (@name ||= self.to_s)
66
66
  end
67
67
 
68
68
  end
@@ -1,3 +1,3 @@
1
1
  module Redistat
2
- VERSION = "0.2.4"
2
+ VERSION = "0.2.5"
3
3
  end
@@ -25,6 +25,6 @@ end
25
25
  class ModelHelper4
26
26
  include Redistat::Model
27
27
 
28
- class_name "FancyHelper"
28
+ scope "FancyHelper"
29
29
 
30
30
  end
@@ -37,7 +37,7 @@ describe Redistat::Model do
37
37
  ModelHelper2.depth.should == :day
38
38
  ModelHelper2.store_event.should == true
39
39
  ModelHelper2.hashed_label.should == true
40
- ModelHelper2.class_name.should be_nil
40
+ ModelHelper2.scope.should be_nil
41
41
 
42
42
  ModelHelper1.depth.should == nil
43
43
  ModelHelper1.store_event.should == nil
@@ -55,7 +55,7 @@ describe Redistat::Model do
55
55
  ModelHelper1.store_event.should == nil
56
56
  ModelHelper1.hashed_label.should == nil
57
57
 
58
- ModelHelper4.class_name.should == "FancyHelper"
58
+ ModelHelper4.scope.should == "FancyHelper"
59
59
  ModelHelper4.send(:name).should == "FancyHelper"
60
60
  end
61
61
 
metadata CHANGED
@@ -1,13 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redistat
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
5
4
  prerelease:
6
- segments:
7
- - 0
8
- - 2
9
- - 4
10
- version: 0.2.4
5
+ version: 0.2.5
11
6
  platform: ruby
12
7
  authors:
13
8
  - Jim Myhrberg
@@ -15,7 +10,7 @@ autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
12
 
18
- date: 2011-03-14 00:00:00 +00:00
13
+ date: 2011-03-16 00:00:00 +00:00
19
14
  default_executable:
20
15
  dependencies:
21
16
  - !ruby/object:Gem::Dependency
@@ -26,11 +21,6 @@ dependencies:
26
21
  requirements:
27
22
  - - ">="
28
23
  - !ruby/object:Gem::Version
29
- hash: 15
30
- segments:
31
- - 2
32
- - 3
33
- - 6
34
24
  version: 2.3.6
35
25
  type: :runtime
36
26
  version_requirements: *id001
@@ -42,11 +32,6 @@ dependencies:
42
32
  requirements:
43
33
  - - ">="
44
34
  - !ruby/object:Gem::Version
45
- hash: 7
46
- segments:
47
- - 1
48
- - 4
49
- - 0
50
35
  version: 1.4.0
51
36
  type: :runtime
52
37
  version_requirements: *id002
@@ -58,11 +43,6 @@ dependencies:
58
43
  requirements:
59
44
  - - ">="
60
45
  - !ruby/object:Gem::Version
61
- hash: 11
62
- segments:
63
- - 2
64
- - 1
65
- - 0
66
46
  version: 2.1.0
67
47
  type: :runtime
68
48
  version_requirements: *id003
@@ -74,11 +54,6 @@ dependencies:
74
54
  requirements:
75
55
  - - ">="
76
56
  - !ruby/object:Gem::Version
77
- hash: 5
78
- segments:
79
- - 0
80
- - 2
81
- - 9
82
57
  version: 0.2.9
83
58
  type: :runtime
84
59
  version_requirements: *id004
@@ -90,11 +65,6 @@ dependencies:
90
65
  requirements:
91
66
  - - ">="
92
67
  - !ruby/object:Gem::Version
93
- hash: 11
94
- segments:
95
- - 2
96
- - 1
97
- - 0
98
68
  version: 2.1.0
99
69
  type: :development
100
70
  version_requirements: *id005
@@ -106,11 +76,6 @@ dependencies:
106
76
  requirements:
107
77
  - - ">="
108
78
  - !ruby/object:Gem::Version
109
- hash: 41
110
- segments:
111
- - 0
112
- - 9
113
- - 9
114
79
  version: 0.9.9
115
80
  type: :development
116
81
  version_requirements: *id006
@@ -122,11 +87,6 @@ dependencies:
122
87
  requirements:
123
88
  - - ">="
124
89
  - !ruby/object:Gem::Version
125
- hash: 1
126
- segments:
127
- - 0
128
- - 6
129
- - 3
130
90
  version: 0.6.3
131
91
  type: :development
132
92
  version_requirements: *id007
@@ -206,23 +166,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
206
166
  requirements:
207
167
  - - ">="
208
168
  - !ruby/object:Gem::Version
209
- hash: 3
210
- segments:
211
- - 0
212
169
  version: "0"
213
170
  required_rubygems_version: !ruby/object:Gem::Requirement
214
171
  none: false
215
172
  requirements:
216
173
  - - ">="
217
174
  - !ruby/object:Gem::Version
218
- hash: 3
219
- segments:
220
- - 0
221
175
  version: "0"
222
176
  requirements: []
223
177
 
224
178
  rubyforge_project: redistat
225
- rubygems_version: 1.5.0
179
+ rubygems_version: 1.6.1
226
180
  signing_key:
227
181
  specification_version: 3
228
182
  summary: A Redis-backed statistics storage and querying library written in Ruby.