launchdarkly-server-sdk 5.6.2 → 5.7.0

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