launchdarkly-server-sdk 5.6.2 → 5.7.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.
@@ -22,7 +22,7 @@ module LaunchDarkly
22
22
  meta = {}
23
23
  with_details = !details_only_if_tracked || flag[:trackEvents]
24
24
  if !with_details && flag[:debugEventsUntilDate]
25
- with_details = flag[:debugEventsUntilDate] > (Time.now.to_f * 1000).to_i
25
+ with_details = flag[:debugEventsUntilDate] > Impl::Util::current_time_millis
26
26
  end
27
27
  if with_details
28
28
  meta[:version] = flag[:version]
@@ -0,0 +1,130 @@
1
+ require "ldclient-rb/impl/util"
2
+
3
+ require "rbconfig"
4
+ require "securerandom"
5
+
6
+ module LaunchDarkly
7
+ module Impl
8
+ class DiagnosticAccumulator
9
+ def self.create_diagnostic_id(sdk_key)
10
+ {
11
+ diagnosticId: SecureRandom.uuid,
12
+ sdkKeySuffix: sdk_key[-6..-1] || sdk_key
13
+ }
14
+ end
15
+
16
+ def initialize(diagnostic_id)
17
+ @id = diagnostic_id
18
+ @lock = Mutex.new
19
+ self.reset(Util.current_time_millis)
20
+ end
21
+
22
+ def reset(time)
23
+ @data_since_date = time
24
+ @stream_inits = []
25
+ end
26
+
27
+ def create_init_event(config)
28
+ return {
29
+ kind: 'diagnostic-init',
30
+ creationDate: Util.current_time_millis,
31
+ id: @id,
32
+ configuration: DiagnosticAccumulator.make_config_data(config),
33
+ sdk: DiagnosticAccumulator.make_sdk_data(config),
34
+ platform: DiagnosticAccumulator.make_platform_data
35
+ }
36
+ end
37
+
38
+ def record_stream_init(timestamp, failed, duration_millis)
39
+ @lock.synchronize do
40
+ @stream_inits.push({ timestamp: timestamp, failed: failed, durationMillis: duration_millis })
41
+ end
42
+ end
43
+
44
+ def create_periodic_event_and_reset(dropped_events, deduplicated_users, events_in_last_batch)
45
+ previous_stream_inits = @lock.synchronize do
46
+ si = @stream_inits
47
+ @stream_inits = []
48
+ si
49
+ end
50
+
51
+ current_time = Util.current_time_millis
52
+ event = {
53
+ kind: 'diagnostic',
54
+ creationDate: current_time,
55
+ id: @id,
56
+ dataSinceDate: @data_since_date,
57
+ droppedEvents: dropped_events,
58
+ deduplicatedUsers: deduplicated_users,
59
+ eventsInLastBatch: events_in_last_batch,
60
+ streamInits: previous_stream_inits
61
+ }
62
+ @data_since_date = current_time
63
+ event
64
+ end
65
+
66
+ def self.make_config_data(config)
67
+ ret = {
68
+ allAttributesPrivate: config.all_attributes_private,
69
+ connectTimeoutMillis: self.seconds_to_millis(config.connect_timeout),
70
+ customBaseURI: config.base_uri != Config.default_base_uri,
71
+ customEventsURI: config.events_uri != Config.default_events_uri,
72
+ customStreamURI: config.stream_uri != Config.default_stream_uri,
73
+ diagnosticRecordingIntervalMillis: self.seconds_to_millis(config.diagnostic_recording_interval),
74
+ eventsCapacity: config.capacity,
75
+ eventsFlushIntervalMillis: self.seconds_to_millis(config.flush_interval),
76
+ inlineUsersInEvents: config.inline_users_in_events,
77
+ pollingIntervalMillis: self.seconds_to_millis(config.poll_interval),
78
+ socketTimeoutMillis: self.seconds_to_millis(config.read_timeout),
79
+ streamingDisabled: !config.stream?,
80
+ userKeysCapacity: config.user_keys_capacity,
81
+ userKeysFlushIntervalMillis: self.seconds_to_millis(config.user_keys_flush_interval),
82
+ usingProxy: ENV.has_key?('http_proxy') || ENV.has_key?('https_proxy') || ENV.has_key?('HTTP_PROXY'),
83
+ usingRelayDaemon: config.use_ldd?,
84
+ }
85
+ ret
86
+ end
87
+
88
+ def self.make_sdk_data(config)
89
+ ret = {
90
+ name: 'ruby-server-sdk',
91
+ version: LaunchDarkly::VERSION
92
+ }
93
+ if config.wrapper_name
94
+ ret[:wrapperName] = config.wrapper_name
95
+ ret[:wrapperVersion] = config.wrapper_version
96
+ end
97
+ ret
98
+ end
99
+
100
+ def self.make_platform_data
101
+ conf = RbConfig::CONFIG
102
+ {
103
+ name: 'ruby',
104
+ osArch: conf['host_cpu'],
105
+ osName: self.normalize_os_name(conf['host_os']),
106
+ osVersion: 'unknown', # there seems to be no portable way to detect this in Ruby
107
+ rubyVersion: conf['ruby_version'],
108
+ rubyImplementation: Object.constants.include?(:RUBY_ENGINE) ? RUBY_ENGINE : 'unknown'
109
+ }
110
+ end
111
+
112
+ def self.normalize_os_name(name)
113
+ case name
114
+ when /linux|arch/i
115
+ 'Linux'
116
+ when /darwin/i
117
+ 'MacOS'
118
+ when /mswin|windows/i
119
+ 'Windows'
120
+ else
121
+ name
122
+ end
123
+ end
124
+
125
+ def self.seconds_to_millis(s)
126
+ (s * 1000).to_i
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,72 @@
1
+ require "securerandom"
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ EventSenderResult = Struct.new(:success, :must_shutdown, :time_from_server)
6
+
7
+ class EventSender
8
+ CURRENT_SCHEMA_VERSION = 3
9
+ DEFAULT_RETRY_INTERVAL = 1
10
+
11
+ def initialize(sdk_key, config, http_client = nil, retry_interval = DEFAULT_RETRY_INTERVAL)
12
+ @client = http_client ? http_client : LaunchDarkly::Util.new_http_client(config.events_uri, config)
13
+ @sdk_key = sdk_key
14
+ @config = config
15
+ @events_uri = config.events_uri + "/bulk"
16
+ @diagnostic_uri = config.events_uri + "/diagnostic"
17
+ @logger = config.logger
18
+ @retry_interval = retry_interval
19
+ end
20
+
21
+ def send_event_data(event_data, is_diagnostic)
22
+ uri = is_diagnostic ? @diagnostic_uri : @events_uri
23
+ payload_id = is_diagnostic ? nil : SecureRandom.uuid
24
+ description = is_diagnostic ? 'diagnostic event' : "#{event_data.length} events"
25
+ res = nil
26
+ (0..1).each do |attempt|
27
+ if attempt > 0
28
+ @logger.warn { "[LDClient] Will retry posting events after #{@retry_interval} second" }
29
+ sleep(@retry_interval)
30
+ end
31
+ begin
32
+ @client.start if !@client.started?
33
+ @logger.debug { "[LDClient] sending #{description}: #{body}" }
34
+ req = Net::HTTP::Post.new(uri)
35
+ req.content_type = "application/json"
36
+ req.body = event_data
37
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| req[k] = v }
38
+ if !is_diagnostic
39
+ req["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
40
+ req["X-LaunchDarkly-Payload-ID"] = payload_id
41
+ end
42
+ req["Connection"] = "keep-alive"
43
+ res = @client.request(req)
44
+ rescue StandardError => exn
45
+ @logger.warn { "[LDClient] Error sending events: #{exn.inspect}." }
46
+ next
47
+ end
48
+ status = res.code.to_i
49
+ if status >= 200 && status < 300
50
+ res_time = nil
51
+ if !res["date"].nil?
52
+ begin
53
+ res_time = Time.httpdate(res["date"])
54
+ rescue ArgumentError
55
+ end
56
+ end
57
+ return EventSenderResult.new(true, false, res_time)
58
+ end
59
+ must_shutdown = !LaunchDarkly::Util.http_error_recoverable?(status)
60
+ can_retry = !must_shutdown && attempt == 0
61
+ message = LaunchDarkly::Util.http_error_message(status, "event delivery", can_retry ? "will retry" : "some events were dropped")
62
+ @logger.error { "[LDClient] #{message}" }
63
+ if must_shutdown
64
+ return EventSenderResult.new(false, true, nil)
65
+ end
66
+ end
67
+ # used up our retries
68
+ return EventSenderResult.new(false, false, nil)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module LaunchDarkly
3
+ module Impl
4
+ module Util
5
+ def self.current_time_millis
6
+ (Time.now.to_f * 1000).to_i
7
+ end
8
+
9
+ def self.default_http_headers(sdk_key, config)
10
+ ret = { "Authorization" => sdk_key, "User-Agent" => "RubyClient/" + LaunchDarkly::VERSION }
11
+ if config.wrapper_name
12
+ ret["X-LaunchDarkly-Wrapper"] = config.wrapper_name +
13
+ (config.wrapper_version ? "/" + config.wrapper_version : "")
14
+ end
15
+ ret
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/diagnostic_events"
1
2
  require "ldclient-rb/impl/event_factory"
2
3
  require "ldclient-rb/impl/store_client_wrapper"
3
4
  require "concurrent/atomics"
@@ -46,10 +47,16 @@ module LaunchDarkly
46
47
  updated_config.instance_variable_set(:@feature_store, @store)
47
48
  @config = updated_config
48
49
 
50
+ if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
51
+ diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
52
+ else
53
+ diagnostic_accumulator = nil
54
+ end
55
+
49
56
  if @config.offline? || !@config.send_events
50
57
  @event_processor = NullEventProcessor.new
51
58
  else
52
- @event_processor = EventProcessor.new(sdk_key, config)
59
+ @event_processor = EventProcessor.new(sdk_key, config, diagnostic_accumulator)
53
60
  end
54
61
 
55
62
  if @config.use_ldd?
@@ -59,7 +66,13 @@ module LaunchDarkly
59
66
 
60
67
  data_source_or_factory = @config.data_source || self.method(:create_default_data_source)
61
68
  if data_source_or_factory.respond_to? :call
62
- @data_source = data_source_or_factory.call(sdk_key, @config)
69
+ # Currently, data source factories take two parameters unless they need to be aware of diagnostic_accumulator, in
70
+ # which case they take three parameters. This will be changed in the future to use a less awkware mechanism.
71
+ if data_source_or_factory.arity == 3
72
+ @data_source = data_source_or_factory.call(sdk_key, @config, diagnostic_accumulator)
73
+ else
74
+ @data_source = data_source_or_factory.call(sdk_key, @config)
75
+ end
63
76
  else
64
77
  @data_source = data_source_or_factory
65
78
  end
@@ -335,13 +348,13 @@ module LaunchDarkly
335
348
 
336
349
  private
337
350
 
338
- def create_default_data_source(sdk_key, config)
351
+ def create_default_data_source(sdk_key, config, diagnostic_accumulator)
339
352
  if config.offline?
340
353
  return NullUpdateProcessor.new
341
354
  end
342
355
  requestor = Requestor.new(sdk_key, config)
343
356
  if config.stream?
344
- StreamProcessor.new(sdk_key, config, requestor)
357
+ StreamProcessor.new(sdk_key, config, requestor, diagnostic_accumulator)
345
358
  else
346
359
  config.logger.info { "Disabling streaming API" }
347
360
  config.logger.warn { "You should only disable the streaming API if instructed to do so by LaunchDarkly support" }
@@ -51,8 +51,7 @@ module LaunchDarkly
51
51
  @client.start if !@client.started?
52
52
  uri = URI(@config.base_uri + path)
53
53
  req = Net::HTTP::Get.new(uri)
54
- req["Authorization"] = @sdk_key
55
- req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
54
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| req[k] = v }
56
55
  req["Connection"] = "keep-alive"
57
56
  cached = @cache.read(uri)
58
57
  if !cached.nil?
@@ -24,7 +24,7 @@ module LaunchDarkly
24
24
 
25
25
  # @private
26
26
  class StreamProcessor
27
- def initialize(sdk_key, config, requestor)
27
+ def initialize(sdk_key, config, requestor, diagnostic_accumulator = nil)
28
28
  @sdk_key = sdk_key
29
29
  @config = config
30
30
  @feature_store = config.feature_store
@@ -33,6 +33,7 @@ module LaunchDarkly
33
33
  @started = Concurrent::AtomicBoolean.new(false)
34
34
  @stopped = Concurrent::AtomicBoolean.new(false)
35
35
  @ready = Concurrent::Event.new
36
+ @connection_attempt_start_time = 0
36
37
  end
37
38
 
38
39
  def initialized?
@@ -44,18 +45,17 @@ module LaunchDarkly
44
45
 
45
46
  @config.logger.info { "[LDClient] Initializing stream connection" }
46
47
 
47
- headers = {
48
- 'Authorization' => @sdk_key,
49
- 'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION
50
- }
48
+ headers = Impl::Util.default_http_headers(@sdk_key, @config)
51
49
  opts = {
52
50
  headers: headers,
53
51
  read_timeout: READ_TIMEOUT_SECONDS,
54
52
  logger: @config.logger
55
53
  }
54
+ log_connection_started
56
55
  @es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
57
56
  conn.on_event { |event| process_message(event) }
58
57
  conn.on_error { |err|
58
+ log_connection_result(false)
59
59
  case err
60
60
  when SSE::Errors::HTTPStatusError
61
61
  status = err.status
@@ -82,6 +82,7 @@ module LaunchDarkly
82
82
  private
83
83
 
84
84
  def process_message(message)
85
+ log_connection_result(true)
85
86
  method = message.type
86
87
  @config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
87
88
  if method == PUT
@@ -137,5 +138,17 @@ module LaunchDarkly
137
138
  def key_for_path(kind, path)
138
139
  path.start_with?(KEY_PATHS[kind]) ? path[KEY_PATHS[kind].length..-1] : nil
139
140
  end
141
+
142
+ def log_connection_started
143
+ @connection_attempt_start_time = Impl::Util::current_time_millis
144
+ end
145
+
146
+ def log_connection_result(is_success)
147
+ if !@diagnostic_accumulator.nil? && @connection_attempt_start_time > 0
148
+ @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success,
149
+ Impl::Util::current_time_millis - @connection_attempt_start_time)
150
+ @connection_attempt_start_time = 0
151
+ end
152
+ end
140
153
  end
141
154
  end
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "5.6.2"
2
+ VERSION = "5.7.0"
3
3
  end
@@ -0,0 +1,163 @@
1
+ require "ldclient-rb/impl/diagnostic_events"
2
+
3
+ require "spec_helper"
4
+
5
+ module LaunchDarkly
6
+ module Impl
7
+ describe DiagnosticAccumulator do
8
+ subject { DiagnosticAccumulator }
9
+
10
+ let(:sdk_key) { "sdk_key" }
11
+ let(:default_id) { subject.create_diagnostic_id("my-key") }
12
+ let(:default_acc) { subject.new(default_id) }
13
+
14
+ it "creates unique ID with SDK key suffix" do
15
+ id1 = subject.create_diagnostic_id("1234567890")
16
+ expect(id1[:sdkKeySuffix]).to eq "567890"
17
+ expect(id1[:diagnosticId]).not_to be_nil
18
+
19
+ id2 = subject.create_diagnostic_id("1234567890")
20
+ expect(id2[:diagnosticId]).not_to eq id1[:diagnosticId]
21
+ end
22
+
23
+ describe "init event" do
24
+ def expected_default_config
25
+ {
26
+ allAttributesPrivate: false,
27
+ connectTimeoutMillis: Config.default_connect_timeout * 1000,
28
+ customBaseURI: false,
29
+ customEventsURI: false,
30
+ customStreamURI: false,
31
+ diagnosticRecordingIntervalMillis: Config.default_diagnostic_recording_interval * 1000,
32
+ eventsCapacity: Config.default_capacity,
33
+ eventsFlushIntervalMillis: Config.default_flush_interval * 1000,
34
+ inlineUsersInEvents: false,
35
+ pollingIntervalMillis: Config.default_poll_interval * 1000,
36
+ socketTimeoutMillis: Config.default_read_timeout * 1000,
37
+ streamingDisabled: false,
38
+ userKeysCapacity: Config.default_user_keys_capacity,
39
+ userKeysFlushIntervalMillis: Config.default_user_keys_flush_interval * 1000,
40
+ usingProxy: false,
41
+ usingRelayDaemon: false
42
+ }
43
+ end
44
+
45
+ it "has basic fields" do
46
+ event = default_acc.create_init_event(Config.new)
47
+ expect(event[:kind]).to eq 'diagnostic-init'
48
+ expect(event[:creationDate]).not_to be_nil
49
+ expect(event[:id]).to eq default_id
50
+ end
51
+
52
+ it "can have default config data" do
53
+ event = default_acc.create_init_event(Config.new)
54
+ expect(event[:configuration]).to eq expected_default_config
55
+ end
56
+
57
+ it "can have custom config data" do
58
+ changes_and_expected = [
59
+ [ { all_attributes_private: true }, { allAttributesPrivate: true } ],
60
+ [ { connect_timeout: 46 }, { connectTimeoutMillis: 46000 } ],
61
+ [ { base_uri: 'http://custom' }, { customBaseURI: true } ],
62
+ [ { events_uri: 'http://custom' }, { customEventsURI: true } ],
63
+ [ { stream_uri: 'http://custom' }, { customStreamURI: true } ],
64
+ [ { diagnostic_recording_interval: 9999 }, { diagnosticRecordingIntervalMillis: 9999000 } ],
65
+ [ { capacity: 4000 }, { eventsCapacity: 4000 } ],
66
+ [ { flush_interval: 46 }, { eventsFlushIntervalMillis: 46000 } ],
67
+ [ { inline_users_in_events: true }, { inlineUsersInEvents: true } ],
68
+ [ { poll_interval: 999 }, { pollingIntervalMillis: 999000 } ],
69
+ [ { read_timeout: 46 }, { socketTimeoutMillis: 46000 } ],
70
+ [ { stream: false }, { streamingDisabled: true } ],
71
+ [ { user_keys_capacity: 999 }, { userKeysCapacity: 999 } ],
72
+ [ { user_keys_flush_interval: 999 }, { userKeysFlushIntervalMillis: 999000 } ],
73
+ [ { use_ldd: true }, { usingRelayDaemon: true } ]
74
+ ]
75
+ changes_and_expected.each do |config_values, expected_values|
76
+ config = Config.new(config_values)
77
+ event = default_acc.create_init_event(config)
78
+ expect(event[:configuration]).to eq expected_default_config.merge(expected_values)
79
+ end
80
+ end
81
+
82
+ it "detects proxy" do
83
+ begin
84
+ ENV["http_proxy"] = 'http://my-proxy'
85
+ event = default_acc.create_init_event(Config.new)
86
+ expect(event[:configuration][:usingProxy]).to be true
87
+ ensure
88
+ ENV["http_proxy"] = nil
89
+ end
90
+ end
91
+
92
+ it "has expected SDK data" do
93
+ event = default_acc.create_init_event(Config.new)
94
+ expect(event[:sdk]).to eq ({
95
+ name: 'ruby-server-sdk',
96
+ version: LaunchDarkly::VERSION
97
+ })
98
+ end
99
+
100
+ it "has expected SDK data with wrapper" do
101
+ event = default_acc.create_init_event(Config.new(wrapper_name: 'my-wrapper', wrapper_version: '2.0'))
102
+ expect(event[:sdk]).to eq ({
103
+ name: 'ruby-server-sdk',
104
+ version: LaunchDarkly::VERSION,
105
+ wrapperName: 'my-wrapper',
106
+ wrapperVersion: '2.0'
107
+ })
108
+ end
109
+
110
+ it "has expected platform data" do
111
+ event = default_acc.create_init_event(Config.new)
112
+ expect(event[:platform]).to include ({
113
+ name: 'ruby'
114
+ })
115
+ end
116
+ end
117
+
118
+ describe "periodic event" do
119
+ it "has correct default values" do
120
+ acc = subject.new(default_id)
121
+ event = acc.create_periodic_event_and_reset(2, 3, 4)
122
+ expect(event).to include({
123
+ kind: 'diagnostic',
124
+ id: default_id,
125
+ droppedEvents: 2,
126
+ deduplicatedUsers: 3,
127
+ eventsInLastBatch: 4,
128
+ streamInits: []
129
+ })
130
+ expect(event[:creationDate]).not_to be_nil
131
+ expect(event[:dataSinceDate]).not_to be_nil
132
+ end
133
+
134
+ it "can add stream init" do
135
+ acc = subject.new(default_id)
136
+ acc.record_stream_init(1000, false, 2000)
137
+ event = acc.create_periodic_event_and_reset(0, 0, 0)
138
+ expect(event[:streamInits]).to eq [{ timestamp: 1000, failed: false, durationMillis: 2000 }]
139
+ end
140
+
141
+ it "resets fields after creating event" do
142
+ acc = subject.new(default_id)
143
+ acc.record_stream_init(1000, false, 2000)
144
+ event1 = acc.create_periodic_event_and_reset(2, 3, 4)
145
+ event2 = acc.create_periodic_event_and_reset(5, 6, 7)
146
+ expect(event1).to include ({
147
+ droppedEvents: 2,
148
+ deduplicatedUsers: 3,
149
+ eventsInLastBatch: 4,
150
+ streamInits: [{ timestamp: 1000, failed: false, durationMillis: 2000 }]
151
+ })
152
+ expect(event2).to include ({
153
+ dataSinceDate: event1[:creationDate],
154
+ droppedEvents: 5,
155
+ deduplicatedUsers: 6,
156
+ eventsInLastBatch: 7,
157
+ streamInits: []
158
+ })
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end