save-your-dosh 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/Gemfile.lock +13 -0
- data/README.md +59 -0
- data/init.rb +1 -0
- data/lib/save-your-dosh.rb +4 -0
- data/lib/save_your_dosh.rb +28 -0
- data/lib/save_your_dosh.yml +13 -0
- data/lib/save_your_dosh/config.rb +36 -0
- data/lib/save_your_dosh/mangler.rb +47 -0
- data/lib/save_your_dosh/new_relic.rb +36 -0
- data/lib/tasks/save_your_dosh.rake +9 -0
- data/spec/lib/save_your_dosh/config_spec.rb +43 -0
- data/spec/lib/save_your_dosh/mangler_spec.rb +70 -0
- data/spec/lib/save_your_dosh/new_relic_spec.rb +50 -0
- data/spec/spec_helper.rb +27 -0
- metadata +60 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Save Your Dosh
|
2
|
+
|
3
|
+
This is a little gem for [heroku](http://heroku.com) that automatically scales
|
4
|
+
dynos in your heroku based app.
|
5
|
+
|
6
|
+
## Prerequisites
|
7
|
+
|
8
|
+
You obviously have to be on `heroku` and `rails`. You also need the New Relic RPM add-on
|
9
|
+
switched on. It doesn't matter whether you have a free or a pro account on new relic.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Hook it up as a rubygem in your `Gemfile`
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'save-your-dosh'
|
17
|
+
```
|
18
|
+
|
19
|
+
Make sure you have the following `ENV` vars in your heroku config
|
20
|
+
|
21
|
+
```
|
22
|
+
» heroku config
|
23
|
+
=== doshmosh Config Vars
|
24
|
+
.....
|
25
|
+
HEROKU_API_KEY: your-heroku-api-key
|
26
|
+
NEW_RELIC_API_KEY: your-new-relic-api-key
|
27
|
+
NEW_RELIC_APP_NAME: your-app-name-on-new-relic
|
28
|
+
NEW_RELIC_ID: your-account-id-on-new-relic
|
29
|
+
......
|
30
|
+
```
|
31
|
+
|
32
|
+
Once you've done with those, add the `rake save:your:dosh` task in heroku's scheduler
|
33
|
+
and set the minimal timeout of `10 mins`. (don't make it less than 6 mins, otherwise
|
34
|
+
new relic will kick your ass)
|
35
|
+
|
36
|
+
## Configuration
|
37
|
+
|
38
|
+
You can mangle with the settings by creating a file like that in your rails app `config/save-your-dosh.yaml`
|
39
|
+
|
40
|
+
```yml
|
41
|
+
dynos:
|
42
|
+
min: 1
|
43
|
+
max: 5
|
44
|
+
threshold: 50 # % of system busyness when we kick in/out a dyno
|
45
|
+
```
|
46
|
+
|
47
|
+
|
48
|
+
## How It Works
|
49
|
+
|
50
|
+
It's pretty simple, every time you kick the `rake save:your:dosh` task via cron or scheduler,
|
51
|
+
it will make a request to the new relic servers for the data on your application business. If
|
52
|
+
it goes over the threshold, it will increase the amount of dynos until reaches the max number
|
53
|
+
from the config. Otherwise it will try to switch dynos off until reaches the minimal amount.
|
54
|
+
|
55
|
+
|
56
|
+
## License
|
57
|
+
|
58
|
+
All the code in this package released under the terms of the MIT license
|
59
|
+
Copyright (C) 2012 Nikolay Nemshilov
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'save_your_dosh'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#
|
2
|
+
# The namespace
|
3
|
+
#
|
4
|
+
module SaveYourDosh
|
5
|
+
extend self
|
6
|
+
|
7
|
+
VERSION = '0.0.0'
|
8
|
+
|
9
|
+
autoload :Config, 'save_your_dosh/config.rb'
|
10
|
+
autoload :Mangler, 'save_your_dosh/mangler.rb'
|
11
|
+
autoload :NewRelic, 'save_your_dosh/new_relic.rb'
|
12
|
+
|
13
|
+
|
14
|
+
def config
|
15
|
+
@config ||= Config.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def mangle!
|
19
|
+
mangler ||= Mangler.new
|
20
|
+
mangler.mangle_dynos!
|
21
|
+
mangler.mangle_workers!
|
22
|
+
end
|
23
|
+
|
24
|
+
Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |ext|
|
25
|
+
load ext
|
26
|
+
} if defined?(Rake)
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# The config object
|
3
|
+
#
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
class SaveYourDosh::Config
|
7
|
+
DEFAULTS = File.dirname(__FILE__) + "/../save_your_dosh.yml"
|
8
|
+
KEYS = %w{ new_relic heroku dynos workers notify interval }
|
9
|
+
|
10
|
+
KEYS.each{ |key| attr_accessor key }
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@new_relic = {
|
14
|
+
'acc_id' => ENV['NEW_RELIC_ID'],
|
15
|
+
'app_id' => ENV['NEW_RELIC_APP_NAME'],
|
16
|
+
'api_key' => ENV['NEW_RELIC_API_KEY']
|
17
|
+
}
|
18
|
+
|
19
|
+
@heroku = {
|
20
|
+
'app_id' => ENV['APP_NAME'],
|
21
|
+
'api_key' => ENV['HEROKU_API_KEY']
|
22
|
+
}
|
23
|
+
|
24
|
+
read DEFAULTS
|
25
|
+
end
|
26
|
+
|
27
|
+
def read(file)
|
28
|
+
config = YAML.load_file(file)
|
29
|
+
|
30
|
+
KEYS.each do |key|
|
31
|
+
instance_variable_set "@#{key}", config[key] if config.has_key?(key)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#
|
2
|
+
# Mangles with the actual heroku settings
|
3
|
+
#
|
4
|
+
require 'heroku-api'
|
5
|
+
|
6
|
+
class SaveYourDosh::Mangler
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@config = SaveYourDosh.config
|
10
|
+
@heroku = Heroku::API.new(api_key: @config.heroku['api_key'])
|
11
|
+
end
|
12
|
+
|
13
|
+
def mangle_dynos!
|
14
|
+
mangle! :dynos do |qty|
|
15
|
+
load = SaveYourDosh::NewRelic.get_dynos_load
|
16
|
+
qty + (load > @config.dynos['threshold'] ? 1 : -1)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def mangle_workers!
|
21
|
+
mangle! :workers do |qty|
|
22
|
+
qty
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# a little wrapper to avoid problems with setting/getting
|
29
|
+
# a wrong thing
|
30
|
+
def mangle!(what, &block)
|
31
|
+
qty = @heroku.get_app(@config.heroku['app_id']).body[what.to_s]
|
32
|
+
new_qty = yield(qty)
|
33
|
+
|
34
|
+
min_qty = @config.send(what)['min']
|
35
|
+
max_qty = @config.send(what)['max']
|
36
|
+
|
37
|
+
new_qty = min_qty if new_qty < min_qty
|
38
|
+
new_qty = max_qty if new_qty > max_qty
|
39
|
+
|
40
|
+
if new_qty != qty
|
41
|
+
Rails.logger.info "SaveYourDosh: mangling with the #{what}, #{qty} -> #{new_qty}" if defined? Rails
|
42
|
+
|
43
|
+
@heroku.send("put_#{what}", @config.heroku['app_id'], new_qty)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# A little proxy for the new-relic API
|
3
|
+
#
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class SaveYourDosh::NewRelic
|
7
|
+
|
8
|
+
DYNOS_LOAD_CMD = %Q{
|
9
|
+
curl --silent -H "x-api-key: %{api_key}" \
|
10
|
+
-d "metrics[]=Instance/Busy" \
|
11
|
+
-d "field=busy_percent" \
|
12
|
+
-d "begin=%{begin_time}" \
|
13
|
+
-d "end=%{end_time}" \
|
14
|
+
-d "summary=1" \
|
15
|
+
-d "app=%{app_id}" \
|
16
|
+
https://api.newrelic.com/api/v1/accounts/%{acc_id}/metrics/data.json
|
17
|
+
}.strip
|
18
|
+
|
19
|
+
def self.get_dynos_load
|
20
|
+
conf = SaveYourDosh.config
|
21
|
+
|
22
|
+
data = DYNOS_LOAD_CMD % {
|
23
|
+
app_id: conf.new_relic['app_id'],
|
24
|
+
acc_id: conf.new_relic['acc_id'],
|
25
|
+
api_key: conf.new_relic['api_key'],
|
26
|
+
begin_time: Time.now - conf.interval * 60,
|
27
|
+
end_time: Time.now
|
28
|
+
}
|
29
|
+
|
30
|
+
JSON.parse(`#{data}`)[0]["busy_percent"]
|
31
|
+
|
32
|
+
rescue JSON::ParserError, NoMethodError
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SaveYourDosh::Config do
|
4
|
+
describe "defaults" do
|
5
|
+
before do
|
6
|
+
ENV['NEW_RELIC_ID'] = 'acc-id'
|
7
|
+
ENV['NEW_RELIC_APP_NAME'] = '12345'
|
8
|
+
ENV['NEW_RELIC_API_KEY'] = 'api-key'
|
9
|
+
|
10
|
+
ENV['APP_NAME'] = 'app-name'
|
11
|
+
ENV['HEROKU_API_KEY'] = 'heroku-api-key'
|
12
|
+
|
13
|
+
@config = SaveYourDosh::Config.new
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have default dynos data" do
|
17
|
+
@config.dynos.should == {
|
18
|
+
"min" => 1, "max" => 5, "threshold" => 50
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should have default workders data" do
|
23
|
+
@config.workers.should == {
|
24
|
+
"min" => 0, "max" => 5, "jobs" => 20
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should read the new_relic settings from the ENV hash" do
|
29
|
+
@config.new_relic.should == {
|
30
|
+
'acc_id' => ENV['NEW_RELIC_ID'],
|
31
|
+
'app_id' => ENV['NEW_RELIC_APP_NAME'],
|
32
|
+
'api_key' => ENV['NEW_RELIC_API_KEY']
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should assign the heroku credentials from the ENV data" do
|
37
|
+
@config.heroku.should == {
|
38
|
+
'app_id' => ENV['APP_NAME'],
|
39
|
+
'api_key' => ENV['HEROKU_API_KEY']
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SaveYourDosh::Mangler do
|
4
|
+
before do
|
5
|
+
@config = SaveYourDosh.config
|
6
|
+
@config.heroku = {
|
7
|
+
'app_id' => 'my-app',
|
8
|
+
'login' => 'login',
|
9
|
+
'password' => 'my-password'
|
10
|
+
}
|
11
|
+
|
12
|
+
@heroku = Heroku::API.new(api_key: 'boo hoo')
|
13
|
+
|
14
|
+
Heroku::API.should_receive(:new).
|
15
|
+
with(api_key: @config.heroku['api_key']).
|
16
|
+
and_return(@heroku)
|
17
|
+
|
18
|
+
@mangler = SaveYourDosh::Mangler.new
|
19
|
+
end
|
20
|
+
|
21
|
+
describe ".mangle_dynos!" do
|
22
|
+
before do
|
23
|
+
@heroku.should_receive(:get_app).
|
24
|
+
with(@config.heroku['app_id']).
|
25
|
+
and_return(Struct.new(:body).new({'dynos' => 2, 'workers' => 1}))
|
26
|
+
|
27
|
+
SaveYourDosh::NewRelic.should_receive(:get_dynos_load).
|
28
|
+
and_return(40)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should try to add dynos if we're getting over the threshold" do
|
32
|
+
@config.dynos['threshold'] = 30
|
33
|
+
|
34
|
+
@heroku.should_receive(:put_dynos).
|
35
|
+
with(@config.heroku['app_id'], 3)
|
36
|
+
|
37
|
+
@mangler.mangle_dynos!
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should try to remove dynos if we're below the threshold" do
|
41
|
+
@config.dynos['threshold'] = 50
|
42
|
+
|
43
|
+
@heroku.should_receive(:put_dynos).
|
44
|
+
with(@config.heroku['app_id'], 1)
|
45
|
+
|
46
|
+
@mangler.mangle_dynos!
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not go below minimal dynos amount" do
|
50
|
+
@config.dynos['threshold'] = 50
|
51
|
+
@config.dynos['min'] = 2
|
52
|
+
@config.dynos['max'] = 2
|
53
|
+
|
54
|
+
@heroku.should_not_receive(:put_dynos)
|
55
|
+
|
56
|
+
@mangler.mangle_dynos!
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should not go above dynos amount" do
|
60
|
+
@config.dynos['threshold'] = 30
|
61
|
+
@config.dynos['min'] = 1
|
62
|
+
@config.dynos['max'] = 2
|
63
|
+
|
64
|
+
@heroku.should_not_receive(:put_dynos)
|
65
|
+
|
66
|
+
@mangler.mangle_dynos!
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SaveYourDosh::NewRelic do
|
4
|
+
before do
|
5
|
+
@config = SaveYourDosh.config
|
6
|
+
@config.new_relic = {
|
7
|
+
'acc_id' => 'acc-id',
|
8
|
+
'app_id' => 'app-name',
|
9
|
+
'api_key' => 'api-key'
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
describe ".get_dynos_load" do
|
14
|
+
it "should make a request to the server for the data" do
|
15
|
+
SaveYourDosh::NewRelic.should_receive(:`).
|
16
|
+
|
17
|
+
with(%Q{
|
18
|
+
curl --silent -H "x-api-key: #{@config.new_relic['api_key']}" \
|
19
|
+
-d "metrics[]=Instance/Busy" \
|
20
|
+
-d "field=busy_percent" \
|
21
|
+
-d "begin=#{Time.now - @config.interval * 60}" \
|
22
|
+
-d "end=#{Time.now}" \
|
23
|
+
-d "summary=1" \
|
24
|
+
-d "app=#{@config.new_relic['app_id']}" \
|
25
|
+
https://api.newrelic.com/api/v1/accounts/#{@config.new_relic['acc_id']}/metrics/data.json
|
26
|
+
}.strip).
|
27
|
+
|
28
|
+
and_return %Q{
|
29
|
+
[{"name":"Instance/Busy","app":"doshmosh","agent_id":513715,"busy_percent":2.299166620165731}]
|
30
|
+
}
|
31
|
+
|
32
|
+
SaveYourDosh::NewRelic.get_dynos_load.should == 2.299166620165731
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should return nil if the server returns not JSON" do
|
36
|
+
SaveYourDosh::NewRelic.should_receive(:`).
|
37
|
+
and_return("Fuck you buddy")
|
38
|
+
|
39
|
+
SaveYourDosh::NewRelic.get_dynos_load.should == nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should return nil if the server returns wrong JSON" do
|
43
|
+
SaveYourDosh::NewRelic.should_receive(:`).
|
44
|
+
and_return('{"fuck": "you buddy"}')
|
45
|
+
|
46
|
+
SaveYourDosh::NewRelic.get_dynos_load.should == nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'heroku-api'
|
3
|
+
|
4
|
+
require File.dirname(__FILE__) + "/../lib/save_your_dosh.rb"
|
5
|
+
|
6
|
+
# mocking the Kernel::` calls in the new-relic API proxy
|
7
|
+
class SaveYourDosh::NewRelic
|
8
|
+
def self.`(str)
|
9
|
+
''
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# disabling the Horoku::Client real interactions
|
14
|
+
class Heroku::API
|
15
|
+
def initialize(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_app(name)
|
19
|
+
Struct.new(:body).new({'dynos' => 1, 'workers' => 1})
|
20
|
+
end
|
21
|
+
|
22
|
+
def put_dynos(name, qty)
|
23
|
+
end
|
24
|
+
|
25
|
+
def put_workers(name, qty)
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: save-your-dosh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nikolay Nemshilov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-20 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: This gem can automatically scale the dynos amount on your heroku app
|
15
|
+
depending on your system busyness
|
16
|
+
email: nemshilov@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/save-your-dosh.rb
|
22
|
+
- lib/save_your_dosh/config.rb
|
23
|
+
- lib/save_your_dosh/mangler.rb
|
24
|
+
- lib/save_your_dosh/new_relic.rb
|
25
|
+
- lib/save_your_dosh.rb
|
26
|
+
- lib/save_your_dosh.yml
|
27
|
+
- lib/tasks/save_your_dosh.rake
|
28
|
+
- spec/lib/save_your_dosh/config_spec.rb
|
29
|
+
- spec/lib/save_your_dosh/mangler_spec.rb
|
30
|
+
- spec/lib/save_your_dosh/new_relic_spec.rb
|
31
|
+
- spec/spec_helper.rb
|
32
|
+
- README.md
|
33
|
+
- init.rb
|
34
|
+
- Gemfile
|
35
|
+
- Gemfile.lock
|
36
|
+
homepage: http://github.com/MadRabbit/save-your-dosh
|
37
|
+
licenses: []
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.8.23
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Heroku dynos auto-scaling thing
|
60
|
+
test_files: []
|