13 Commits

Author SHA1 Message Date
92375b229a Merge branch 'release/v0.0.3' 2010-11-24 00:38:32 +00:00
e362c93d9a Version bump to 0.0.3 2010-11-24 00:38:19 +00:00
0f5b7449b0 updated readme 2010-11-24 00:37:28 +00:00
ea732b4734 updated gemfile and rakefile 2010-11-24 00:37:10 +00:00
62c3492c93 some whitespace cleanup 2010-11-24 00:36:44 +00:00
c5f52455cc replaced Redistat::Model#find alias to #fetch with
#lookup instead to avoid conflicts with
ActiveRecord
2010-11-24 00:36:25 +00:00
1226f8b89a Merge branch 'release/v0.0.2' into dev 2010-11-22 13:30:19 +00:00
bc7a563f20 Merge branch 'release/v0.0.2' 2010-11-22 13:30:14 +00:00
39da4c96a7 Version bump to 0.0.2 2010-11-22 13:29:56 +00:00
fa7903dc7a added gemspec to .gitignore 2010-11-22 13:29:41 +00:00
8a0e1a47a2 added proper support for hashed_label option, and
disabled it by default
2010-11-22 13:28:19 +00:00
8e3e54a027 updated gemspec description in Rakefile 2010-11-22 12:36:14 +00:00
07ae8b5c78 Merge branch 'release/v0.0.1' into dev 2010-11-22 12:31:45 +00:00
16 changed files with 137 additions and 98 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ pkg
.yardoc/*
spec/db/*
doc
redistat.gemspec

10
Gemfile
View File

@@ -1,12 +1,14 @@
source 'http://rubygems.org/'
gem 'activesupport', '>= 2.3.0'
gem 'json', '>= 1.0.0'
gem 'redis', '>= 2.0.0'
gem 'json', '>= 1.4.6'
gem 'redis', '>= 2.1.1'
gem 'system_timer', '>= 1.0.0'
gem 'time_ext', '>= 0.2.6'
group :development do
gem 'rspec', '>= 2.0.1'
gem 'yard', '>= 0.6.1'
gem 'jeweler', '>= 1.5.1'
gem 'rspec', '>= 2.1.0'
gem 'yard', '>= 0.6.3'
gem 'i18n'
end

View File

@@ -3,8 +3,14 @@ GEM
specs:
activesupport (3.0.3)
diff-lcs (1.1.2)
git (1.2.5)
i18n (0.4.2)
jeweler (1.5.1)
bundler (~> 1.0.0)
git (>= 1.2.5)
rake
json (1.4.6)
rake (0.8.7)
redis (2.1.1)
rspec (2.1.0)
rspec-core (~> 2.1.0)
@@ -14,6 +20,7 @@ GEM
rspec-expectations (2.1.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.1.0)
system_timer (1.0)
time_ext (0.2.6)
activesupport (>= 2.3.0)
yard (0.6.3)
@@ -24,8 +31,10 @@ PLATFORMS
DEPENDENCIES
activesupport (>= 2.3.0)
i18n
json (>= 1.0.0)
redis (>= 2.0.0)
rspec (>= 2.0.1)
jeweler (>= 1.5.1)
json (>= 1.4.6)
redis (>= 2.1.1)
rspec (>= 2.1.0)
system_timer (>= 1.0.0)
time_ext (>= 0.2.6)
yard (>= 0.6.1)
yard (>= 0.6.3)

View File

@@ -2,11 +2,66 @@
A Redis-backed statistics storage and querying library written in Ruby.
## Early Beta
Redistat was originally created to replace a small hacked together statistics collection solution which was MySQL-based. When I started I had a short list of requirements:
Currently this is an early beta release. Readme and documentation is forthcoming.
* Store and increment/decrement integer values (counters, etc)
* Up to the second statistics available at all times
* Screamingly fast
For now, please check `spec/model_spec.rb` and `spec/model_helper.rb` to get started with how to use Redistat.
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
gem install redistat
## Usage
The simplest way to use Redistat is through the model wrapper.
class VisitorStats
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:
VisitorStats.store('/about', {:views => 1})
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.
To later retrieve statistics, we use the `fetch` method:
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.
## Some Technical Details
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`.
In short, the above call to `store` creates the following keys in Redis:
VisitorStats//about:2010
VisitorStats//about:201011
VisitorStats//about:20101124
VisitorStats//about:2010112401
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.
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.
## Todo
* Proper/complete readme.
* Documentation.
* Anything else that becomes apparent after real-world use.
## Note on Patches/Pull Requests

View File

@@ -5,16 +5,18 @@ begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = 'redistat'
gem.summary = 'TODO: one-line summary of your gem'
gem.description = 'TODO: longer description of your gem'
gem.summary = 'A Redis-backed statistics storage and querying library written in Ruby.'
gem.description = 'A Redis-backed statistics storage and querying library written in Ruby.'
gem.email = 'contact@jimeh.me'
gem.homepage = 'http://github.com/jimeh/redistat'
gem.authors = ['Jim Myhrberg']
gem.add_dependency 'activesupport', '>= 2.3.0'
gem.add_dependency 'json', '>= 1.0.0'
gem.add_dependency 'redis', '>= 2.0.0'
gem.add_dependency 'json', '>= 1.4.6'
gem.add_dependency 'redis', '>= 2.1.1'
gem.add_dependency 'system_timer', '>= 1.0.0'
gem.add_dependency 'time_ext', '>= 0.2.6'
gem.add_development_dependency 'rspec', '>= 2.0.1'
gem.add_development_dependency 'jeweler', '>= 1.5.1'
gem.add_development_dependency 'rspec', '>= 2.1.0'
gem.add_development_dependency 'yard', '>= 0.6.1'
end
Jeweler::GemcutterTasks.new

View File

@@ -1 +1 @@
0.0.1
0.0.3

View File

@@ -82,23 +82,3 @@ module Redistat
module_function :connect, :connection, :flush, :redis, :redis=, :options, :threaded
end

View File

@@ -5,20 +5,20 @@ module Redistat
attr_accessor :date
attr_accessor :options
def initialize(scope, label = nil, date = nil, options = {})
def initialize(scope, label_name = nil, time_stamp = nil, options = {})
@options = default_options.merge(options || {})
@scope = scope
self.label = label if !label.nil?
self.date = date ||= Time.now
@options = default_options.merge(options ||= {})
self.label = label_name if !label_name.nil?
self.date = time_stamp ||= Time.now
end
def default_options
{ :depth => :day }
{ :depth => :hour, :hashed_label => false }
end
def prefix
key = "#{@scope}"
key << "/" + ((@options[:label_hash].nil? || @options[:label_hash] == true) ? @label.hash : @label.name) if !label.nil?
key << "/#{label}" if !label.nil?
key << ":"
key
end
@@ -40,7 +40,7 @@ module Redistat
end
def label=(input)
@label = (input.instance_of?(Redistat::Label)) ? input : Label.create(input)
@label = (input.instance_of?(Redistat::Label)) ? input : Label.create(input, @options)
end
def to_s(depth = nil)

View File

@@ -2,16 +2,23 @@ module Redistat
class Label
include Database
attr_reader :name
attr_reader :hash
attr_reader :raw
def initialize(str)
@name = str.to_s
@hash = Digest::SHA1.hexdigest(@name)
def initialize(str, options = {})
@options = options
@raw = str.to_s
end
def name
@options[:hashed_label] ? hash : @raw
end
def hash
@hash ||= Digest::SHA1.hexdigest(@raw)
end
def save
@saved = (db.set("#{KEY_LEBELS}#{@hash}", @name) == "OK")
@saved = (db.set("#{KEY_LEBELS}#{hash}", @raw) == "OK") if @options[:hashed_label]
self
end
@@ -19,8 +26,8 @@ module Redistat
@saved ||= false
end
def self.create(name)
self.new(name).save
def self.create(name, options = {})
self.new(name, options).save
end
end

View File

@@ -18,7 +18,15 @@ module Redistat
:till => till
}.merge(options.merge(opts)))
end
alias :find :fetch
alias :lookup :fetch
def hashed_label(boolean = nil)
if !boolean.nil?
options[:hashed_label] = boolean
else
options[:hashed_label] || nil
end
end
def depth(depth = nil)
if !depth.nil?

View File

@@ -101,12 +101,3 @@ describe Redistat::Finder do
end
end

View File

@@ -19,24 +19,24 @@ describe Redistat::Key do
end
it "should convert to string properly" do
@key.to_s.should == "#{@scope}/#{@label_hash}:#{@key.date.to_s(:hour)}"
@key.to_s.should == "#{@scope}/#{@label}:#{@key.date.to_s(:hour)}"
props = [:year, :month, :day, :hour, :min, :sec]
props.each do
@key.to_s(props.last).should == "#{@scope}/#{@label_hash}:#{@key.date.to_s(props.last)}"
@key.to_s(props.last).should == "#{@scope}/#{@label}:#{@key.date.to_s(props.last)}"
props.pop
end
end
it "should abide to hash_label option" do
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour, :label_hash => true})
it "should abide to hashed_label option" do
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour, :hashed_label => true})
@key.to_s.should == "#{@scope}/#{@label_hash}:#{@key.date.to_s(:hour)}"
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour, :label_hash => false})
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour, :hashed_label => false})
@key.to_s.should == "#{@scope}/#{@label}:#{@key.date.to_s(:hour)}"
end
it "should have default depth option" do
@key = Redistat::Key.new(@scope, @label, @date)
@key.depth.should == :day
@key.depth.should == :hour
end
it "should allow changing attributes" do

View File

@@ -15,14 +15,14 @@ describe Redistat::Label do
end
it "should store a label hash lookup key" do
@label.save
@label.saved?.should be_true
db.get("#{Redistat::KEY_LEBELS}#{@label.hash}").should == @name
label = Redistat::Label.new(@name, {:hashed_label => true}).save
label.saved?.should be_true
db.get("#{Redistat::KEY_LEBELS}#{label.hash}").should == @name
@name = "contact_us"
@label = Redistat::Label.create(@name)
@label.saved?.should be_true
db.get("#{Redistat::KEY_LEBELS}#{@label.hash}").should == @name
name = "contact_us"
label = Redistat::Label.create(name, {:hashed_label => true})
label.saved?.should be_true
db.get("#{Redistat::KEY_LEBELS}#{label.hash}").should == name
end
end

View File

@@ -11,5 +11,6 @@ class ModelHelper2
depth :day
store_event true
hashed_label true
end

View File

@@ -16,17 +16,23 @@ describe Redistat::Model do
it "should listen to model-defined options" do
ModelHelper2.depth.should == :day
ModelHelper2.store_event.should == true
ModelHelper2.hashed_label.should == true
ModelHelper.depth.should == nil
ModelHelper.store_event.should == nil
ModelHelper.hashed_label.should == nil
ModelHelper.depth(:hour)
ModelHelper.depth.should == :hour
ModelHelper.store_event(true)
ModelHelper.store_event.should == true
ModelHelper.hashed_label(true)
ModelHelper.hashed_label.should == true
ModelHelper.options[:depth] = nil
ModelHelper.options[:store_event] = nil
ModelHelper.options[:hashed_label] = nil
ModelHelper.depth.should == nil
ModelHelper.store_event.should == nil
ModelHelper.hashed_label.should == nil
end
it "should store and fetch stats" do

View File

@@ -47,26 +47,3 @@ describe Redistat::Summary do
end
end