diff --git a/lib/redistat.rb b/lib/redistat.rb index f95cefc..6a230b5 100644 --- a/lib/redistat.rb +++ b/lib/redistat.rb @@ -36,6 +36,7 @@ module Redistat KEY_EVENT = ".event:" KEY_LEBELS = "Redistat.lables:" KEY_EVENT_IDS = ".event_ids" + GROUP_SEPARATOR = "/" class InvalidOptions < ArgumentError; end class RedisServerIsTooOld < Exception; end diff --git a/lib/redistat/event.rb b/lib/redistat/event.rb index 54cb936..f22b162 100644 --- a/lib/redistat/event.rb +++ b/lib/redistat/event.rb @@ -31,7 +31,10 @@ module Redistat end def default_options - { :depth => :hour, :store_event => false, :connection_ref => nil } + { :depth => :hour, + :store_event => false, + :connection_ref => nil, + :enable_grouping => true } end def new? @@ -72,7 +75,7 @@ module Redistat def save return false if !self.new? - Summary.update_all(@key, @stats, depth_limit, @connection_ref) + Summary.update_all(@key, @stats, depth_limit, @connection_ref, @options[:enable_grouping]) if @options[:store_event] @id = self.next_id db.hmset("#{self.scope}#{KEY_EVENT}#{@id}", diff --git a/lib/redistat/key.rb b/lib/redistat/key.rb index d253a5d..9e08cf2 100644 --- a/lib/redistat/key.rb +++ b/lib/redistat/key.rb @@ -39,10 +39,20 @@ module Redistat @label.hash end + def label_groups + @label.groups + end + def label=(input) @label = (input.instance_of?(Redistat::Label)) ? input : Label.create(input, @options) end + def groups + @groups ||= label_groups.map do |label_name| + self.class.new(@scope, label_name, self.date, @options) + end + end + def to_s(depth = nil) depth ||= @options[:depth] key = self.prefix diff --git a/lib/redistat/label.rb b/lib/redistat/label.rb index 5ecf865..e059eab 100644 --- a/lib/redistat/label.rb +++ b/lib/redistat/label.rb @@ -5,6 +5,10 @@ module Redistat attr_reader :raw attr_reader :connection_ref + def self.create(name, options = {}) + self.new(name, options).save + end + def initialize(str, options = {}) @options = options @raw = str.to_s @@ -31,8 +35,18 @@ module Redistat @saved ||= false end - def self.create(name, options = {}) - self.new(name, options).save + def groups + return @groups if @groups + @groups = [] + parent = "" + @raw.split(GROUP_SEPARATOR).each do |part| + if !part.blank? + group = ((parent.blank?) ? "" : "#{parent}/") + part + @groups << group + parent = group + end + end + @groups.reverse! end end diff --git a/lib/redistat/summary.rb b/lib/redistat/summary.rb index 5dbcab1..ed03f99 100644 --- a/lib/redistat/summary.rb +++ b/lib/redistat/summary.rb @@ -2,23 +2,57 @@ module Redistat class Summary include Database - def self.update_all(key, stats = {}, depth_limit = nil, connection_ref = nil) + def self.update_all(key, stats = {}, depth_limit = nil, connection_ref = nil, enable_grouping = nil) stats ||= {} - depth_limit ||= key.depth return nil if stats.size == 0 + + depth_limit ||= key.depth + enable_grouping = true if enable_grouping.nil? + + if enable_grouping + stats = inject_group_summaries(stats) + key.groups.each { |k| + update_key(k, stats, depth_limit, connection_ref) + } + else + update_key(key, stats, depth_limit, connection_ref) + end + end + + private + + def self.update_key(key, stats, depth_limit, connection_ref) Date::DEPTHS.each do |depth| update(key, stats, depth, connection_ref) break if depth == depth_limit end end - private - def self.update(key, stats, depth, connection_ref = nil) stats.each do |field, value| db(connection_ref).hincrby key.to_s(depth), field, value end end + def self.inject_group_summaries!(stats) + stats.each do |key, value| + parts = key.to_s.split(GROUP_SEPARATOR) + parts.pop + if parts.size > 0 + sum_parts = [] + 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 + end + end + end + stats + end + + def self.inject_group_summaries(stats) + inject_group_summaries!(stats.clone) + end + end end \ No newline at end of file diff --git a/spec/key_spec.rb b/spec/key_spec.rb index 9d8bec4..cc48d08 100644 --- a/spec/key_spec.rb +++ b/spec/key_spec.rb @@ -14,6 +14,7 @@ describe Redistat::Key do @key.scope.should == @scope @key.label.should == @label @key.label_hash.should == @label_hash + @key.label_groups.should == @key.instance_variable_get("@label").groups @key.date.should be_instance_of(Redistat::Date) @key.date.to_time.to_s.should == @date.to_s end @@ -60,4 +61,16 @@ describe Redistat::Key do @key.label_hash == @label_hash end + it "should create a group of keys from label group" do + label = 'message/public/offensive' + result = [ "message/public/offensive", + "message/public", + "message" ] + + key = Redistat::Key.new(@scope, label, @date, {:depth => :hour}) + + key.label_groups.should == result + key.groups.map { |k| k.label }.should == result + end + end \ No newline at end of file diff --git a/spec/label_spec.rb b/spec/label_spec.rb index abe277c..4141d12 100644 --- a/spec/label_spec.rb +++ b/spec/label_spec.rb @@ -25,4 +25,24 @@ describe Redistat::Label do db.get("#{Redistat::KEY_LEBELS}#{label.hash}").should == name end + it "should separate label names into groups" do + name = "message/public/offensive" + label = Redistat::Label.new(name) + label.name.should == name + label.groups.should == [ "message/public/offensive", + "message/public", + "message" ] + + name = "/message/public/" + label = Redistat::Label.new(name) + label.name.should == name + label.groups.should == [ "message/public", + "message" ] + + name = "message" + label = Redistat::Label.new(name) + label.name.should == name + label.groups.should == [ "message" ] + end + end \ No newline at end of file diff --git a/spec/summary_spec.rb b/spec/summary_spec.rb index eadcdbb..303c03f 100644 --- a/spec/summary_spec.rb +++ b/spec/summary_spec.rb @@ -46,4 +46,78 @@ describe Redistat::Summary do end end + it "should inject stats key grouping summaries" do + hash = { "count/hello" => 3, "count/world" => 7, + "death/bomb" => 4, "death/unicorn" => 3, + :"od/sugar" => 7, :"od/meth" => 8 } + res = Redistat::Summary.send(:inject_group_summaries, hash) + res.should == { "count" => 10, "count/hello" => 3, "count/world" => 7, + "death" => 7, "death/bomb" => 4, "death/unicorn" => 3, + "od" => 15, :"od/sugar" => 7, :"od/meth" => 8 } + end + + it "should properly store key group summaries" do + stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4} + Redistat::Summary.update_all(@key, stats, :hour) + summary = db.hgetall(@key.to_s(:hour)) + summary.should have(4).items + summary["views"].should == "3" + summary["visitors"].should == "6" + summary["visitors/eu"].should == "2" + summary["visitors/us"].should == "4" + end + + it "should not store key group summaries when option is disabled" do + stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4} + Redistat::Summary.update_all(@key, stats, :hour, nil, false) + summary = db.hgetall(@key.to_s(:hour)) + summary.should have(3).items + summary["views"].should == "3" + summary["visitors/eu"].should == "2" + summary["visitors/us"].should == "4" + end + + it "should store label-based grouping enabled stats" do + stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4} + label = "views/about_us" + key = Redistat::Key.new(@scope, label, @date) + Redistat::Summary.update_all(key, stats, :hour) + + key.groups[0].label.should == "views/about_us" + key.groups[1].label.should == "views" + child1 = key.groups[0] + parent = key.groups[1] + + label = "views/contact" + key = Redistat::Key.new(@scope, label, @date) + Redistat::Summary.update_all(key, stats, :hour) + + key.groups[0].label.should == "views/contact" + key.groups[1].label.should == "views" + child2 = key.groups[0] + + summary = db.hgetall(child1.to_s(:hour)) + summary["views"].should == "3" + summary["visitors/eu"].should == "2" + summary["visitors/us"].should == "4" + + summary = db.hgetall(child2.to_s(:hour)) + summary["views"].should == "3" + summary["visitors/eu"].should == "2" + summary["visitors/us"].should == "4" + + summary = db.hgetall(parent.to_s(:hour)) + summary["views"].should == "6" + summary["visitors/eu"].should == "4" + summary["visitors/us"].should == "8" + end + end + + + + + + + +