unleash 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +143 -0
- data/Rakefile +6 -0
- data/TODO.md +37 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/simple.rb +53 -0
- data/lib/unleash.rb +28 -0
- data/lib/unleash/activation_strategy.rb +17 -0
- data/lib/unleash/client.rb +74 -0
- data/lib/unleash/configuration.rb +68 -0
- data/lib/unleash/context.rb +18 -0
- data/lib/unleash/feature_toggle.rb +67 -0
- data/lib/unleash/metrics.rb +25 -0
- data/lib/unleash/metrics_reporter.rb +61 -0
- data/lib/unleash/scheduled_executor.rb +35 -0
- data/lib/unleash/strategy/application_hostname.rb +24 -0
- data/lib/unleash/strategy/base.rb +16 -0
- data/lib/unleash/strategy/default.rb +13 -0
- data/lib/unleash/strategy/gradual_rollout_random.rb +26 -0
- data/lib/unleash/strategy/gradual_rollout_sessionid.rb +20 -0
- data/lib/unleash/strategy/gradual_rollout_userid.rb +20 -0
- data/lib/unleash/strategy/remote_address.rb +18 -0
- data/lib/unleash/strategy/unknown.rb +13 -0
- data/lib/unleash/strategy/user_with_id.rb +18 -0
- data/lib/unleash/strategy/util.rb +16 -0
- data/lib/unleash/toggle_fetcher.rb +144 -0
- data/lib/unleash/version.rb +3 -0
- data/unleash-client.gemspec +30 -0
- metadata +135 -0
data/examples/simple.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'unleash'
|
4
|
+
require 'unleash/context'
|
5
|
+
|
6
|
+
puts ">> START simple.rb"
|
7
|
+
|
8
|
+
# Unleash.configure do |config|
|
9
|
+
# config.url = 'http://unleash.herokuapp.com/api'
|
10
|
+
# config.app_name = 'simple-test'
|
11
|
+
# config.refresh_interval = 2
|
12
|
+
# config.metrics_interval = 2
|
13
|
+
# config.retry_limit = 2
|
14
|
+
# end
|
15
|
+
# @unleash = Unleash::Client.new
|
16
|
+
|
17
|
+
# or:
|
18
|
+
|
19
|
+
@unleash = Unleash::Client.new( url: 'http://unleash.herokuapp.com/api', app_name: 'simple-test',
|
20
|
+
instance_id: 'local-test-cli',
|
21
|
+
refresh_interval: 2,
|
22
|
+
metrics_interval: 2,
|
23
|
+
retry_limit: 2,
|
24
|
+
log_level: Logger::DEBUG,
|
25
|
+
)
|
26
|
+
|
27
|
+
# feature_name = "AwesomeFeature"
|
28
|
+
feature_name = "4343443"
|
29
|
+
unleash_context = Unleash::Context.new
|
30
|
+
unleash_context.user_id = 123
|
31
|
+
|
32
|
+
sleep 1
|
33
|
+
3.times do
|
34
|
+
if @unleash.is_enabled?(feature_name, unleash_context)
|
35
|
+
puts "> #{feature_name} is enabled"
|
36
|
+
else
|
37
|
+
puts "> #{feature_name} is not enabled"
|
38
|
+
end
|
39
|
+
sleep 1
|
40
|
+
puts "---"
|
41
|
+
puts ""
|
42
|
+
puts ""
|
43
|
+
end
|
44
|
+
|
45
|
+
sleep 3
|
46
|
+
feature_name = "foobar"
|
47
|
+
if @unleash.is_enabled?(feature_name, unleash_context, true)
|
48
|
+
puts "> #{feature_name} is enabled"
|
49
|
+
else
|
50
|
+
puts "> #{feature_name} is not enabled"
|
51
|
+
end
|
52
|
+
|
53
|
+
puts ">> END simple.rb"
|
data/lib/unleash.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'unleash/version'
|
2
|
+
require 'unleash/configuration'
|
3
|
+
require 'unleash/context'
|
4
|
+
require 'unleash/client'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
module Unleash
|
8
|
+
TIME_RESOLUTION = 3
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.initialize
|
15
|
+
self.toggles = []
|
16
|
+
self.toggle_metrics = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Support for configuration via yield:
|
20
|
+
def self.configure()
|
21
|
+
self.configuration ||= Unleash::Configuration.new
|
22
|
+
yield(configuration)
|
23
|
+
|
24
|
+
self.configuration.validate!
|
25
|
+
self.configuration.refresh_backup_file!
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
class ActivationStrategy
|
5
|
+
attr_accessor :name, :params
|
6
|
+
|
7
|
+
def initialize(name, params = {})
|
8
|
+
self.name = name
|
9
|
+
if params.is_a?(Hash)
|
10
|
+
self.params = params
|
11
|
+
else
|
12
|
+
Unleash.logger.warning "Invalid params provided for ActivationStrategy #{params}"
|
13
|
+
self.params = {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'unleash/configuration'
|
2
|
+
require 'unleash/toggle_fetcher'
|
3
|
+
require 'unleash/metrics_reporter'
|
4
|
+
require 'unleash/feature_toggle'
|
5
|
+
require 'logger'
|
6
|
+
require 'time'
|
7
|
+
|
8
|
+
module Unleash
|
9
|
+
|
10
|
+
class Client
|
11
|
+
def initialize(*opts)
|
12
|
+
Unleash.configuration ||= Unleash::Configuration.new(*opts)
|
13
|
+
Unleash.configuration.validate!
|
14
|
+
|
15
|
+
Unleash.logger = Unleash.configuration.logger
|
16
|
+
Unleash.logger.level = Unleash.configuration.log_level
|
17
|
+
|
18
|
+
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
|
19
|
+
|
20
|
+
unless Unleash.configuration.disable_metrics
|
21
|
+
Unleash.toggle_metrics = Unleash::Metrics.new
|
22
|
+
Unleash.reporter = Unleash::MetricsReporter.new
|
23
|
+
scheduledExecutor = Unleash::ScheduledExecutor.new('MetricsReporter', Unleash.configuration.metrics_interval)
|
24
|
+
scheduledExecutor.run do
|
25
|
+
Unleash.reporter.send
|
26
|
+
end
|
27
|
+
end
|
28
|
+
register
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_enabled?(feature, context = nil, default_value = false)
|
32
|
+
Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}"
|
33
|
+
|
34
|
+
toggle_as_hash = Unleash.toggles.select{ |toggle| toggle['name'] == feature }.first
|
35
|
+
|
36
|
+
if toggle_as_hash.nil?
|
37
|
+
Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found"
|
38
|
+
return default_value
|
39
|
+
end
|
40
|
+
|
41
|
+
toggle = Unleash::FeatureToggle.new(toggle_as_hash)
|
42
|
+
toggle_result = toggle.is_enabled?(context, default_value)
|
43
|
+
|
44
|
+
return toggle_result
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def info
|
49
|
+
return {
|
50
|
+
'appName': Unleash.configuration.app_name,
|
51
|
+
'instanceId': Unleash.configuration.instance_id,
|
52
|
+
'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION,
|
53
|
+
'strategies': Unleash::STRATEGIES.keys,
|
54
|
+
'started': Time.now.iso8601(Unleash::TIME_RESOLUTION),
|
55
|
+
'interval': Unleash.configuration.metrics_interval_in_millis
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def register
|
60
|
+
Unleash.logger.debug "register()"
|
61
|
+
|
62
|
+
uri = URI(Unleash.configuration.client_register_url)
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
64
|
+
http.open_timeout = Unleash.configuration.timeout # in seconds
|
65
|
+
http.read_timeout = Unleash.configuration.timeout # in seconds
|
66
|
+
headers = {'Content-Type' => 'application/json'}
|
67
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
68
|
+
request.body = info.to_json
|
69
|
+
|
70
|
+
# Send the request
|
71
|
+
response = http.request(request)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
module Unleash
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :url, :app_name, :instance_id,
|
7
|
+
:disable_metrics, :timeout, :retry_limit,
|
8
|
+
:refresh_interval, :metrics_interval,
|
9
|
+
:backup_file, :logger, :log_level
|
10
|
+
|
11
|
+
def initialize(opts = {})
|
12
|
+
self.app_name = opts[:app_name] || nil
|
13
|
+
self.url = opts[:url] || nil
|
14
|
+
self.instance_id = opts[:instance_id] || SecureRandom.uuid
|
15
|
+
|
16
|
+
self.disable_metrics = opts[:disable_metrics] || false
|
17
|
+
self.refresh_interval = opts[:refresh_interval] || 15
|
18
|
+
self.metrics_interval = opts[:metrics_interval] || 10
|
19
|
+
self.timeout = opts[:timeout] || 30
|
20
|
+
self.retry_limit = opts[:retry_limit] || 1
|
21
|
+
|
22
|
+
self.backup_file = opts[:backup_file] || nil
|
23
|
+
|
24
|
+
self.logger = opts[:logger] || Logger.new(STDOUT)
|
25
|
+
self.log_level = opts[:log_level] || Logger::ERROR
|
26
|
+
|
27
|
+
|
28
|
+
if opts[:logger].nil?
|
29
|
+
# on default logger, use custom formatter that includes thread_name:
|
30
|
+
self.logger.formatter = proc do |severity, datetime, progname, msg|
|
31
|
+
thread_name = (Thread.current[:name] || "Unleash").rjust(16, ' ')
|
32
|
+
"[#{datetime.iso8601(6)} #{thread_name} #{severity.ljust(5, ' ')}] : #{msg}\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
refresh_backup_file!
|
37
|
+
end
|
38
|
+
|
39
|
+
def metrics_interval_in_millis
|
40
|
+
self.metrics_interval * 1_000
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate!
|
44
|
+
if self.app_name.nil? or self.url.nil?
|
45
|
+
raise ArgumentError, "URL and app_name are required"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def refresh_backup_file!
|
50
|
+
if self.backup_file.nil?
|
51
|
+
self.backup_file = Dir.tmpdir + "/unleash-#{app_name}-repo.json"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch_toggles_url
|
56
|
+
self.url + '/features'
|
57
|
+
end
|
58
|
+
|
59
|
+
def client_metrics_url
|
60
|
+
self.url + '/client/metrics'
|
61
|
+
end
|
62
|
+
|
63
|
+
def client_register_url
|
64
|
+
self.url + '/client/register'
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Unleash
|
2
|
+
|
3
|
+
class Context
|
4
|
+
attr_accessor :user_id, :session_id, :remote_address, :properties
|
5
|
+
|
6
|
+
def initialize(params = {})
|
7
|
+
params_is_a_hash = params.is_a?(Hash)
|
8
|
+
self.user_id = params_is_a_hash ? params.fetch(:user_id, '') : ''
|
9
|
+
self.session_id = params_is_a_hash ? params.fetch(:session_id, '') : ''
|
10
|
+
self.remote_address = params_is_a_hash ? params.fetch(:remote_address, '') : ''
|
11
|
+
self.properties = params_is_a_hash && params[:properties].is_a?(Hash) ? params.fetch(:properties, {}) : {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"<Context: user_id=#{self.user_id},session_id=#{self.session_id},remote_address=#{self.remote_address},properties=#{self.properties}>"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'unleash/activation_strategy'
|
2
|
+
require 'unleash/strategy/base'
|
3
|
+
require 'unleash/strategy/default'
|
4
|
+
require 'unleash/strategy/application_hostname'
|
5
|
+
require 'unleash/strategy/gradual_rollout_random'
|
6
|
+
require 'unleash/strategy/gradual_rollout_sessionid'
|
7
|
+
require 'unleash/strategy/gradual_rollout_userid'
|
8
|
+
require 'unleash/strategy/remote_address'
|
9
|
+
require 'unleash/strategy/user_with_id'
|
10
|
+
require 'unleash/strategy/unknown'
|
11
|
+
|
12
|
+
module Unleash
|
13
|
+
STRATEGIES = {
|
14
|
+
applicationHostname: Unleash::Strategy::ApplicationHostname.new,
|
15
|
+
gradualRolloutRandom: Unleash::Strategy::GradualRolloutRandom.new,
|
16
|
+
gradualRolloutSessionId: Unleash::Strategy::GradualRolloutSessionId.new,
|
17
|
+
gradualRolloutUserId: Unleash::Strategy::GradualRolloutUserId.new,
|
18
|
+
remoteAddress: Unleash::Strategy::RemoteAddress.new,
|
19
|
+
userWithId: Unleash::Strategy::UserWithId.new,
|
20
|
+
unknown: Unleash::Strategy::Unknown.new,
|
21
|
+
default: Unleash::Strategy::Default.new,
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
class FeatureToggle
|
26
|
+
attr_accessor :name, :enabled, :strategies, :choices, :choices_lock
|
27
|
+
|
28
|
+
def initialize(params={})
|
29
|
+
self.name = params['name'] || nil
|
30
|
+
self.enabled = params['enabled'] || false
|
31
|
+
|
32
|
+
self.strategies = params['strategies']
|
33
|
+
.select{|s| ( s.key?('name') && Unleash::STRATEGIES.key?(s['name'].to_sym) ) }
|
34
|
+
.map{|s| ActivationStrategy.new(s['name'], s['parameters'])} || []
|
35
|
+
|
36
|
+
# Unleash.logger.debug "FeatureToggle params: #{params}"
|
37
|
+
# Unleash.logger.debug "strategies: #{self.strategies}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
"<FeatureToggle: name=#{self.name},enabled=#{self.enabled},choices=#{self.choices},strategies=#{self.strategies}>"
|
42
|
+
end
|
43
|
+
|
44
|
+
def is_enabled?(context, default_result)
|
45
|
+
if not ['NilClass', 'Unleash::Context'].include? context.class.name
|
46
|
+
Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context"
|
47
|
+
context = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
result = self.enabled && self.strategies.select{ |s|
|
51
|
+
strategy = Unleash::STRATEGIES.fetch(s.name.to_sym, :unknown)
|
52
|
+
r = strategy.is_enabled?(s.params, context)
|
53
|
+
Unleash.logger.debug "Strategy #{s.name} returned #{r} with context: #{context}" #"for params #{s.params} "
|
54
|
+
r
|
55
|
+
}.any?
|
56
|
+
result ||= default_result
|
57
|
+
|
58
|
+
Unleash.logger.debug "FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} and Strategies combined returned #{result})"
|
59
|
+
|
60
|
+
choice = result ? :yes : :no
|
61
|
+
Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
|
62
|
+
|
63
|
+
return result
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Unleash
|
2
|
+
|
3
|
+
class Metrics
|
4
|
+
attr_accessor :features
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.features = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
self.features.to_json
|
12
|
+
end
|
13
|
+
|
14
|
+
def increment(feature, choice)
|
15
|
+
raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice
|
16
|
+
|
17
|
+
self.features[feature] = {yes: 0, no: 0} unless self.features.include? feature
|
18
|
+
self.features[feature][choice] += 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset
|
22
|
+
self.features = {}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'unleash/configuration'
|
2
|
+
require 'unleash/metrics'
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module Unleash
|
8
|
+
|
9
|
+
class MetricsReporter
|
10
|
+
attr_accessor :last_time, :client
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
self.last_time = Time.now
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_hash
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_report
|
20
|
+
now = Time.now
|
21
|
+
start, stop, self.last_time = self.last_time, now, now
|
22
|
+
report = {
|
23
|
+
'appName': Unleash.configuration.app_name,
|
24
|
+
'instanceId': Unleash.configuration.instance_id,
|
25
|
+
'bucket': {
|
26
|
+
'start': start.iso8601(Unleash::TIME_RESOLUTION),
|
27
|
+
'stop': stop.iso8601(Unleash::TIME_RESOLUTION),
|
28
|
+
'toggles': Unleash.toggle_metrics.features
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
Unleash.toggle_metrics.reset
|
33
|
+
return report
|
34
|
+
end
|
35
|
+
|
36
|
+
def send
|
37
|
+
Unleash.logger.debug "send() Report"
|
38
|
+
|
39
|
+
generated_report = self.generate_report()
|
40
|
+
|
41
|
+
uri = URI(Unleash.configuration.client_metrics_url)
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
43
|
+
http.open_timeout = Unleash.configuration.timeout # in seconds
|
44
|
+
http.read_timeout = Unleash.configuration.timeout # in seconds
|
45
|
+
headers = {'Content-Type' => 'application/json'}
|
46
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
47
|
+
request.body = generated_report.to_json
|
48
|
+
|
49
|
+
Unleash.logger.debug "Report to send: #{request.body}"
|
50
|
+
|
51
|
+
response = http.request(request)
|
52
|
+
|
53
|
+
if ['200','202'].include? response.code
|
54
|
+
Unleash.logger.debug "Report sent to unleash server sucessfully. Server responded with http code #{response.code}"
|
55
|
+
else
|
56
|
+
Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Unleash
|
2
|
+
|
3
|
+
class ScheduledExecutor
|
4
|
+
attr_accessor :name, :interval, :max_exceptions, :retry_count
|
5
|
+
|
6
|
+
def initialize(name, interval, max_exceptions = 5)
|
7
|
+
self.name = name || ''
|
8
|
+
self.interval = interval
|
9
|
+
self.max_exceptions = max_exceptions
|
10
|
+
self.retry_count = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(&blk)
|
14
|
+
thread = Thread.new do
|
15
|
+
Thread.current[:name] = self.name
|
16
|
+
|
17
|
+
loop do
|
18
|
+
Unleash.logger.debug "thread #{name} sleeping for #{interval} seconds"
|
19
|
+
sleep interval
|
20
|
+
|
21
|
+
Unleash.logger.debug "thread #{name} started"
|
22
|
+
begin
|
23
|
+
yield
|
24
|
+
self.retry_count = 0
|
25
|
+
rescue Exception => e
|
26
|
+
self.retry_count += 1
|
27
|
+
Unleash.logger.error "thread #{name} throwing exception (#{self.retry_count} of #{self.max_exceptions})", e
|
28
|
+
end
|
29
|
+
|
30
|
+
break if self.retry_count > self.max_exceptions
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|