ff-ruby-server-sdk 1.0.6 → 1.1.0

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