unleash 0.1.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.
- 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
|