merry_go_round 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ bundler_args: --without development
3
+ rvm:
4
+ - 1.9.3
@@ -0,0 +1,19 @@
1
+ ## Submitting a Pull Request
2
+
3
+ 1. [Fork the repository.][fork]
4
+ 2. [Create a topic branch.][branch]
5
+ 3. Add tests for your unimplemented feature or bug fix.
6
+ 4. Run `bundle exec rake`. If your tests pass, return to step 3.
7
+ 5. Implement your feature or bug fix.
8
+ 6. Run `bundle exec rake`. If your tests fail, return to step 5.
9
+ 7. Run `open coverage/index.html`. If your changes are not completely covered
10
+ by your tests, return to step 3.
11
+ 8. Add documentation for your feature or bug fix.
12
+ 9. Run `bundle exec rake doc`. If your changes are not 100% documented, go
13
+ back to step 8.
14
+ 10. Add, commit, and push your changes.
15
+ 11. [Submit a pull request.][pr]
16
+
17
+ [fork]: http://help.github.com/fork-a-repo/
18
+ [branch]: http://learn.github.com/p/branching.html
19
+ [pr]: http://help.github.com/send-pull-requests/
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Gem dependencies
4
+ gemspec
5
+
6
+ gem 'rake', group: [:development, :test]
7
+
8
+ # Development dependencies
9
+ group :development do
10
+ gem 'yard'
11
+ gem 'redcarpet'
12
+ end
13
+
14
+ # Testing dependencies
15
+ group :test do
16
+ gem 'minitest'
17
+ gem 'minitest-wscolor'
18
+ gem 'simplecov', require: false
19
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Seesaw Decisions Corporation
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
8
+ task default: :test
9
+
10
+ begin
11
+ require 'yard'
12
+ YARD::Rake::YardocTask.new(:doc) do |task|
13
+ task.files = ['Readme.markdown', 'LICENSE', 'lib/**/*.rb']
14
+ task.options = [
15
+ '--output-dir', 'doc',
16
+ '--markup', 'markdown',
17
+ ]
18
+ end
19
+ rescue LoadError
20
+ end
data/Readme.markdown ADDED
@@ -0,0 +1,53 @@
1
+ # Merry Go Round
2
+
3
+ *Note: This is a work in progress and it not production ready. If want to help, we'll love you forever.*
4
+
5
+ Simple data-warehousing with Redis and ActiveRecord.
6
+
7
+ All data is stored in Redis until it is aggregated. Once it is aggregated, it is stored in ActiveRecord. Merry Go Round handles all of the collecting, aggregating, and querying for you. All you have to do it put your data in and get your data out. All of the hard work of warehousing is done for you automatically.
8
+
9
+ Redis is just used as a temporary store until the data can be warehoused in ActiveRecord. You can blow away Redis at anytime and only lose data since the last aggregation.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ``` ruby
16
+ gem 'merry_go_round'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install merry_go_round
26
+
27
+ ## Usage
28
+
29
+ You'll need to add the migrations to your app. Simply run:
30
+
31
+ $ merry install
32
+
33
+ Merry Go Round will automatically detect most Redis servers. If you need to configure it, add an initializer with:
34
+
35
+ ``` ruby
36
+ MerryGoRound.configure do |config|
37
+ config.redis = { url: 'redis://redis.example.com:7372/12', namespace: 'mynamespace' }
38
+ end
39
+ ```
40
+
41
+ ### Aggregating
42
+
43
+ To run the aggregator, run:
44
+
45
+ $ merry go
46
+
47
+ By default, it will aggregate everything at 1 minute, 1 hour, 1 day, 1 week, 1 month, and 1 year. All you need to do is run the command and it will automatically aggregate for each time period. If that time period is not complete, it will not aggregate them (i.e. if you have been running Merry Go Round for 5 days, it won't generate week and higher aggregations).
48
+
49
+ It is recommened to run the aggregator via cron (or [Heroku Scheduler](https://devcenter.heroku.com/articles/scheduler)) every few minutes. Since it uses ActiveRecord to store the aggregations, it will have to load your environment.
50
+
51
+ ## Contributing
52
+
53
+ See the [contributing guide](Contributing.markdown).
data/bin/merry ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'merry_go_round/cli'
6
+
7
+ MerryGoRound::Cli.start
@@ -0,0 +1,16 @@
1
+ require 'active_record'
2
+
3
+ module MerryGoRound
4
+ # Aggregation is the ActiveRecord store for aggregated data points. You should
5
+ # never create aggregations directly unless you know what you're doing.
6
+ class Aggregation < ActiveRecord::Base
7
+ attr_accessible :key, :value, :timestamp, :granularity, :parent_id
8
+
9
+ belongs_to :aggregation, foreign_key: :parent_id
10
+ has_many :aggregations, foreign_key: :parent_id
11
+
12
+ def self.values_for_key(key, granularity = 60)
13
+ where('key = ? AND granularity = ?', granularity)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,69 @@
1
+ module MerryGoRound
2
+ class Aggregator
3
+ GRANULARITIES = [:minute, :hour, :day, :week, :year] # :month, :quarter
4
+
5
+ def aggregate!
6
+ from_redis
7
+ existing_slivers
8
+ end
9
+
10
+ private
11
+
12
+ def redis
13
+ MerryGoRound.redis
14
+ end
15
+
16
+ # Aggregate stuff since the last aggregation in Redis
17
+ def from_redis
18
+ # Aggregate to the first granularity
19
+ redis_granularity = GRANULARITIES.first
20
+
21
+ # Get entry keys from Redis
22
+ redis_keys = redis.keys 'entry-*'
23
+
24
+ # Loop through the Redis keys
25
+ redis_keys.each do |redis_key|
26
+ # Extract the timestamp
27
+ timestamp = Time.utc(redis_key.sub('entry-', '').to_i)
28
+
29
+ # Make sure this timestamp hasn't already been aggregated
30
+ redis.multi do
31
+ # Get the hash of Merry Go Round keys and values
32
+ hash = redis.hgetall redis_key
33
+
34
+ # Loop through each key/value in the hash
35
+ hash.each do |key, value|
36
+ Sliver.create(key: key, value: value, timestamp: timestamp, granularity: redis_granularity)
37
+ end
38
+
39
+ # Delete the key
40
+ redis.del redis_key
41
+ end
42
+ end
43
+ end
44
+
45
+ # Aggregate existing slivers (i.e. hour -> day)
46
+ def existing_slivers
47
+ # Loop through all of the granularities except for the first one
48
+ grans = GRANULARITIES.slice(1, GRANULARITIES.length - 1)
49
+ grans.each_with_index do |granularity, index|
50
+ # Get the previous granularity
51
+ prev_gran = GRANULARITIES[index - 1]
52
+
53
+ # Get slivers with the previous granularity
54
+ Sliver.where(granularity: prev_gran, )
55
+ end
56
+ end
57
+
58
+ def self.seconds(granularity)
59
+ map = {
60
+ minute: 60,
61
+ hour: 3600,
62
+ day: 86400,
63
+ week: 604800,
64
+ year: 31536000
65
+ }
66
+ map[granularity]
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,17 @@
1
+ require 'thor'
2
+ require 'merry_go_round'
3
+
4
+ module MerryGoRound
5
+ class Cli < Thor
6
+ desc 'go', 'Aggregate collected data in Redis into ActiveRecord Slivers.'
7
+ def go
8
+ MerryGoRound.aggregate!
9
+ end
10
+
11
+ desc 'install', 'Install the Merry Go Round migration'
12
+ def install
13
+ system 'rails generate merry_go_round:install'
14
+ puts "\n\nThe migratation has been installed. Don't forget to run `rake db:migrate`.\n"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module MerryGoRound
2
+ class Entry
3
+ def initialize(key, increment = 1, timestamp = Time.now)
4
+ @key = key
5
+ @increment = increment
6
+
7
+ # Note, storing in the past isn't supported. If you set this to something the
8
+ # aggregator has already run, the results are undefined.
9
+ @timestamp = timestamp
10
+ end
11
+
12
+ def record!
13
+ # Normalize time. Drop seconds and convert to UTC
14
+ time = Time.at(@timestamp.to_i - @timestamp.sec).utc.to_i
15
+
16
+ # Store in Redis
17
+ redis.hincrby "entry-#{time.to_s}", @key, @increment
18
+ end
19
+
20
+ private
21
+
22
+ def redis
23
+ MerryGoRound.redis
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ module MerryGoRound
2
+ class Railtie < Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module MerryGoRound
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,53 @@
1
+ require 'merry_go_round/version'
2
+ require 'merry_go_round/aggregator'
3
+ require 'merry_go_round/entry'
4
+ require 'merry_go_round/aggregation'
5
+ require 'merry_go_round/railtie' if defined? Rails
6
+
7
+ require 'redis'
8
+ require 'redis/namespace'
9
+
10
+ module MerryGoRound
11
+ def self.configure
12
+ yield self
13
+ end
14
+
15
+ def self.aggregate!
16
+ Aggregator.new.aggregate!
17
+ end
18
+
19
+ def self.record(*args)
20
+ Entry.new(*args).record!
21
+ end
22
+
23
+ def self.redis
24
+ # Set redis to nothing make the setter run and setup a default if it's nothing
25
+ self.redis = {} unless defined? @@redis
26
+
27
+ # Return the namespaced Redis instance
28
+ @@redis
29
+ end
30
+
31
+ def self.redis=(options = {})
32
+ client = nil
33
+ if options.is_a?(Redis)
34
+ client = options
35
+ else
36
+ url = options[:url] || determine_redis_provider || 'redis://localhost:6379/0'
37
+ driver = options[:driver] || 'ruby'
38
+ namespace = options[:namespace] || 'merry_go_round'
39
+
40
+ client = Redis.connect(url: url, driver: driver)
41
+ end
42
+
43
+ @@redis = Redis::Namespace.new(namespace, redis: client)
44
+ end
45
+
46
+ private
47
+
48
+ def self.determine_redis_provider
49
+ return ENV['REDISTOGO_URL'] if ENV['REDISTOGO_URL']
50
+ provider = ENV['REDIS_PROVIDER'] || 'REDIS_URL'
51
+ ENV[provider]
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ Description:
2
+ The merry_go_round:install generator creates the necessary migrations to
3
+ use Merry Go Round.
@@ -0,0 +1,23 @@
1
+ module MerryGoRound
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path('../templates', __FILE__)
7
+ desc 'Add Merry Go Round migrations'
8
+
9
+ def self.next_migration_number(path)
10
+ unless @prev_migration_nr
11
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
12
+ else
13
+ @prev_migration_nr += 1
14
+ end
15
+ @prev_migration_nr.to_s
16
+ end
17
+
18
+ def copy_migrations
19
+ migration_template 'add_aggregations.rb', 'db/migrate/add_merry_go_round_aggregations.rb'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ class AddMerryGoRoundAggregations < ActiveRecord::Migration
2
+ def change
3
+ create_table :merry_go_round_aggregations do |t|
4
+ # The name of whatever you're tracking
5
+ t.string :key, null: false
6
+
7
+ # The integer value of your count
8
+ t.integer :value, null: false, default: 0
9
+
10
+ # When it was counted rounded to a normalized time
11
+ t.datetime :timestamp, null: false
12
+
13
+ # A string representing the granularity (minute, hour, day, week, month, year)
14
+ t.string :granularity, null: false
15
+
16
+ # Foreign key for tree structure
17
+ t.integer :parent_id
18
+ end
19
+
20
+ add_index :merry_go_round_aggregations, [:key, :granularity, :timestamp, :parent_id], name: 'merry_go_round_aggregations_kgtp'
21
+ add_index :merry_go_round_aggregations, [:parent_id, :granularity, :timestamp], name: 'merry_go_round_aggregations_pgt'
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'merry_go_round/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'merry_go_round'
8
+ gem.version = MerryGoRound::VERSION
9
+ gem.authors = ['Sam Soffes']
10
+ gem.email = ['sam@soff.es']
11
+ gem.description = 'Simple data-warehousing.'
12
+ gem.summary = 'Simple data-warehousing with Redis and PostgreSQL.'
13
+ gem.homepage = 'https://github.com/seesawco/merry_go_round'
14
+ gem.license = 'MIT'
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ['lib']
20
+
21
+ gem.required_ruby_version = '>= 1.9.2'
22
+ gem.add_dependency 'activerecord', '>= 3.0.0'
23
+ gem.add_dependency 'redis', '>= 3.0.0'
24
+ gem.add_dependency 'redis-namespace'
25
+ gem.add_dependency 'thor'
26
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ class MerryGoRoundTest < MerryGoRound::TestCase
4
+ def test_configure
5
+ redis = Redis.new
6
+ MerryGoRound.configure do |config|
7
+ config.redis = redis
8
+ end
9
+
10
+ # It returns a namespaced Redis, so call `.redis` again on it
11
+ assert_equal redis, MerryGoRound.redis.redis
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.require :test
4
+
5
+ require 'simplecov'
6
+ SimpleCov.start
7
+
8
+ require 'minitest/autorun'
9
+ require 'merry_go_round'
10
+
11
+ # Support files
12
+ Dir["#{File.expand_path(File.dirname(__FILE__))}/support/*.rb"].each do |file|
13
+ require file
14
+ end
15
+
16
+ class MerryGoRound::TestCase < MiniTest::Unit::TestCase
17
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: merry_go_round
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sam Soffes
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
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.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: redis
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 3.0.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 3.0.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: redis-namespace
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: thor
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Simple data-warehousing.
79
+ email:
80
+ - sam@soff.es
81
+ executables:
82
+ - merry
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - .travis.yml
88
+ - Contributing.markdown
89
+ - Gemfile
90
+ - LICENSE
91
+ - Rakefile
92
+ - Readme.markdown
93
+ - bin/merry
94
+ - lib/merry_go_round.rb
95
+ - lib/merry_go_round/aggregation.rb
96
+ - lib/merry_go_round/aggregator.rb
97
+ - lib/merry_go_round/cli.rb
98
+ - lib/merry_go_round/entry.rb
99
+ - lib/merry_go_round/railtie.rb
100
+ - lib/merry_go_round/version.rb
101
+ - lib/rails/generators/merry_go_round/install/USAGE
102
+ - lib/rails/generators/merry_go_round/install/install_generator.rb
103
+ - lib/rails/generators/merry_go_round/install/templates/add_aggregations.rb
104
+ - merry_go_round.gemspec
105
+ - test/merry_go_round_test.rb
106
+ - test/test_helper.rb
107
+ homepage: https://github.com/seesawco/merry_go_round
108
+ licenses:
109
+ - MIT
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: 1.9.2
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ segments:
127
+ - 0
128
+ hash: 3503042711735793145
129
+ requirements: []
130
+ rubyforge_project:
131
+ rubygems_version: 1.8.23
132
+ signing_key:
133
+ specification_version: 3
134
+ summary: Simple data-warehousing with Redis and PostgreSQL.
135
+ test_files:
136
+ - test/merry_go_round_test.rb
137
+ - test/test_helper.rb
138
+ has_rdoc: