34 Commits

Author SHA1 Message Date
00e0015ac2 Merge branch 'release/v0.2.5' 2011-03-16 00:58:43 +00:00
7e82246662 started release v0.2.5 2011-03-16 00:58:31 +00:00
84f3bf26b5 updated readme 2011-03-16 00:57:18 +00:00
2e2d3273cc set #scope rather than #class_name as the preferred method for setting the scope value within a Redistat::Model 2011-03-16 00:56:59 +00:00
a983e554c6 Merge branch 'release/v0.2.4' into dev 2011-03-14 11:14:06 +00:00
ecbd15ef1e Merge branch 'release/v0.2.4' 2011-03-14 11:14:01 +00:00
2b2461dd9f started release v0.2.4 2011-03-14 11:13:50 +00:00
3e177c9ae4 changed json dependency back to '>= 1.4.0' for backwards compatibility with certain older projects/gems, make sure you're using 1.5.0 or later of the json gem for JRuby support 2011-03-14 11:13:06 +00:00
d4cd5402bc create Label#join method for easily joining Labels 2011-03-14 11:10:24 +00:00
d560a7deff killed an old line of commented out code 2011-03-14 10:38:16 +00:00
3df6666704 additions to specs 2011-03-14 10:37:56 +00:00
80fd63059b updated time_ext dependency to ensure there's no loading issues within Rails applications 2011-03-13 23:47:02 +00:00
d0b7f097a8 updated json dependency to '>= 1.5.0' to ensure JRuby compatability 2011-03-13 23:46:00 +00:00
875d16b01c Merge branch 'release/v0.2.3' into dev 2011-03-13 20:30:21 +00:00
00791d36d8 Merge branch 'release/v0.2.3' 2011-03-13 20:30:17 +00:00
94b589c5e6 started release v0.2.3 2011-03-13 20:30:11 +00:00
cdf52869d6 added #find_event method to Model 2011-03-13 20:28:17 +00:00
8b711d4d9c fixed a bug with Event#find 2011-03-13 20:24:06 +00:00
e4aaedfe58 made Key#scope return Scope object instead of Scope#to_s 2011-03-13 20:23:41 +00:00
ea820d44f4 fixed typo in Finder spec 2011-03-13 19:52:30 +00:00
acedf071d1 improved options passed into Finder object, :depth option is not needed if :interval is set to a depth value instead of true 2011-03-13 19:51:04 +00:00
108b6ab02e Finder's options methods now set the option when an argument is supplied and returns self for method chaining. When no argument is supplied it returns the option value itself.
Example:

    finder = Redistat::Finder.new
    finder.scope("Foo") #=> Finder object
    finder.scope        #=> Scope object
    finder.scope.to_s   #=> "Foo"
2011-03-13 19:46:52 +00:00
9920c0dc2f Merge branch 'release/v0.2.2' into dev 2011-03-12 22:26:58 +00:00
a72ad31c51 Merge branch 'release/v0.2.2' 2011-03-12 22:26:52 +00:00
008228660e started release v0.2.2 2011-03-12 22:26:12 +00:00
b8ddcdf71a Merge remote-tracking branch 'origin/dev' into dev 2011-03-12 22:25:35 +00:00
94fcd5b4ae fixed a ruby 1.9.x issue 2011-03-12 22:24:06 +00:00
e2a551d01c syntax beautification 2011-03-12 22:23:06 +00:00
43fc8bc2dd updated Hash extensions and specs 2011-03-12 22:21:15 +00:00
dcca3556ea some cleanup 2011-03-12 22:00:21 +00:00
d5f79b82a9 somewhat better loading procedure 2011-03-12 21:59:52 +00:00
0938781cd1 extend ::Hash with #set_or_incr and #merge_and_incr methods 2011-03-12 21:47:54 +00:00
82119fcf69 wrong usage of interval method in Finder spec 2011-03-11 15:38:10 +00:00
746d0fea8f Merge branch 'release/v0.2.1' into dev 2011-03-10 16:31:42 +00:00
24 changed files with 342 additions and 99 deletions

191
README.md
View File

@@ -1,4 +1,4 @@
# Redistat
# Redistat #
A Redis-backed statistics storage and querying library written in Ruby.
@@ -10,68 +10,197 @@ Redistat was originally created to replace a small hacked together statistics co
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.
## Installation
## Installation ##
gem install redistat
If you are using Ruby 1.8.x, it's recommended you also install the `SystemTimer` gem, as the Redis gem will otherwise complain.
## Usage
## Usage (Crash Course) ##
The simplest way to use Redistat is through the model wrapper.
view\_stats.rb:
class VisitorStats
require 'redistat'
class ViewStats
include Redistat::Model
end
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.
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:
### Simple Example ###
VisitorStats.store('/about', {:views => 1})
Store:
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.
ViewStats.store('hello', {:world => 4})
ViewStats.store('hello', {:world => 2}, 2.hours.ago)
To later retrieve statistics, we use the `fetch` method:
Fetch:
stats = VisitorStats.fetch('/about', 2.hour.ago, Time.now)
# stats => [{:views => 1}]
# stats.total => {:views => 1}
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.
For more detailed usage, please check spec files, and source code till I have time to write up a complete readme.
ViewStats.find('hello', 1.hour.ago, 1.hour.from_now).all
#=> [{'world' => 4}]
ViewStats.find('hello', 1.hour.ago, 1.hour.from_now).total
#=> {'world' => 4}
ViewStats.find('hello', 3.hour.ago, 1.hour.from_now).total
#=> {'world' => 6}
## Some Technical Details
### Advanced Example ###
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`.
Store page view on product #44 from Chrome 11:
In short, the above call to `store` creates the following keys in Redis:
ViewStats.store('views/product/44', {'count/chrome/11' => 1})
Fetch product #44 stats:
VisitorStats//about:2010
VisitorStats//about:201011
VisitorStats//about:20101124
VisitorStats//about:2010112401
ViewStats.find('views/product/44', 23.hours.ago, 1.hour.from_now).total
#=> { 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
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.
Store a page view on product #32 from Firefox 3:
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.
ViewStats.store('views/product/32', {'count/firefox/3' => 1})
Fetch product #32 stats:
ViewStats.find('views/product/32', 23.hours.ago, 1.hour.from_now).total
#=> { 'count' => 1, 'count/firefox' => 1, 'count/firefox/3' => 1 }
Fetch stats for all products:
ViewStats.find('views/product', 23.hours.ago, 1.hour.from_now).total
#=> { 'count' => 2,
# 'count/chrome' => 1,
# 'count/chrome/11' => 1,
# 'count/firefox' => 1,
# 'count/firefox/3' => 1 }
Store a 404 error view:
ViewStats.store('views/error/404', {'count/chrome/9' => 1})
Fetch stats for all views across the board:
ViewStats.find('views', 23.hours.ago, 1.hour.from_now).total
#=> { 'count' => 3,
# 'count/chrome' => 2,
# 'count/chrome/9' => 1,
# 'count/chrome/11' => 1,
# 'count/firefox' => 1,
# 'count/firefox/3' => 1 }
Fetch list of products known to Redistat:
finder = ViewStats.find('views/product', 23.hours.ago, 1.hour.from_now)
finder.children.map { |child| child.label.me }
#=> [ "32", "44" ]
finder.children.map { |child| child.label.to_s }
#=> [ "views/products/32", "views/products/44" ]
finder.children.map { |child| child.total }
#=> [ { "count" => 1, "count/firefox" => 1, "count/firefox/3" => 1 },
# { "count" => 1, "count/chrome" => 1, "count/chrome/11" => 1 } ]
## Todo
## Terminology ##
* Proper/complete readme.
### Scope ###
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.
### Label ###
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.
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`.
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.
### Input Statistics Data ###
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.
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.
### Depth (Storage Accuracy) ###
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`.
You can set depth within your model using the `#depth` class method. Available depths are: `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec`
### Time Ranges ###
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.
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`.
### The Finder Object ###
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.
This section does need further expanding as there's a lot to cover when it comes to the finder.
## Internals ##
### Storing / Writing ###
Redistat stores all data into a Redis hash keys. The Redis key name the used consists of three parts. The scope, label, and datetime:
{scope}/{label}:{datetime}
For example, this...
ViewStats.store('views/product/44', {'count/chrome/11' => 1})
...would store the follow hash of data...
{ 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
...to all 12 of these Redis hash keys...
ViewStats/views:2011
ViewStats/views:201103
ViewStats/views:20110315
ViewStats/views:2011031510
ViewStats/views/product:2011
ViewStats/views/product:201103
ViewStats/views/product:20110315
ViewStats/views/product:2011031510
ViewStats/views/product/44:2011
ViewStats/views/product/44:201103
ViewStats/views/product/44:20110315
ViewStats/views/product/44:2011031510
...by creating the Redis key, and/or hash field if needed, otherwise it simply increments the already existing data.
It would also create the following Redis sets to keep track of which child labels are available:
ViewStats.label_index:
ViewStats.label_index:views
ViewStats.label_index:views/product
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.
### Fetching / Reading ###
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.
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.
## Todo ##
* More details in Readme.
* Documentation.
* Anything else that becomes apparent after real-world use.
## Credits
## Credits ##
[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.
## Note on Patches/Pull Requests
## Note on Patches/Pull Requests ##
* Fork the project.
* 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
* Send me a pull request. Bonus points for topic branches.
## License and Copyright
## License and Copyright ##
Copyright (c) 2011 Jim Myhrberg.

View File

@@ -1,14 +1,20 @@
require 'rubygems'
require 'active_support'
require 'active_support/hash_with_indifferent_access' if !{}.respond_to?(:with_indifferent_access) # Active Support 2.x and 3.x
require 'redis'
require 'date'
require 'time'
require 'time_ext'
require 'json'
require 'digest/sha1'
# Active Support 2.x or 3.x
require 'active_support'
if !{}.respond_to?(:with_indifferent_access)
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/reverse_merge'
end
require 'time_ext'
require 'redis'
require 'json'
require 'redistat/options'
require 'redistat/connection'
require 'redistat/database'
@@ -17,7 +23,6 @@ require 'redistat/date'
require 'redistat/date_helper'
require 'redistat/event'
require 'redistat/finder'
require 'redistat/finder/date_set'
require 'redistat/key'
require 'redistat/label'
require 'redistat/model'
@@ -26,10 +31,7 @@ require 'redistat/scope'
require 'redistat/summary'
require 'redistat/version'
require 'redistat/core_ext/date'
require 'redistat/core_ext/time'
require 'redistat/core_ext/fixnum'
require 'redistat/core_ext/bignum'
require 'redistat/core_ext'
module Redistat

5
lib/redistat/core_ext.rb Normal file
View File

@@ -0,0 +1,5 @@
require 'redistat/core_ext/bignum'
require 'redistat/core_ext/date'
require 'redistat/core_ext/fixnum'
require 'redistat/core_ext/hash'
require 'redistat/core_ext/time'

View File

@@ -0,0 +1,23 @@
class Hash
def merge_and_incr(hash)
self.clone.merge_and_incr!(hash)
end
def merge_and_incr!(hash)
raise ArgumentError unless hash.is_a?(Hash)
hash.each do |key, value|
self[key] = value unless self.set_or_incr(key, value)
end
self
end
def set_or_incr(key, value)
return false unless value.is_a?(Numeric)
self[key] = 0 unless self.has_key?(key)
return false unless self[key].is_a?(Numeric)
self[key] += value
true
end
end

View File

@@ -91,7 +91,7 @@ module Redistat
event = db.hgetall "#{scope}#{KEY_EVENT}#{id}"
return nil if event.size == 0
self.new( event["scope"], event["label"], event["date"], JSON.parse(event["stats"]),
JSON.parse(event["meta"]), JSON.parse(event["options"]), false )
JSON.parse(event["options"]), JSON.parse(event["meta"]), false )
end
end

View File

@@ -1,3 +1,5 @@
require 'redistat/finder/date_set'
module Redistat
class Finder
include Database
@@ -79,49 +81,56 @@ module Redistat
}
end
def connection_ref(ref)
def connection_ref(ref = nil)
return options[:connection_ref] if ref.nil?
reset! if options[:connection_ref] != ref
options[:connection_ref] = ref
self
end
def scope(scope)
reset! if !options[:scope].nil? && options[:scope].to_s != scope
options[:scope] = Scope.new(scope)
def scope(input = nil)
return options[:scope] if input.nil?
reset! if !options[:scope].nil? && options[:scope].to_s != input.to_s
options[:scope] = Scope.new(input)
self
end
def label(label)
reset! if options.has_key?(:label) && options[:label].to_s != label.to_s
options[:label] = (!label.nil?) ? Label.new(label) : nil
def label(input = nil)
return options[:label] if input.nil?
reset! if options.has_key?(:label) && options[:label].to_s != input.to_s
options[:label] = (!input.nil?) ? Label.new(input) : nil
self
end
def dates(from, till)
from(from).till(till)
def dates(start, finish)
from(start).till(finish)
end
alias :date :dates
def from(date)
def from(date = nil)
return options[:from] if date.nil?
reset! if options[:from] != date
options[:from] = date
self
end
def till(date)
def till(date = nil)
return options[:till] if date.nil?
reset! if options[:till] != date
options[:till] = date
self
end
alias :until :till
def depth(unit)
def depth(unit = nil)
return options[:depth] if unit.nil?
reset! if options[:depth] != unit
options[:depth] = unit
self
end
def interval(unit)
def interval(unit = nil)
return options[:interval] if unit.nil?
reset! if options[:interval] != unit
options[:interval] = unit
self

View File

@@ -9,6 +9,10 @@ module Redistat
end
def find_date_sets(start_date, end_date, depth = nil, interval = false)
if depth.nil? && interval.is_a?(Symbol)
depth = interval
interval = true
end
start_date = start_date.to_time if start_date.is_a?(::Date)
end_date = end_date.to_time if end_date.is_a?(::Date)
if !interval

View File

@@ -30,13 +30,14 @@ module Redistat
options[:depth]
end
def scope
@scope.to_s
end
# def scope
# @scope.to_s
# end
def scope=(input)
@scope = (input.instance_of?(Redistat::Scope)) ? input : Scope.new(input)
end
attr_reader :scope
def label=(input)
@label = (input.instance_of?(Redistat::Label)) ? input : Label.create(input, @options)
@@ -60,7 +61,6 @@ module Redistat
def update_index
@label.groups.each do |label|
# break if label.parent.nil?
parent = (label.parent || "")
db.sadd("#{scope}#{LABEL_INDEX}#{parent}", label.me)
end

View File

@@ -11,6 +11,11 @@ module Redistat
self.new(name, opts).save
end
def self.join(*args)
args = args.map {|i| i.to_s}
self.new(args.reject {|i| i.blank? }.join(GROUP_SEPARATOR))
end
def initialize(str, opts = {})
parse_options(opts)
@raw = str.to_s

View File

@@ -13,7 +13,7 @@ module Redistat
#
def store(label, stats = {}, date = nil, opts = {}, meta = {})
Event.new(name, label, date, stats, options.merge(opts), meta).save
Event.new(self.name, label, date, stats, options.merge(opts), meta).save
end
alias :event :store
@@ -29,18 +29,22 @@ module Redistat
:till => till }.merge(options.merge(opts)) )
end
def find_event(event_id)
Event.find(self.name, event_id)
end
#
# options methods
#
option_accessor :depth
option_accessor :class_name
option_accessor :scope
option_accessor :store_event
option_accessor :hashed_label
option_accessor :label_indexing
alias :scope :class_name
alias :class_name :scope
def connect_to(opts = {})
Connection.create(opts.merge(:ref => name))
@@ -58,7 +62,7 @@ module Redistat
alias :redis :connection
def name
options[:class_name] || (@name ||= self.to_s)
options[:scope] || (@name ||= self.to_s)
end
end

View File

@@ -5,8 +5,6 @@ module Redistat
base.extend(ClassMethods)
end
class InvalidDefaultOptions < ArgumentError; end
module ClassMethods
def option_accessor(*opts)
opts.each do |option|

View File

@@ -12,12 +12,5 @@ module Redistat
@till = options[:till] ||= nil
end
def set_or_incr(key, value)
self[key] = 0 if !self.has_key?(key)
self[key] += value
self
end
end
end
end

View File

@@ -18,10 +18,10 @@ module Redistat
if options[:enable_grouping]
stats = inject_group_summaries(stats)
key.groups.each { |k|
key.groups.each do |k|
update_key(k, stats, depth_limit, options[:connection_ref])
k.update_index if options[:label_indexing]
}
end
else
update_key(key, stats, depth_limit, options[:connection_ref])
end
@@ -43,6 +43,7 @@ module Redistat
end
def self.inject_group_summaries!(stats)
summaries = {}
stats.each do |key, value|
parts = key.to_s.split(GROUP_SEPARATOR)
parts.pop
@@ -51,11 +52,11 @@ module Redistat
parts.each do |part|
sum_parts << part
sum_key = sum_parts.join(GROUP_SEPARATOR)
(stats.has_key?(sum_key)) ? stats[sum_key] += value : stats[sum_key] = value
(summaries.has_key?(sum_key)) ? summaries[sum_key] += value : summaries[sum_key] = value
end
end
end
stats
stats.merge_and_incr!(summaries)
end
def self.inject_group_summaries(stats)

View File

@@ -1,3 +1,3 @@
module Redistat
VERSION = "0.2.1"
VERSION = "0.2.5"
end

View File

@@ -22,10 +22,9 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'activesupport', '>= 2.3.6'
s.add_runtime_dependency 'json', '>= 1.4.0'
s.add_runtime_dependency 'redis', '>= 2.1.0'
s.add_runtime_dependency 'time_ext', '>= 0.2.8'
s.add_runtime_dependency 'time_ext', '>= 0.2.9'
s.add_development_dependency 'rspec', '>= 2.1.0'
s.add_development_dependency 'rcov', '>= 0.9.9'
s.add_development_dependency 'yard', '>= 0.6.3'
s.add_development_dependency 'ruby-debug'
end

View File

@@ -0,0 +1,30 @@
require "spec_helper"
describe Hash do
it "should #set_or_incr values" do
hash = {:count => 1}
hash.set_or_incr(:sum, 3).should be_true
hash.should == {:count => 1, :sum => 3}
hash.set_or_incr(:count, 4).should be_true
hash.should == {:count => 5, :sum => 3}
hash.set_or_incr(:count, 'test').should be_false
hash.set_or_incr(:view, 'test').should be_false
hash.should == {:count => 5, :sum => 3}
hash[:view] = 'test'
hash.set_or_incr(:view, 3).should be_false
end
it "should #merge_and_incr hashes" do
hash = { :count => 1, :city => 'hell', :sum => 3, :name => 'john' }
new_hash = { :count => 3, :city => 'slum', :views => 2 }
hash.clone.merge_and_incr(new_hash).should == { :count => 4, :city => 'slum', :views => 2,
:sum => 3, :name => 'john' }
new_hash = { :count => 'six', :city => 'slum', :views => 2, :time => 'late' }
hash.clone.merge_and_incr(new_hash).should == { :count => 'six', :city => 'slum', :views => 2,
:sum => 3, :name => 'john', :time => 'late' }
end
end

View File

@@ -8,8 +8,8 @@ describe Redistat::Event do
@scope = "PageViews"
@label = "about_us"
@label_hash = Digest::SHA1.hexdigest(@label)
@stats = {:views => 1}
@meta = {:user_id => 239}
@stats = {'views' => 1}
@meta = {'user_id' => 239}
@options = {:depth => :hour}
@date = Time.now
@event = Redistat::Event.new(@scope, @label, @date, @stats, @options, @meta)
@@ -17,7 +17,7 @@ describe Redistat::Event do
it "should initialize properly" do
@event.id.should be_nil
@event.scope.should == @scope
@event.scope.to_s.should == @scope
@event.label.to_s.should == @label
@event.label_hash.should == @label_hash
@event.date.to_time.to_s.should == @date.to_s
@@ -63,9 +63,11 @@ describe Redistat::Event do
it "should find event by id" do
@event = Redistat::Event.new(@scope, @label, @date, @stats, @options.merge({:store_event => true}), @meta).save
fetched = Redistat::Event.find(@scope, @event.id)
@event.scope.should == fetched.scope
@event.scope.to_s.should == fetched.scope.to_s
@event.label.to_s.should == fetched.label.to_s
@event.date.to_s.should == fetched.date.to_s
@event.stats.should == fetched.stats
@event.meta.should == fetched.meta
end
it "should store summarized statistics" do

View File

@@ -28,11 +28,13 @@ describe Redistat::Finder::DateSet do
result = Redistat::Finder::DateSet.new.find_date_sets(t_start, t_end, :hour, true)
result[0][:add].should == ["2010082818", "2010082819", "2010082820", "2010082821", "2010082822"]
result[0][:rem].should == []
result.should == Redistat::Finder::DateSet.new(t_start, t_end, nil, :hour)
t_end = t_start + 4.days
result = Redistat::Finder::DateSet.new.find_date_sets(t_start, t_end, :day, true)
result[0][:add].should == ["20100828", "20100829", "20100830", "20100831", "20100901"]
result[0][:rem].should == []
result.should == Redistat::Finder::DateSet.new(t_start, t_end, nil, :day)
end
it "should find start keys properly" do

View File

@@ -25,28 +25,36 @@ describe Redistat::Finder do
finder.options[:label].to_s.should == options[:label]
finder.options.should == options.merge(:scope => finder.options[:scope], :label => finder.options[:label])
finder = Redistat::Finder.scope("hello")
finder.options[:scope].to_s.should == "hello"
finder.scope.to_s.should == "hello"
finder = Redistat::Finder.label("hello")
finder.options[:label].to_s.should == "hello"
finder.label.to_s.should == "hello"
finder = Redistat::Finder.dates(@two_hours_ago, @one_hour_ago)
finder.options[:from].should == @two_hours_ago
finder.options[:till].should == @one_hour_ago
finder = Redistat::Finder.scope("hello")
finder.options[:scope].to_s.should == "hello"
finder = Redistat::Finder.label("hello")
finder.options[:label].to_s.should == "hello"
finder = Redistat::Finder.from(@two_hours_ago)
finder.options[:from].should == @two_hours_ago
finder.from.should == @two_hours_ago
finder = Redistat::Finder.till(@one_hour_ago)
finder.options[:till].should == @one_hour_ago
finder.till.should == @one_hour_ago
finder = Redistat::Finder.depth(:hour)
finder.options[:depth].should == :hour
finder.depth.should == :hour
finder = Redistat::Finder.interval(:hour)
finder.options[:interval].should == :hour
finder = Redistat::Finder.interval(true)
finder.options[:interval].should be_true
finder.interval.should be_true
finder = Redistat::Finder.interval(false)
finder.options[:interval].should be_false
finder.interval.should be_false
end
it "should fetch stats properly" do

View File

@@ -13,7 +13,7 @@ describe Redistat::Key do
end
it "should initialize properly" do
@key.scope.should == @scope
@key.scope.to_s.should == @scope
@key.label.to_s.should == @label
@key.label_hash.should == @label_hash
@key.groups.map { |k| k.instance_variable_get("@label") }.should == @key.instance_variable_get("@label").groups
@@ -28,6 +28,8 @@ describe Redistat::Key do
@key.to_s(props.last).should == "#{@scope}/#{@label}:#{@key.date.to_s(props.last)}"
props.pop
end
key = Redistat::Key.new(@scope, nil, @date, {:depth => :hour})
key.to_s.should == "#{@scope}:#{key.date.to_s(:hour)}"
end
it "should abide to hashed_label option" do
@@ -44,10 +46,10 @@ describe Redistat::Key do
it "should allow changing attributes" do
# scope
@key.scope.should == @scope
@key.scope.to_s.should == @scope
@scope = "VisitorCount"
@key.scope = @scope
@key.scope.should == @scope
@key.scope.to_s.should == @scope
# date
@key.date.to_time.to_s.should == @date.to_s
@date = Time.now

View File

@@ -25,6 +25,19 @@ describe Redistat::Label do
db.hget(Redistat::KEY_LABELS, label.hash).should == name
end
it "should join labels" do
include Redistat
label = Label.join('email', 'message', 'public')
label.should be_a(Label)
label.to_s.should == 'email/message/public'
label = Label.join(Label.new('email'), Label.new('message'), Label.new('public'))
label.should be_a(Label)
label.to_s.should == 'email/message/public'
label = Label.join('email', '', 'message', nil, 'public')
label.should be_a(Label)
label.to_s.should == 'email/message/public'
end
describe "Grouping" do
before(:each) do
@name = "message/public/offensive"

View File

@@ -25,6 +25,6 @@ end
class ModelHelper4
include Redistat::Model
class_name "FancyHelper"
scope "FancyHelper"
end

View File

@@ -28,11 +28,16 @@ describe Redistat::Model do
finder.options[:till].should == one_hour_ago
end
it "should #find_event" do
Redistat::Event.should_receive(:find).with('ModelHelper1', 1)
ModelHelper1.find_event(1)
end
it "should listen to model-defined options" do
ModelHelper2.depth.should == :day
ModelHelper2.store_event.should == true
ModelHelper2.hashed_label.should == true
ModelHelper2.class_name.should be_nil
ModelHelper2.scope.should be_nil
ModelHelper1.depth.should == nil
ModelHelper1.store_event.should == nil
@@ -50,7 +55,7 @@ describe Redistat::Model do
ModelHelper1.store_event.should == nil
ModelHelper1.hashed_label.should == nil
ModelHelper4.class_name.should == "FancyHelper"
ModelHelper4.scope.should == "FancyHelper"
ModelHelper4.send(:name).should == "FancyHelper"
end

View File

@@ -46,6 +46,15 @@ describe Redistat::Summary do
end
end
it "should update summaries even if no label is set" do
key = Redistat::Key.new(@scope, nil, @date, {:depth => :day})
Redistat::Summary.update(key, @stats, :hour)
summary = db.hgetall(key.to_s(:hour))
summary.should have(2).items
summary["views"].should == "3"
summary["visitors"].should == "2"
end
it "should inject stats key grouping summaries" do
hash = { "count/hello" => 3, "count/world" => 7,
"death/bomb" => 4, "death/unicorn" => 3,