unleash 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"
@@ -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