minuteman 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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