minuteman 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+ gem "rake"
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ minuteman (0.0.1)
5
+ redis (~> 3.0.2)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ minitest (4.1.0)
11
+ rake (0.9.2.2)
12
+ redis (3.0.2)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ minitest (~> 4.1.0)
19
+ minuteman!
20
+ rake
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Bruno Aguirre
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Minuteman
2
+
3
+ _Wikipedia_: Minutemen were members of teams from Massachusetts that were well-prepared
4
+ militia companies of select men from the American colonial partisan militia
5
+ during the American Revolutionary War. _They provided a highly mobile, rapidly
6
+ deployed force that allowed the colonies to respond immediately to war threats,
7
+ hence the name._
8
+
9
+ ![Minuteman](http://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Minute_Man_Statue_Lexington_Massachusetts_cropped.jpg/220px-Minute_Man_Statue_Lexington_Massachusetts_cropped.jpg)
10
+
11
+ Fast analytics using Redis bitwise operations
12
+
13
+ ## Origin
14
+ Freenode - #cuba.rb - 2012/10/30 15:20 UYT
15
+
16
+ **conanbatt:** anyone here knows some good web app metrics tool ?
17
+
18
+ **conanbatt:** i use google analytics for the page itself, and its good, but for the webapp its really not useful
19
+
20
+ **tizoc: conanbatt:** [http://amix.dk/blog/post/19714]() you can port this (if an equivalent doesn't exist already)
21
+
22
+ **conanbatt:** the metrics link is excellent but its python and released 5 days ago lol
23
+
24
+ **elcuervo: tizoc:** the idea it's awesome
25
+
26
+ **elcuervo:** interesting...
27
+
28
+
29
+ ## Inspiration
30
+
31
+ * [http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/]()
32
+ * [http://amix.dk/blog/post/19714]()
33
+ * [http://en.wikipedia.org/wiki/Bit_array]()
34
+
35
+ ## Installation
36
+
37
+ ### Important!
38
+
39
+ Depends on Redis 2.6 for the `bitop` operation. You can install it using:
40
+
41
+ ```bash
42
+ brew install --devel redis
43
+ ```
44
+
45
+ or upgrading your current version:
46
+
47
+ ```bash
48
+ brew upgrade --devel redis
49
+ ```
50
+
51
+ And then install the gem
52
+
53
+ ```bash
54
+ gem install minuteman
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ```ruby
60
+ require "minuteman"
61
+
62
+ # Accepts an options hash that will be sent as is to Redis.new
63
+ analytics = Minuteman.new
64
+
65
+ # Mark an event for a given id
66
+ analytics.mark("login:successful", user.id)
67
+ analytics.mark("login:successful", other_user.id)
68
+
69
+ # Mark in bulk
70
+ analytics.mark("programming:love:ruby", User.where(favorite: "ruby").map(&:id))
71
+
72
+ # Fetch events for a given time
73
+ today_events = analytics.day("login:successful", Time.now.utc)
74
+
75
+ # This also exists
76
+ analytics.year("login:successful", Time.now.utc)
77
+ analytics.month("login:successful", Time.now.utc)
78
+ analytics.week("login:successful", Time.now.utc)
79
+ analytics.day("login:successful", Time.now.utc)
80
+ analytics.hour("login:successful", Time.now.utc)
81
+ analytics.minute("login:successful", Time.now.utc)
82
+
83
+ # Check event length
84
+ today_events.length
85
+ #=> 2
86
+
87
+ # Check for existance
88
+ today_events.include?(user.id)
89
+ #=> true
90
+ today_events.include?(admin.id)
91
+ #=> false
92
+
93
+ # Bulk check
94
+ today_events.include?(User.all.map(&:id))
95
+ #=> [true, true, false, false]
96
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new("spec") do |t|
4
+ t.pattern = "test/**/*_test.rb"
5
+ end
6
+
7
+ task :default => [:test]
8
+ task :test => [:spec]
data/lib/minuteman.rb ADDED
@@ -0,0 +1,84 @@
1
+ require "redis"
2
+ require "time"
3
+ require "minuteman/time_events"
4
+
5
+ # Until redis gem gets updated
6
+ class Redis
7
+ def bitop(operation, destkey, *keys)
8
+ synchronize do |client|
9
+ client.call([:bitop, operation, destkey] + keys)
10
+ end
11
+ end
12
+
13
+ def bitcount(key, start = 0, stop = -1)
14
+ synchronize do |client|
15
+ client.call([:bitcount, key, start, stop])
16
+ end
17
+ end
18
+ end
19
+
20
+ class Minuteman
21
+ attr_reader :redis
22
+
23
+ PREFIX = "minuteman"
24
+
25
+ # Public: Initializes Minuteman
26
+ #
27
+ # options - The hash to be sent to Redis.new
28
+ #
29
+ def initialize(options = {})
30
+ @redis = Redis.new(options)
31
+ end
32
+
33
+ # Public: Generates the methods to fech data
34
+ #
35
+ # event_name - The event name to be searched for
36
+ # date - A Time object used to do the search
37
+ #
38
+ %w[year month week day hour minute].each do |method_name|
39
+ define_method(method_name) do |event_name, date|
40
+ constructor = self.class.const_get(method_name.capitalize)
41
+ constructor.new(@redis, event_name, date)
42
+ end
43
+ end
44
+
45
+ # Public: Marks an id to a given event on a given time
46
+ #
47
+ # event_name - The event name to be searched for
48
+ # ids - The ids to be tracked
49
+ #
50
+ # Examples
51
+ #
52
+ # analytics = Minuteman.new
53
+ # analytics.mark("login", 1)
54
+ # analytics.mark("login", [2, 3, 4])
55
+ #
56
+ def mark(event_name, ids, time = Time.now.utc)
57
+ event_time = time.kind_of?(Time) ? time : Time.parse(time.to_s)
58
+ time_events = TimeEvents.start(redis, event_name, event_time)
59
+
60
+ @redis.multi do
61
+ time_events.each do |event|
62
+ Array(ids).each { |id| redis.setbit(event.key, id, 1) }
63
+ end
64
+ end
65
+ end
66
+
67
+ # Public: Resets the bit operation cache keys
68
+ #
69
+ def reset_operations_cache
70
+ prefix = [
71
+ PREFIX, Minuteman::BitOperations::BIT_OPERATION_PREFIX
72
+ ].join("_")
73
+
74
+ keys = @redis.keys([prefix, "*"].join("_"))
75
+ @redis.del(keys) if keys.any?
76
+ end
77
+
78
+ # Public: Resets all the used keys
79
+ #
80
+ def reset_all
81
+ keys = @redis.keys([PREFIX, "*"].join("_"))
82
+ @redis.del(keys) if keys.any?
83
+ end
84
+ end
@@ -0,0 +1,87 @@
1
+ class Minuteman
2
+ module BitOperations
3
+ BIT_OPERATION_PREFIX = "bitop"
4
+
5
+ # Public: Checks for the existance of ids on a given set
6
+ #
7
+ # ids - Array of ids
8
+ #
9
+ def include?(*ids)
10
+ result = ids.map { |id| redis.getbit(key, id) == 1 }
11
+ result.size == 1 ? result.first : result
12
+ end
13
+
14
+ # Public: Resets the current key
15
+ #
16
+ def reset
17
+ redis.rem(key)
18
+ end
19
+
20
+ # Public: Cheks for the amount of ids stored on the current key
21
+ #
22
+ def length
23
+ redis.bitcount(key)
24
+ end
25
+
26
+ # Public: Calculates the NOT of the current key
27
+ #
28
+ def -@
29
+ bit_operation("NOT", key)
30
+ end
31
+
32
+ # Public: Calculates the XOR against another timespan
33
+ #
34
+ # timespan: Another BitOperations enabled class
35
+ #
36
+ def ^(timespan)
37
+ bit_operation("XOR", [key, timespan.key])
38
+ end
39
+
40
+ # Public: Calculates the OR against another timespan
41
+ #
42
+ # timespan: Another BitOperations enabled class
43
+ #
44
+ def |(timespan)
45
+ bit_operation("OR", [key, timespan.key])
46
+ end
47
+
48
+ # Public: Calculates the AND against another timespan
49
+ #
50
+ # timespan: Another BitOperations enabled class
51
+ #
52
+ def &(timespan)
53
+ bit_operation("AND", [key, timespan.key])
54
+ end
55
+
56
+ private
57
+
58
+ # Private: The destination key for the operation
59
+ #
60
+ # type - The bitwise operation
61
+ # events - The events to permuted
62
+ #
63
+ def destination_key(type, events)
64
+ [
65
+ Minuteman::PREFIX,
66
+ BIT_OPERATION_PREFIX,
67
+ type,
68
+ events.join("-")
69
+ ].join("_")
70
+ end
71
+
72
+ # Private: Executes a bit operation
73
+ #
74
+ # type - The bitwise operation
75
+ # events - The events to permuted
76
+ #
77
+ def bit_operation(type, events)
78
+ key = destination_key(type, Array(events))
79
+ @redis.bitop(type, key, events)
80
+ BitOperation.new(@redis, key)
81
+ end
82
+ end
83
+
84
+ class BitOperation < Struct.new(:redis, :key)
85
+ include BitOperations
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ require "minuteman/time_spans"
2
+
3
+ class Minuteman
4
+ module TimeEvents
5
+ # Public: Helper to get all the time trakers ready
6
+ #
7
+ # redis - The Redis connection
8
+ # event_name - The event to be tracked
9
+ # date - A given Time object
10
+ #
11
+ def self.start(redis, event_name, time)
12
+ [Year, Month, Week, Day, Hour, Minute].map do |t|
13
+ t.new(redis, event_name, time)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ require "minuteman/bit_operations"
2
+
3
+ class Minuteman
4
+ class TimeSpan
5
+ include BitOperations
6
+
7
+ attr_reader :key, :redis
8
+
9
+ DATE_FORMAT = "%s-%02d-%02d"
10
+ TIME_FORMAT = "%02d:%02d"
11
+
12
+ # Public: Initializes the base TimeSpan class
13
+ #
14
+ # redis - The Redis connection
15
+ # event_name - The event to be tracked
16
+ # date - A given Time object
17
+ #
18
+ def initialize(redis, event_name, date)
19
+ @redis = redis
20
+ @key = build_key(event_name, time_format(date))
21
+ end
22
+
23
+ private
24
+
25
+ # Private: The redis key that's going to be used
26
+ #
27
+ # event_name - The event to be tracked
28
+ # date - A given Time object
29
+ #
30
+ def build_key(event_name, date)
31
+ [Minuteman::PREFIX, event_name, date.join("-")].join("_")
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ require "minuteman/time_span"
2
+ require "minuteman/time_spans/year"
3
+ require "minuteman/time_spans/month"
4
+ require "minuteman/time_spans/week"
5
+ require "minuteman/time_spans/day"
6
+ require "minuteman/time_spans/hour"
7
+ require "minuteman/time_spans/minute"
@@ -0,0 +1,13 @@
1
+ class Minuteman
2
+ class Day < TimeSpan
3
+ private
4
+
5
+ # Private: The format that's going the be used for the date part of the key
6
+ #
7
+ # date - A given Time object
8
+ #
9
+ def time_format(date)
10
+ [DATE_FORMAT % [date.year, date.month, date.day]]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class Minuteman
2
+ class Hour < TimeSpan
3
+ private
4
+
5
+ # Private: The format that's going the be used for the date part of the key
6
+ #
7
+ # date - A given Time object
8
+ #
9
+ def time_format(date)
10
+ full_date = DATE_FORMAT % [date.year, date.month, date.day]
11
+ time = TIME_FORMAT % [date.hour, 0]
12
+ [full_date + " " + time]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class Minuteman
2
+ class Minute < TimeSpan
3
+ private
4
+
5
+ # Private: The format that's going the be used for the date part of the key
6
+ #
7
+ # date - A given Time object
8
+ #
9
+ def time_format(date)
10
+ full_date = DATE_FORMAT % [date.year, date.month, date.day]
11
+ time = TIME_FORMAT % [date.hour, date.min]
12
+ [full_date + " " + time]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ class Minuteman
2
+ class Month < TimeSpan
3
+ private
4
+
5
+ # Private: The format that's going the be used for the date part of the key
6
+ #
7
+ # date - A given Time object
8
+ #
9
+ def time_format(date)
10
+ [date.year, "%02d" % date.month]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ class Minuteman
2
+ class Week < TimeSpan
3
+ private
4
+
5
+ # Private: The format that's going the be used for the date part of the key
6
+ #
7
+ # date - A given Time object
8
+ #
9
+ def time_format(date)
10
+ week = date.strftime("%W")
11
+ [date.year, "W" + week]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ class Minuteman
2
+ class Year < TimeSpan
3
+ private
4
+
5
+ # Private: The format that's going the be used for the date part of the key
6
+ #
7
+ # date - A given Time object
8
+ #
9
+ def time_format(date)
10
+ [date.year]
11
+ end
12
+ end
13
+ end
data/minuteman.gemspec ADDED
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "minuteman"
3
+ s.version = "0.0.1"
4
+ s.summary = "Bit Analytics"
5
+ s.description = "Fast and furious tracking system using Redis bitwise operations"
6
+ s.authors = ["elcuervo"]
7
+ s.email = ["yo@brunoaguirre.com"]
8
+ s.homepage = "http://github.com/elcuervo/minuteman"
9
+ s.files = `git ls-files`.split("\n")
10
+ s.test_files = `git ls-files test`.split("\n")
11
+
12
+ s.add_dependency("redis", "~> 3.0.2")
13
+
14
+ s.add_development_dependency("minitest", "~> 4.1.0")
15
+ end
@@ -0,0 +1,7 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+
3
+ require "bundler/setup"
4
+ require "minitest/spec"
5
+ require "minitest/pride"
6
+ require "minitest/autorun"
7
+ require "minuteman"
@@ -0,0 +1,92 @@
1
+ require_relative "../test_helper"
2
+
3
+ describe Minuteman do
4
+ before do
5
+ @analytics = Minuteman.new
6
+
7
+ today = Time.now.utc
8
+ last_month = today - (3600 * 24 * 30)
9
+ last_week = today - (3600 * 24 * 7)
10
+ last_minute = today - 61
11
+
12
+ @analytics.mark("login", 12)
13
+ @analytics.mark("login", [2, 42])
14
+ @analytics.mark("login", 2, last_week)
15
+ @analytics.mark("login:successful", 567, last_month)
16
+
17
+ @year_events = @analytics.year("login", today)
18
+ @week_events = @analytics.week("login", today)
19
+ @month_events = @analytics.month("login", today)
20
+ @day_events = @analytics.day("login", today)
21
+ @hour_events = @analytics.hour("login", today)
22
+ @minute_events = @analytics.minute("login", today)
23
+
24
+ @last_week_events = @analytics.week("login", last_week)
25
+ @last_minute_events = @analytics.minute("login", last_minute)
26
+ @last_month_events = @analytics.month("login:successful", last_month)
27
+ end
28
+
29
+ after { @analytics.reset_all }
30
+
31
+ it "should initialize correctly" do
32
+ assert @analytics.redis
33
+ end
34
+
35
+ it "should track an event on a time" do
36
+ assert_equal 3, @year_events.length
37
+ assert_equal 3, @week_events.length
38
+ assert_equal 1, @last_week_events.length
39
+ assert_equal 1, @last_month_events.length
40
+ assert_equal [true, true, false], @week_events.include?(12, 2, 1)
41
+
42
+ assert @year_events.include?(12)
43
+ assert @month_events.include?(12)
44
+ assert @day_events.include?(12)
45
+ assert @hour_events.include?(12)
46
+ assert @minute_events.include?(12)
47
+
48
+ assert @last_week_events.include?(2)
49
+ assert !@month_events.include?(5)
50
+ assert !@last_minute_events.include?(12)
51
+ assert @last_month_events.include?(567)
52
+ end
53
+
54
+ it "should accept the AND bitwise operations" do
55
+ and_operation = @week_events & @last_week_events
56
+
57
+ assert @week_events.include?(2)
58
+ assert @week_events.include?(12)
59
+
60
+ assert @last_week_events.include?(2)
61
+ assert !@last_week_events.include?(12)
62
+
63
+ assert_equal 1, and_operation.length
64
+
65
+ assert !and_operation.include?(12)
66
+ assert and_operation.include?(2)
67
+ end
68
+
69
+ it "should accept the OR bitwise operations" do
70
+ or_operation = @week_events | @last_week_events
71
+
72
+ assert @week_events.include?(2)
73
+ assert @last_week_events.include?(2)
74
+ assert !@last_week_events.include?(12)
75
+
76
+ assert_equal 3, or_operation.length
77
+
78
+ assert or_operation.include?(12)
79
+ assert or_operation.include?(2)
80
+ end
81
+
82
+ it "should accept the NOT bitwise operations" do
83
+ not_operation = -@week_events
84
+
85
+ assert @week_events.include?(2)
86
+ assert @week_events.include?(12)
87
+
88
+ assert !not_operation.include?(12)
89
+ assert !not_operation.include?(2)
90
+ end
91
+
92
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minuteman
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - elcuervo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.2
30
+ - !ruby/object:Gem::Dependency
31
+ name: minitest
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 4.1.0
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 4.1.0
46
+ description: Fast and furious tracking system using Redis bitwise operations
47
+ email:
48
+ - yo@brunoaguirre.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - Gemfile
54
+ - Gemfile.lock
55
+ - LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - lib/minuteman.rb
59
+ - lib/minuteman/bit_operations.rb
60
+ - lib/minuteman/time_events.rb
61
+ - lib/minuteman/time_span.rb
62
+ - lib/minuteman/time_spans.rb
63
+ - lib/minuteman/time_spans/day.rb
64
+ - lib/minuteman/time_spans/hour.rb
65
+ - lib/minuteman/time_spans/minute.rb
66
+ - lib/minuteman/time_spans/month.rb
67
+ - lib/minuteman/time_spans/week.rb
68
+ - lib/minuteman/time_spans/year.rb
69
+ - minuteman.gemspec
70
+ - test/test_helper.rb
71
+ - test/unit/minuteman_test.rb
72
+ homepage: http://github.com/elcuervo/minuteman
73
+ licenses: []
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 1.8.23
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Bit Analytics
96
+ test_files:
97
+ - test/test_helper.rb
98
+ - test/unit/minuteman_test.rb