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.
@@ -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