ff-ruby-server-sdk 1.0.6 → 1.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1dc03fbc008625a0836f68d7633ed828fca000860ca0ed4bb570acac0e5d0b1c
4
- data.tar.gz: 4d7b5998b0fa349c54f08589a094c2d9674989dde242bd0ca173222e92d579c1
3
+ metadata.gz: '09354edeb352628c1f9744e9c685dc27c2547fc81f3db0e9254dfe8bc9122d58'
4
+ data.tar.gz: 7f011367b6a2d16162b42f2f02db2730cd04dcff6bf75170d90b2c065e4203a6
5
5
  SHA512:
6
- metadata.gz: 8c0be4cdc264f5efa858b9746824e0304a255f21ff1c8b030e3b9556766f895c7864c26e279bab9370d696e5005934c55a79a11be23d146e8fd477cc82ad2bcf
7
- data.tar.gz: a17fdcd1fbcdbc8d4942acf543b06751648a6c986c3abb82e0bb7b239ce0e983349fa6cab0dc9a679df4b4250bce527ab76c93c27560edfa839b328ffb06bd53
6
+ metadata.gz: 3f3f09d1185dc89443b8b55bf9fe3bdb5ae6105cae81ec861df2affe25b2ade4b7aa7c3620dc1fc41cdde1979b624c8eef3d57e15474ffd8c2934d8dbc03b843
7
+ data.tar.gz: 02eabc3cce448322b89c5901e183b8d2e9f25d47b605569d26aa07743f669e51d2779efc2c87179e711e37e68f381c6a1458352568e8bff399f19a548fc2b5cd
data/Gemfile CHANGED
@@ -16,10 +16,16 @@ gem "moneta"
16
16
 
17
17
  # SSE support:
18
18
  gem "rest-client"
19
- gem "sse-client"
20
19
 
21
20
  # Concurrency support:
22
- gem "concurrent-ruby", require: "concurrent"
21
+ gem "concurrent-ruby", "1.1.10", require: "concurrent"
23
22
 
24
23
  # Evaluator dependencies:
25
24
  gem "murmurhash3"
25
+
26
+ gem "typhoeus"
27
+
28
+ group :test do
29
+ gem 'simplecov', '~> 0.21.2'
30
+ end
31
+
data/README.md CHANGED
@@ -81,7 +81,7 @@ client.close
81
81
 
82
82
  ```bash
83
83
  # Install the deps
84
- gem install ff-ruby-server-sdk typhoeus openapi_client
84
+ gem install ff-ruby-server-sdk typhoeus
85
85
 
86
86
  # Set your API Key
87
87
  export FF_API_KEY=<your key here>
@@ -96,7 +96,7 @@ use docker to quickly get started
96
96
 
97
97
  ```bash
98
98
  # Install the package
99
- docker run -v $(pwd):/app -w /app -e FF_API_KEY=$FF_API_KEY ruby:2.7-buster gem install --install-dir ./gems ff-ruby-server-sdk typhoeus openapi_client
99
+ docker run -v $(pwd):/app -w /app -e FF_API_KEY=$FF_API_KEY ruby:2.7-buster gem install --install-dir ./gems ff-ruby-server-sdk typhoeus
100
100
 
101
101
  # Run the script
102
102
  docker run -v $(pwd):/app -w /app -e FF_API_KEY=$FF_API_KEY -e GEM_HOME=/app/gems ruby:2.7-buster ruby ./example/getting_started/getting_started.rb
@@ -108,6 +108,7 @@ Further examples and config options are in the further reading section:
108
108
 
109
109
  [Further Reading](docs/further_reading.md)
110
110
 
111
+ [Ruby on Rails example](ruby_on_rails_example/README.md)
111
112
 
112
113
  -------------------------
113
114
  [Harness](https://www.harness.io/) is a feature management platform that helps teams to build better software and to
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+
3
+ ruby -v
4
+ apt-get update
5
+ apt-get install -y npm
6
+ apt-get install -y maven
7
+ apt-get install -y jq
8
+ mvn --version
9
+ npm install @openapitools/openapi-generator-cli -g
10
+ npm install @openapitools/openapi-generator-cli -D
11
+ gem install minitest-junit
12
+ sh scripts/install.sh
13
+ gem env
14
+ gem install ff-ruby-server-sdk typhoeus
15
+ ruby test/ff/ruby/server/sdk/sdk_test.rb --junit
@@ -32,7 +32,7 @@ target = Target.new("RubySDK", identifier="rubysdk", attributes={"location": "em
32
32
  # Loop forever reporting the state of the flag
33
33
  loop do
34
34
  result = client.bool_variation(flagName, target, false)
35
- logger.info "Flag variation: #{result}"
35
+ logger.info "Flag #{flagName} is set to: #{result}"
36
36
  sleep 10
37
37
  end
38
38
 
@@ -0,0 +1,67 @@
1
+ require 'ff/ruby/server/sdk/api/config'
2
+ require 'ff/ruby/server/sdk/dto/target'
3
+ require 'ff/ruby/server/sdk/api/cf_client'
4
+ require 'ff/ruby/server/sdk/api/config_builder'
5
+
6
+ require "logger"
7
+ require "securerandom"
8
+
9
+ $stdout.sync = true
10
+ logger = Logger.new $stdout
11
+
12
+ # API Key
13
+ apiKey = ENV['FF_API_KEY'] || 'changeme'
14
+
15
+ # Flag Name
16
+ flagName = ENV['FF_FLAG_NAME'] || 'harnessappdemodarkmode'
17
+
18
+ logger.info "Harness Ruby SDK TLS example"
19
+
20
+ =begin
21
+ For ff servers with a custom or private CAs, you can use 'tls_ca_cert' to pass in the CA bundle in ASCII PEM format.
22
+ You should also include any intermediate CAs so the full trust chain can be resolved. Typhoeus HTTP client uses libcurl
23
+ underneath, when developing you should enable debugging(true) to get more detailed error diagnostics logged, which
24
+ aren't reported through OpenAPI. Common errors include:
25
+
26
+ SSL peer certificate or SSH remote key was not OK - you have an invalid or missing CA for the server you're trying
27
+ to connect to. It can also mean the server hostname and request
28
+ hostname don't match.
29
+ SSL: no alternative certificate subject name - The hostname or IP used in your SDK URLs do not match the SANs
30
+ matches target host name ‘<host>' configured in the cert sent by the web server. You should either
31
+ fix your URLs or ensure the SANs in the X.509 cert are configured
32
+ correctly.
33
+
34
+ The example below assumes you have an ff-server (or proxy) configured with TLS for a server hosted on
35
+ 'ffserver:8000' where the web server's cert has a SANs with DNS entry 'ffserver'. CA.crt tells the SDK you trust this
36
+ server.
37
+
38
+ Typhoeus/libcurl by default has its default CA bundle stored at /etc/ssl/cert.pem. You can append your CA here if
39
+ you choose not to use 'tls_ca_cert'.
40
+
41
+ =end
42
+
43
+
44
+ client = CfClient.instance
45
+ client.init(apiKey, ConfigBuilder.new.logger(logger)
46
+ .event_url("https://ffserver:8001/api/1.0")
47
+ .config_url("https://ffserver:8000/api/1.0")
48
+ .tls_ca_cert("/path/to/CA.crt")
49
+ .debugging(true)
50
+ .build)
51
+
52
+ client.wait_for_initialization
53
+
54
+
55
+ # Create a target (different targets can get different results based on rules. This include a custom attribute 'location')
56
+ target = Target.new("RubySDK", identifier="rubysdk", attributes={"location": "emea"})
57
+
58
+ # Loop forever reporting the state of the flag
59
+ loop do
60
+ result = client.bool_variation(flagName, target, false)
61
+ logger.info "#{flagName} flag variation: #{result}"
62
+ sleep 10
63
+ end
64
+
65
+ client.close
66
+
67
+
@@ -2,56 +2,47 @@ require_relative "../common/closeable"
2
2
 
3
3
  class AuthService < Closeable
4
4
 
5
- def initialize(connector = nil, poll_interval_in_sec = 60, callback = nil, logger = nil)
5
+ def initialize(connector, callback, logger, retry_delay_ms = 6000)
6
6
 
7
7
  unless connector.kind_of?(Connector)
8
-
9
8
  raise "The 'connector' parameter must be of '" + Connector.to_s + "' data type"
10
9
  end
11
10
 
12
11
  unless callback.kind_of?(ClientCallback)
13
-
14
12
  raise "The 'callback' parameter must be of '" + ClientCallback.to_s + "' data type"
15
13
  end
16
14
 
15
+ @logger = logger
17
16
  @callback = callback
18
17
  @connector = connector
19
- @poll_interval_in_sec = poll_interval_in_sec
20
-
21
- if logger != nil
22
-
23
- @logger = logger
24
- else
25
-
26
- @logger = Logger.new(STDOUT)
27
- end
18
+ @retry_delay_ms = retry_delay_ms
19
+ @authenticated = false
28
20
  end
29
21
 
30
22
  def start_async
31
-
32
23
  @logger.debug "Async starting: " + self.to_s
33
24
 
34
- @ready = true
35
-
36
- @thread = Thread.new do
37
-
38
- @logger.debug "Async started: " + self.to_s
39
-
40
- while @ready do
41
-
42
- @logger.debug "Async auth iteration"
43
-
44
- if @connector.authenticate
25
+ @thread = Thread.new :report_on_exception => true do
26
+ attempt = 1
27
+ until @authenticated do
28
+ http_code = @connector.authenticate
45
29
 
30
+ if http_code == 200
31
+ @authenticated = true
46
32
  @callback.on_auth_success
47
33
  stop_async
48
- @logger.info "Stopping Auth service"
34
+ elsif should_retry_http_code http_code
35
+ delay_ms = @retry_delay_ms * [10, attempt].min
36
+ @logger.warn "Got HTTP code #{http_code} while authenticating on attempt #{attempt}, will retry in #{delay_ms} ms"
37
+ sleep(delay_ms/1000)
38
+ attempt += 1
39
+ @logger.info "Retrying to authenticate, attempt #{attempt}..."
49
40
  else
50
-
51
- @logger.error "Exception while authenticating, retry in " + @poll_interval_in_sec.to_s + " seconds"
41
+ @logger.warn "Auth Service got HTTP code #{http_code} while authenticating, will not attempt to reconnect"
42
+ @callback.on_auth_failed
43
+ stop_async
44
+ next
52
45
  end
53
-
54
- sleep(@poll_interval_in_sec)
55
46
  end
56
47
  end
57
48
 
@@ -59,33 +50,50 @@ class AuthService < Closeable
59
50
  end
60
51
 
61
52
  def close
62
-
63
53
  stop_async
64
54
  end
65
55
 
66
- def on_auth_success
56
+ protected
67
57
 
68
- unless @callback == nil
58
+ def on_auth_success
69
59
 
60
+ if @callback != nil
70
61
  unless @callback.kind_of?(ClientCallback)
71
-
72
62
  raise "Expected '" + ClientCallback.to_s + "' data type for the callback"
73
63
  end
74
-
75
64
  @callback.on_auth_success
76
65
  end
77
66
  end
78
67
 
79
- protected
80
-
81
68
  def stop_async
82
-
83
- @ready = false
84
-
85
69
  if @thread != nil
86
-
70
+ @logger.info "Stopping Auth service, status=#{@thread.status}"
87
71
  @thread.exit
88
72
  @thread = nil
73
+ @logger.info "Stopping Auth service done"
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def is_authenticated
80
+ @authenticated
81
+ end
82
+
83
+ def should_retry_http_code(code)
84
+ # 408 request timeout
85
+ # 425 too early
86
+ # 429 too many requests
87
+ # 500 internal server error
88
+ # 502 bad gateway
89
+ # 503 service unavailable
90
+ # 504 gateway timeout
91
+ # -1 OpenAPI error (timeout etc)
92
+ case code
93
+ when 408,425,429,500,502,503,504,-1
94
+ return true
95
+ else
96
+ return false
89
97
  end
90
98
  end
91
99
  end
@@ -1,5 +1,5 @@
1
- require "openapi_client"
2
1
 
2
+ require_relative "../../generated/lib/openapi_client"
3
3
  require_relative "../common/closeable"
4
4
  require_relative "inner_client"
5
5
 
@@ -2,44 +2,49 @@ require_relative "../common/closeable"
2
2
 
3
3
  class ClientCallback < Closeable
4
4
 
5
+ TBI = RuntimeError.new("To be implemented")
6
+
5
7
  def initialize
6
8
  super
7
-
8
- @tbi = "To be implemented"
9
9
  end
10
10
 
11
11
  def on_auth_success
12
12
 
13
- raise @tbi
13
+ raise TBI
14
+ end
15
+
16
+ def on_auth_failed
17
+
18
+ raise TBI
14
19
  end
15
20
 
16
21
  def on_authorized
17
22
 
18
- raise @tbi
23
+ raise TBI
19
24
  end
20
25
 
21
26
  def is_closing
22
27
 
23
- raise @tbi
28
+ raise TBI
24
29
  end
25
30
 
26
31
  def on_processor_ready(processor)
27
32
 
28
- raise @tbi
33
+ raise TBI
29
34
  end
30
35
 
31
36
  def on_update_processor_ready
32
37
 
33
- raise @tbi
38
+ raise TBI
34
39
  end
35
40
 
36
41
  def on_metrics_processor_ready
37
42
 
38
- raise @tbi
43
+ raise TBI
39
44
  end
40
45
 
41
46
  def update(message, manual)
42
47
 
43
- raise @tbi
48
+ raise TBI
44
49
  end
45
50
  end
@@ -7,7 +7,7 @@ class Config
7
7
  attr_accessor :config_url, :event_url, :stream_enabled, :poll_interval_in_seconds, :analytics_enabled,
8
8
  :frequency, :buffer_size, :all_attributes_private, :private_attributes, :connection_timeout,
9
9
  :read_timeout, :write_timeout, :debugging, :metrics_service_acceptable_duration, :cache, :store,
10
- :logger
10
+ :logger, :ssl_ca_cert
11
11
 
12
12
  # Static:
13
13
  class << self
@@ -35,7 +35,7 @@ class Config
35
35
 
36
36
  @frequency = @@min_frequency
37
37
 
38
- @buffer_size = 1024
38
+ @buffer_size = 2048
39
39
 
40
40
  @all_attributes_private = false
41
41
 
@@ -82,7 +82,7 @@ class Config
82
82
 
83
83
  def verify_ssl
84
84
 
85
- nil
85
+ true
86
86
  end
87
87
 
88
88
  def cert_file
@@ -97,7 +97,7 @@ class Config
97
97
 
98
98
  def ssl_ca_cert
99
99
 
100
- nil
100
+ @ssl_ca_cert
101
101
  end
102
102
 
103
103
  def client_side_validation
@@ -83,6 +83,11 @@ class ConfigBuilder
83
83
  self
84
84
  end
85
85
 
86
+ def tls_ca_cert(cert_file)
87
+ @config.ssl_ca_cert = cert_file
88
+ self
89
+ end
90
+
86
91
  def logger(logger)
87
92
 
88
93
  @config.logger = logger
@@ -97,17 +97,21 @@ class InnerClient < ClientCallback
97
97
  @poll_processor.start
98
98
 
99
99
  if @config.stream_enabled
100
-
101
100
  @update_processor.start
102
101
  end
103
102
 
104
103
  if @config.analytics_enabled
105
-
106
104
  @metrics_processor.start
107
105
  end
108
106
 
109
107
  end
110
108
 
109
+ def on_auth_failed
110
+ @config.logger.warn "Authentication failed with a non-recoverable error - defaults will be served"
111
+ @initialized = true
112
+
113
+ end
114
+
111
115
  def close
112
116
 
113
117
  @config.logger.info "Closing the client: " + self.to_s
@@ -268,12 +272,13 @@ class InnerClient < ClientCallback
268
272
  @metrics_processor.init(@connector, @config, @metrics_callback)
269
273
 
270
274
  @evaluator = Evaluator.new(@repository, logger = @config.logger)
271
- @evaluator_callback = InnerClientFlagEvaluateCallback.new(@metrics_processor, logger = @config.logger)
272
275
 
273
- @auth_service = AuthService.new(
276
+ if @config.analytics_enabled
277
+ @evaluator_callback = InnerClientFlagEvaluateCallback.new(@metrics_processor, logger = @config.logger)
278
+ end
274
279
 
280
+ @auth_service = AuthService.new(
275
281
  connector = @connector,
276
- poll_interval_in_sec = @config.poll_interval_in_seconds,
277
282
  callback = self,
278
283
  logger = @config.logger
279
284
  )
@@ -312,4 +317,5 @@ class InnerClient < ClientCallback
312
317
 
313
318
  @my_mutex.synchronize(&block)
314
319
  end
320
+
315
321
  end
@@ -25,6 +25,6 @@ class InnerClientFlagEvaluateCallback < FlagEvaluateCallback
25
25
 
26
26
  @logger.debug "Processing evaluation: " + feature_config.feature.to_s + ", " + target.identifier.to_s
27
27
 
28
- @metrics_processor.push_to_queue(target, feature_config, variation)
28
+ @metrics_processor.register_evaluation(target, feature_config, variation)
29
29
  end
30
30
  end
@@ -53,7 +53,7 @@ class InnerClientUpdater < Updater
53
53
 
54
54
  def on_error
55
55
 
56
- @logger.error "Error occurred"
56
+ @logger.error "InnerClientUpdater error occurred"
57
57
  end
58
58
 
59
59
  def update(message)
@@ -2,15 +2,27 @@ class MetricsEvent
2
2
 
3
3
  attr_accessor :feature_config, :target, :variation
4
4
 
5
- def initialize(
6
-
7
- feature_config,
8
- target,
9
- variation
10
- )
5
+ def initialize(feature_config, target, variation)
11
6
 
12
7
  @target = target
13
8
  @variation = variation
14
9
  @feature_config = feature_config
10
+ freeze
11
+ end
12
+
13
+ def ==(other)
14
+ eql?(other)
15
+ end
16
+
17
+ def eql?(other)
18
+ feature_config.feature == other.feature_config.feature and
19
+ variation.identifier == other.variation.identifier and
20
+ target.identifier == other.target.identifier
15
21
  end
22
+
23
+ def hash
24
+ feature_config.feature.hash | variation.identifier.hash | target.identifier.hash
25
+ end
26
+
27
+
16
28
  end
@@ -9,25 +9,47 @@ require_relative "../api/summary_metrics"
9
9
 
10
10
  class MetricsProcessor < Closeable
11
11
 
12
- def init(
12
+ class FrequencyMap < Concurrent::Map
13
+ def initialize(options = nil, &block)
14
+ super
15
+ end
13
16
 
14
- connector,
15
- config,
16
- callback
17
- )
17
+ def increment(key)
18
+ compute(key) do |old_value|
19
+ if old_value == nil; 1 else old_value + 1 end
20
+ end
21
+ end
18
22
 
19
- unless connector.kind_of?(Connector)
23
+ def get(key)
24
+ self[key]
25
+ end
26
+
27
+ def drain_to_map
28
+ result = {}
29
+ each_key do |key|
30
+ result[key] = 0
31
+ end
32
+ result.each_key do |key|
33
+ value = get_and_set(key, 0)
34
+ result[key] = value
35
+ delete_pair(key, 0)
36
+ end
37
+ result
38
+ end
39
+ end
20
40
 
41
+
42
+ def init(connector, config, callback)
43
+
44
+ unless connector.kind_of?(Connector)
21
45
  raise "The 'connector' must be of '" + Connector.to_s + "' data type"
22
46
  end
23
47
 
24
48
  unless callback.kind_of?(MetricsCallback)
25
-
26
49
  raise "The 'callback' must be of '" + MetricsCallback.to_s + "' data type"
27
50
  end
28
51
 
29
52
  unless config.kind_of?(Config)
30
-
31
53
  raise "The 'config' must be of '" + Config.to_s + "' data type"
32
54
  end
33
55
 
@@ -51,247 +73,151 @@ class MetricsProcessor < Closeable
51
73
  @feature_name_attribute = "featureName"
52
74
  @variation_identifier_attribute = "variationIdentifier"
53
75
 
54
- @queue = SizedQueue.new(@config.buffer_size)
55
- @executor = Concurrent::FixedThreadPool.new(100)
76
+ @executor = Concurrent::FixedThreadPool.new(10)
77
+
78
+ @frequency_map = FrequencyMap.new
79
+
80
+ @max_buffer_size = config.buffer_size - 1
56
81
 
57
82
  @callback.on_metrics_ready
58
83
  end
59
84
 
60
85
  def start
61
-
62
86
  @config.logger.info "Starting metrics processor with request interval: " + @config.frequency.to_s
63
87
  start_async
64
88
  end
65
89
 
66
90
  def stop
67
-
68
91
  @config.logger.info "Stopping metrics processor"
69
92
  stop_async
70
93
  end
71
94
 
72
95
  def close
73
-
74
96
  stop
75
97
  @config.logger.info "Closing metrics processor"
76
98
  end
77
99
 
78
- def push_to_queue(
79
-
80
- target,
81
- feature_config,
82
- variation
83
- )
84
-
85
- @executor.post do
86
-
87
- @config.logger.debug "Pushing to the metrics queue: START"
88
-
89
- event = MetricsEvent.new(feature_config, target, variation)
90
- @queue.push(event)
91
-
92
- @config.logger.debug "Pushing to the metrics queue: END, queue size: " + @queue.size.to_s
93
-
94
- end
95
- end
96
-
97
- def send_data_and_reset_cache(data)
98
-
99
- @config.logger.debug "Reading from queue and building cache"
100
-
101
- @jar_version = get_version
102
-
103
- unless data.empty?
104
-
105
- map = {}
106
-
107
- data.each do |event|
108
-
109
- new_value = 1
110
- current = map[event]
111
-
112
- if current != nil
113
-
114
- new_value = current + 1
115
- end
116
-
117
- map[event] = new_value
118
- end
119
-
120
- metrics = prepare_summary_metrics_body(map)
121
-
122
- if !metrics.metrics_data.empty? && !metrics.target_data.empty?
100
+ def register_evaluation(target, feature_config, variation)
123
101
 
124
- start_time = (Time.now.to_f * 1000).to_i
125
-
126
- @connector.post_metrics(metrics)
127
-
128
- end_time = (Time.now.to_f * 1000).to_i
129
-
130
- if end_time - start_time > @config.metrics_service_acceptable_duration
131
-
132
- @config.logger.debug "Metrics service API duration=[" + (end_time - start_time).to_s + "]"
133
- end
102
+ if @frequency_map.size > @max_buffer_size
103
+ @config.logger.warn "metrics buffer is full #{@frequency_map.size} - flushing metrics"
104
+ @executor.post do
105
+ run_one_iteration
134
106
  end
135
-
136
- @global_target_set.merge(@staging_target_set)
137
- @staging_target_set.clear
138
107
  end
108
+
109
+ event = MetricsEvent.new(feature_config, target, variation)
110
+ @frequency_map.increment event
139
111
  end
140
112
 
141
- protected
113
+ private
142
114
 
143
115
  def run_one_iteration
116
+ send_data_and_reset_cache @frequency_map.drain_to_map
144
117
 
145
- @config.logger.debug "Async metrics iteration: " + @thread.to_s
146
-
147
- data = []
118
+ @config.logger.debug "metrics: frequency map size #{@frequency_map.size}. global target size #{@global_target_set.size}"
119
+ end
148
120
 
149
- until @queue.empty?
121
+ def send_data_and_reset_cache(map)
122
+ metrics = prepare_summary_metrics_body(map)
150
123
 
151
- item = @queue.pop
152
- data.push(item)
124
+ if !metrics.metrics_data.empty? && !metrics.target_data.empty?
125
+ start_time = (Time.now.to_f * 1000).to_i
126
+ @connector.post_metrics(metrics)
127
+ end_time = (Time.now.to_f * 1000).to_i
128
+ if end_time - start_time > @config.metrics_service_acceptable_duration
129
+ @config.logger.debug "Metrics service API duration=[" + (end_time - start_time).to_s + "]"
130
+ end
153
131
  end
154
132
 
155
- send_data_and_reset_cache(data)
156
- end
133
+ @global_target_set.merge(@staging_target_set)
134
+ @staging_target_set.clear
157
135
 
158
- def prepare_summary_metrics_body(data)
136
+ end
159
137
 
160
- summary_metrics_data = {}
138
+ def prepare_summary_metrics_body(freq_map)
161
139
  metrics = OpenapiClient::Metrics.new({ :target_data => [], :metrics_data => [] })
162
-
163
- add_target_data(
164
-
165
- metrics,
166
- Target.new(
167
-
168
- name = @global_target_name,
169
- identifier = @global_target
170
- )
171
- )
172
-
173
- data.each do |key, value|
174
-
175
- target = key.target
176
-
177
- add_target_data(metrics, target)
178
-
179
- summary_metrics = prepare_summary_metrics_key(key)
180
-
181
- summary_metrics_data[summary_metrics] = value
140
+ add_target_data(metrics, Target.new(name = @global_target_name, identifier = @global_target))
141
+ freq_map.each_key do |key|
142
+ add_target_data(metrics, key.target)
182
143
  end
183
-
184
- summary_metrics_data.each do |key, value|
185
-
144
+ total_count = 0
145
+ freq_map.each do |key, value|
146
+ total_count += value
186
147
  metrics_data = OpenapiClient::MetricsData.new({ :attributes => [] })
187
148
  metrics_data.timestamp = (Time.now.to_f * 1000).to_i
188
149
  metrics_data.count = value
189
150
  metrics_data.metrics_type = "FFMETRICS"
190
- metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @feature_name_attribute, :value => key.feature_name }))
191
- metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @variation_identifier_attribute, :value => key.variation_identifier }))
151
+ metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @feature_name_attribute, :value => key.feature_config.feature }))
152
+ metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @variation_identifier_attribute, :value => key.variation.identifier }))
192
153
  metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @target_attribute, :value => @global_target }))
193
154
  metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @sdk_type, :value => @server }))
194
155
  metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @sdk_language, :value => "ruby" }))
195
156
  metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @sdk_version, :value => @jar_version }))
196
-
197
157
  metrics.metrics_data.push(metrics_data)
198
158
  end
159
+ @config.logger.debug "Pushed #{total_count} metric evaluations to server. metrics_data count is #{freq_map.size}"
199
160
 
200
161
  metrics
201
162
  end
202
163
 
203
- private
204
-
205
- def start_async
206
-
207
- @config.logger.debug "Async starting: " + self.to_s
208
-
209
- @ready = true
210
-
211
- @thread = Thread.new do
212
-
213
- @config.logger.debug "Async started: " + self.to_s
214
-
215
- while @ready do
216
-
217
- unless @initialized
218
-
219
- @initialized = true
220
- @config.logger.info "Metrics processor initialized"
221
- end
222
-
223
- sleep(@config.frequency)
224
-
225
- run_one_iteration
226
- end
227
- end
228
-
229
- @thread.run
230
- end
231
-
232
- def stop_async
233
-
234
- @queue.clear
235
-
236
- @ready = false
237
- @initialized = false
238
- end
239
-
240
- def prepare_summary_metrics_key(key)
241
-
242
- SummaryMetrics.new(
243
-
244
- feature_name = key.feature_config.feature,
245
- variation_identifier = key.variation.identifier,
246
- variation_value = key.variation.value
247
- )
248
- end
249
-
250
164
  def add_target_data(metrics, target)
251
165
 
252
166
  target_data = OpenapiClient::TargetData.new({ :attributes => [] })
253
167
  private_attributes = target.private_attributes
254
168
 
255
169
  if !@staging_target_set.include?(target) && !@global_target_set.include?(target) && !target.is_private
256
-
257
170
  @staging_target_set.add(target)
258
-
259
171
  attributes = target.attributes
260
-
261
172
  attributes.each do |k, v|
262
-
263
173
  key_value = OpenapiClient::KeyValue.new
264
-
265
174
  if !private_attributes.empty?
266
-
267
175
  unless private_attributes.include?(k)
268
-
269
176
  key_value = OpenapiClient::KeyValue.new({ :key => k, :value => v.to_s })
270
177
  end
271
178
  else
272
-
273
179
  key_value = OpenapiClient::KeyValue.new({ :key => k, :value => v.to_s })
274
180
  end
275
-
276
181
  target_data.attributes.push(key_value)
277
182
  end
278
-
279
183
  target_data.identifier = target.identifier
280
-
281
184
  if target.name == nil || target.name == ""
282
-
283
185
  target_data.name = target.identifier
284
186
  else
285
-
286
187
  target_data.name = target.name
287
188
  end
288
-
289
189
  metrics.target_data.push(target_data)
290
190
  end
291
191
  end
292
192
 
293
- def get_version
193
+ def start_async
194
+ @config.logger.debug "Async starting: " + self.to_s
195
+ @ready = true
196
+ @thread = Thread.new do
197
+ @config.logger.debug "Async started: " + self.to_s
198
+ while @ready do
199
+ unless @initialized
200
+ @initialized = true
201
+ @config.logger.info "Metrics processor initialized"
202
+ end
203
+ sleep(@config.frequency)
204
+ run_one_iteration
205
+ end
206
+ end
207
+ @thread.run
208
+ end
294
209
 
210
+ def stop_async
211
+ @ready = false
212
+ @initialized = false
213
+ end
214
+
215
+ def get_version
295
216
  Ff::Ruby::Server::Sdk::VERSION
296
217
  end
218
+
219
+ def get_frequency_map
220
+ @frequency_map
221
+ end
222
+
297
223
  end
@@ -1,5 +1,5 @@
1
1
  require "json"
2
- require "sse-client"
2
+ require 'restclient'
3
3
 
4
4
  require_relative './service'
5
5
 
@@ -10,109 +10,105 @@ class Events < Service
10
10
  url,
11
11
  headers,
12
12
  updater,
13
- logger = nil
13
+ config
14
14
  )
15
15
 
16
- if @updater != nil
16
+ @url = url
17
+ @headers = headers
18
+ @headers['params'] = {}
19
+ @updater = updater
20
+ @config = config
17
21
 
22
+ if @updater != nil
18
23
  unless @updater.kind_of?(Updater)
19
-
20
24
  raise "The 'callback' parameter must be of '" + Updater.to_s + "' data type"
21
25
  end
22
26
  end
23
27
 
24
- @updater = updater
25
-
26
- if logger != nil
27
-
28
- @logger = logger
28
+ if @config.logger != nil
29
+ @logger = @config.logger
29
30
  else
30
-
31
31
  @logger = Logger.new(STDOUT)
32
32
  end
33
33
 
34
- @sse = SSE::EventSource.new(
35
-
36
- url = url,
37
- query = {},
38
- headers = headers
39
- )
40
-
41
- @sse.open do
42
-
43
- on_open
44
- end
45
-
46
- @sse.error do |error|
47
-
48
- if error != nil
49
-
50
- @logger.error "SSE ERROR: " + error.body
51
- end
52
-
53
- on_error
54
- end
55
-
56
- @sse.message do |message|
57
-
58
- on_message(message)
59
- end
60
-
61
- @sse.on("*") do |message|
62
-
63
- on_message(message)
64
- end
65
-
66
34
  @updater.on_ready
67
35
  end
68
36
 
69
37
  def start
70
-
71
38
  @logger.info "Starting EventSource service"
72
-
73
- @sse.start
39
+ begin
40
+ conn = RestClient::Request.execute(method: :get,
41
+ url: @url,
42
+ headers: @headers,
43
+ block_response: proc { |response| response_handler response },
44
+ before_execution_proc: nil,
45
+ log: false,
46
+ read_timeout: 60,
47
+ ssl_ca_file: @config.ssl_ca_cert)
48
+
49
+
50
+ rescue => e
51
+ @logger.warn "SSE connection failed: " + e.message
52
+ on_error
53
+ end
74
54
  end
75
55
 
76
56
  def stop
77
-
78
57
  @logger.info "Stopping EventSource service"
79
-
80
58
  on_closed
81
59
  end
82
60
 
83
61
  def close
84
-
85
62
  stop
86
63
  end
87
64
 
88
65
  def on_open
89
-
90
66
  @logger.info "EventSource connected"
91
-
92
67
  @updater.on_connected
93
68
  end
94
69
 
95
70
  def on_error
96
-
97
71
  @logger.error "EventSource error"
98
-
99
72
  @updater.on_error
100
-
101
73
  stop
102
74
  end
103
75
 
104
76
  def on_closed
105
-
106
77
  @logger.info "EventSource disconnected"
107
-
108
78
  @updater.on_disconnected
109
79
  end
110
80
 
111
81
  def on_message(message)
112
-
113
82
  @logger.debug "EventSource message received " + message.to_s
114
-
115
83
  msg = JSON.parse(message)
116
84
  @updater.update(msg)
117
85
  end
86
+
87
+ private
88
+
89
+ def emit_line(line)
90
+ if line.start_with?("data:")
91
+ @logger.debug "SSE emit line: " + line
92
+ on_message line[line.index("{")..-1]
93
+ end
94
+ end
95
+
96
+ def response_handler(response)
97
+ on_open
98
+ case response.code
99
+ when "200"
100
+ line = ""
101
+ response.read_body do |chunk|
102
+ line << chunk
103
+ while line.sub!(/^(.*)\n/,"")
104
+ emit_line $1
105
+ end
106
+ end
107
+ close
108
+ else
109
+ @logger.error "SSE ERROR: http_code=%d body=%d" % [response.code, response.body]
110
+ on_error
111
+ end
112
+ end
113
+
118
114
  end
@@ -13,6 +13,7 @@ class HarnessConnector < Connector
13
13
  @config = config
14
14
  @on_unauthorized = on_unauthorized
15
15
  @user_agent = "RubySDK " + Ff::Ruby::Server::Sdk::VERSION
16
+ @sdk_info = "Ruby #{Ff::Ruby::Server::Sdk::VERSION} Server"
16
17
 
17
18
  @api = OpenapiClient::ClientApi.new(make_api_client)
18
19
  @metrics_api = OpenapiClient::MetricsApi.new(make_metrics_api_client)
@@ -36,14 +37,21 @@ class HarnessConnector < Connector
36
37
 
37
38
  @config.logger.info "Token has been obtained"
38
39
  process_token
39
- return true
40
+ return 200
40
41
 
41
42
  rescue OpenapiClient::ApiError => e
42
43
 
44
+ if e.message.include? "the server returns an error"
45
+ # NOTE openapi-generator 5.2.1 has a bug where exceptions don't contain any useful information and we can't
46
+ # determine if a timeout has occurred. This is fixed in 6.3.0 but requires Ruby version to be increased to 2.7
47
+ # https://github.com/OpenAPITools/openapi-generator/releases/tag/v6.3.0
48
+ @config.logger.warn "OpenapiClient::ApiError [\n\n#{e}\n]"
49
+ return -1
50
+ end
51
+
43
52
  log_error(e)
53
+ return e.code
44
54
  end
45
-
46
- false
47
55
  end
48
56
 
49
57
  def get_flags
@@ -59,6 +67,7 @@ class HarnessConnector < Connector
59
67
  rescue OpenapiClient::ApiError => e
60
68
 
61
69
  log_error(e)
70
+ return nil
62
71
  end
63
72
  end
64
73
 
@@ -75,6 +84,7 @@ class HarnessConnector < Connector
75
84
  rescue OpenapiClient::ApiError => e
76
85
 
77
86
  log_error(e)
87
+ return nil
78
88
  end
79
89
  end
80
90
 
@@ -139,15 +149,19 @@ class HarnessConnector < Connector
139
149
  headers = {
140
150
 
141
151
  "Authorization" => "Bearer " + @token,
142
- "API-Key" => @api_key
143
- }
152
+ "API-Key" => @api_key,
153
+ "User-Agent" => @user_agent,
154
+ "Harness-SDK-Info" => @sdk_info,
155
+ "Harness-AccountID" => @account_id,
156
+ "Harness-EnvironmentID" => @environment_id
157
+ }.compact
144
158
 
145
159
  @event_source = Events.new(
146
160
 
147
161
  url,
148
162
  headers,
149
163
  updater,
150
- @config.logger
164
+ @config
151
165
  )
152
166
 
153
167
  @event_source
@@ -169,7 +183,10 @@ class HarnessConnector < Connector
169
183
  api_client = OpenapiClient::ApiClient.new
170
184
 
171
185
  api_client.config = @config
186
+ api_client.config.connection_timeout = @config.read_timeout / 1000
187
+ api_client.config.read_timeout = @config.read_timeout / 1000
172
188
  api_client.user_agent = @user_agent
189
+ api_client.default_headers['Harness-SDK-Info'] = @sdk_info
173
190
 
174
191
  api_client
175
192
  end
@@ -189,30 +206,32 @@ class HarnessConnector < Connector
189
206
 
190
207
  api_client.config = config
191
208
  api_client.user_agent = @user_agent
209
+ api_client.default_headers['Harness-SDK-Info'] = @sdk_info
192
210
 
193
211
  api_client
194
212
  end
195
213
 
196
214
  def process_token
197
-
198
- headers = {
199
-
200
- "Authorization" => "Bearer " + @token
201
- }
202
-
203
- @api.api_client.default_headers = @api.api_client.default_headers.merge(headers)
204
- @metrics_api.api_client.default_headers = @metrics_api.api_client.default_headers.merge(headers)
205
-
206
215
  decoded_token = JWT.decode @token, nil, false
207
216
 
208
217
  if decoded_token != nil && !decoded_token.empty?
209
218
 
210
219
  @environment = decoded_token[0]["environment"]
211
220
  @cluster = decoded_token[0]["clusterIdentifier"]
221
+ @environment_id = decoded_token[0]["environmentIdentifier"]
222
+ @account_id = decoded_token[0]["accountID"]
223
+
224
+ headers = {
225
+ "Authorization" => "Bearer " + @token,
226
+ "Harness-AccountID" => @account_id,
227
+ "Harness-EnvironmentID" => @environment_id
228
+ }.compact
229
+
230
+ @api.api_client.default_headers = @api.api_client.default_headers.merge(headers)
231
+ @metrics_api.api_client.default_headers = @metrics_api.api_client.default_headers.merge(headers)
212
232
 
213
233
  @config.logger.debug "Token has been processed: environment='" + @environment.to_s + "', cluster='" + @cluster.to_s + "'"
214
234
  else
215
-
216
235
  @config.logger.error "ERROR: Could not obtain the environment and cluster data from the token"
217
236
  end
218
237
  end
@@ -231,6 +250,12 @@ class HarnessConnector < Connector
231
250
 
232
251
  def log_error(e)
233
252
 
234
- @config.logger.error "ERROR - Start\n\n" + e.to_s + "\nERROR - End"
253
+ if e.code == 0
254
+ type = "typhoeus/libcurl"
255
+ else
256
+ type = "HTTP code #{e.code}"
257
+ end
258
+
259
+ @config.logger.warn "OpenapiClient::ApiError (#{type}) [\n\n" + e.to_s + "\n]"
235
260
  end
236
261
  end
@@ -5,7 +5,7 @@ module Ff
5
5
  module Server
6
6
  module Sdk
7
7
 
8
- VERSION = "1.0.6"
8
+ VERSION = "1.1.0"
9
9
  end
10
10
  end
11
11
  end
data/scripts/openapi.sh CHANGED
@@ -57,12 +57,9 @@ if which openapi-generator-cli; then
57
57
  gem install concurrent-ruby -v 1.1.10 && \
58
58
  gem install murmurhash3 -v 0.1.6 && \
59
59
  cd "$dir_path/.." && \
60
- openapi-generator-cli generate -i api.yaml -g ruby -o "$generated_path" && \
61
- cd "$generated_path" && gem build openapi_client.gemspec && \
62
- test -e "openapi_client-1.0.0.gem" && \
63
- gem install --dev "openapi_client-1.0.0.gem"; then
60
+ openapi-generator-cli generate -i api.yaml -g ruby -o "$generated_path"; then
64
61
 
65
- echo "Generated API has been installed with success: $generated_path"
62
+ echo "API has been generated with success: $generated_path"
66
63
  else
67
64
 
68
65
  echo "ERROR: 'openapi-generator-cli' is not installed [1] 😬"
data/scripts/sdk_specs.sh CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/bin/bash
2
2
 
3
3
  export ff_ruby_sdk="ff-ruby-server-sdk"
4
- export ff_ruby_sdk_version="1.0.6"
4
+ export ff_ruby_sdk_version="1.1.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ff-ruby-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 'Miloš Vasić, cyr.: Милош Васић'
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-01-19 00:00:00.000000000 Z
11
+ date: 2023-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -122,20 +122,6 @@ dependencies:
122
122
  - - '='
123
123
  - !ruby/object:Gem::Version
124
124
  version: 2.1.0
125
- - !ruby/object:Gem::Dependency
126
- name: sse-client
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - '='
130
- - !ruby/object:Gem::Version
131
- version: 1.1.0
132
- type: :runtime
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - '='
137
- - !ruby/object:Gem::Version
138
- version: 1.1.0
139
125
  - !ruby/object:Gem::Dependency
140
126
  name: concurrent-ruby
141
127
  requirement: !ruby/object:Gem::Requirement
@@ -165,19 +151,25 @@ dependencies:
165
151
  - !ruby/object:Gem::Version
166
152
  version: 0.1.6
167
153
  - !ruby/object:Gem::Dependency
168
- name: openapi_client
154
+ name: typhoeus
169
155
  requirement: !ruby/object:Gem::Requirement
170
156
  requirements:
171
157
  - - "~>"
172
158
  - !ruby/object:Gem::Version
173
- version: 1.0.0
159
+ version: '1.0'
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: 1.0.1
174
163
  type: :runtime
175
164
  prerelease: false
176
165
  version_requirements: !ruby/object:Gem::Requirement
177
166
  requirements:
178
167
  - - "~>"
179
168
  - !ruby/object:Gem::Version
180
- version: 1.0.0
169
+ version: '1.0'
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 1.0.1
181
173
  description: Harness is a feature management platform that helps teams to build better
182
174
  software and to test features quicker.
183
175
  email:
@@ -198,6 +190,7 @@ files:
198
190
  - api.yaml
199
191
  - bin/console
200
192
  - bin/setup
193
+ - buildInContainer.sh
201
194
  - docs/build.md
202
195
  - docs/further_reading.md
203
196
  - docs/images/ff-gui.png
@@ -207,6 +200,7 @@ files:
207
200
  - example/number_variation_example/number_variation_example.rb
208
201
  - example/simple_example/example.rb
209
202
  - example/string_variation_example/string_variation_example.rb
203
+ - example/tls_example/tls_example.rb
210
204
  - example/url_change_example/url_change_example.rb
211
205
  - lib/ff/ruby/server/generated/lib/openapi_client.rb
212
206
  - lib/ff/ruby/server/generated/lib/openapi_client/api/client_api.rb
@@ -291,7 +285,7 @@ metadata:
291
285
  homepage_uri: https://www.harness.io/
292
286
  source_code_uri: https://github.com/harness/ff-ruby-server-sdk
293
287
  changelog_uri: https://github.com/harness/ff-ruby-server-sdk/blob/main/CHANGELOG.md
294
- post_install_message:
288
+ post_install_message:
295
289
  rdoc_options: []
296
290
  require_paths:
297
291
  - lib
@@ -307,8 +301,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
307
301
  - !ruby/object:Gem::Version
308
302
  version: '0'
309
303
  requirements: []
310
- rubygems_version: 3.1.6
311
- signing_key:
304
+ rubygems_version: 3.4.10
305
+ signing_key:
312
306
  specification_version: 4
313
307
  summary: Harness is a feature management platform that helps teams to build better
314
308
  software and to test features quicker.