solarwinds_apm 7.0.1 → 7.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: 62a494962e1b42497287295bec1c5ad4ae5dfa9b57df068d8897707e89e31c73
4
- data.tar.gz: dd32d8923f0f93776bad22e506aee1a639c59f153cb67b4c944f92d4018f36d8
3
+ metadata.gz: d565e746144994b5c3a81ee19665ee9b2d4e81fbad2e21da567465455c37e6c0
4
+ data.tar.gz: 949351c94748745d2ba19268a531ba5b4f805568132772dcfb9749b0af3698a8
5
5
  SHA512:
6
- metadata.gz: f28f6c76e3baa28539dc87704f1199e336cea2024a5394e229594d81a77f21280a216738e3bd3a53f5f9a3922a237925e85dd86789e86ff5d47e00b831acb0d0
7
- data.tar.gz: e9ad6b91c3d1da150dba81c28a974fbeafe376dbe24be277ec27aeabca58809083c526446d9d66b4b705fe1b304b2b337d3073f8bc094b6e3f5f827709f63388
6
+ metadata.gz: 209a50b78aa94dc85805ef8c6c08db7584966ca62293c53aa9744852170c3c84ac4ef24b2e43e508bad5d1e505f4cd82d72d984a00823232b9a9e13bddb061c6
7
+ data.tar.gz: '0962ab7325e66d3f330361b9f0187f75c07eabcd2d0d400db0d55e75c2124aefa2fe630cbe2eef506f48f58f6e6d3cb6ad1d36f1a816845618b6bd2649e8b951'
data/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  The `solarwinds_apm` gem starting from version 6.0.0 is an [OpenTelemetry Ruby](https://opentelemetry.io/docs/instrumentation/ruby/) distribution. It provides automatic instrumentation and custom SolarWinds Observability features for Ruby applications.
4
4
 
5
5
  ## Requirements
6
+ >
6
7
  > [!NOTE]
7
8
  > Versions before 7.0.0 only support Linux and will go into no-op mode on other platforms.
8
9
 
@@ -21,15 +21,16 @@ module SolarWindsAPM
21
21
  #
22
22
  # === Example:
23
23
  #
24
- # class DogfoodsController < ApplicationController
24
+ # class OrdersController < ApplicationController
25
25
  #
26
26
  # def create
27
- # @dogfood = Dogfood.new(params.permit(:brand, :name))
28
- # @dogfood.save
27
+ # @order = Order.new(params.permit(:item, :quantity))
28
+ # if @order.save
29
+ # custom_name = "orderscontroller.create_for_#{params[:item]}"
30
+ # SolarWindsAPM::API.set_transaction_name(custom_name)
31
+ # end
29
32
  #
30
- # SolarWindsAPM::API.set_transaction_name("dogfoodscontroller.create_for_#{params[:brand]}")
31
- #
32
- # redirect_to @dogfood
33
+ # redirect_to @order
33
34
  # end
34
35
  #
35
36
  # end
@@ -87,21 +87,32 @@ module SolarWindsAPM
87
87
  SolarWindsAPM.logger.level = SW_LOG_LEVEL_MAPPING.dig(log_level, :stdlib) || ::Logger::INFO # default log level info
88
88
  end
89
89
 
90
+ # e.g. enable_disable_config('STRING', :key, value, false, bool: true)
90
91
  def self.enable_disable_config(env_var, key, value, default, bool: false)
91
- env_value = ENV[env_var.to_s]&.downcase
92
+ raw_env_value = ENV.fetch(env_var, '')
93
+ env_value = raw_env_value.downcase
92
94
  valid_env_values = bool ? %w[true false] : %w[enabled disabled]
93
95
 
94
- if env_var && valid_env_values.include?(env_value)
96
+ if !env_var.empty? && valid_env_values.include?(env_value)
95
97
  value = bool ? true?(env_value) : env_value.to_sym
96
- elsif env_var && !env_value.to_s.empty?
97
- SolarWindsAPM.logger.warn("[#{name}/#{__method__}] #{env_var} must be #{valid_env_values.join('/')} (current setting is #{ENV.fetch(env_var, nil)}). Using default value: #{default}.")
98
- return @@config[key.to_sym] = default
98
+ elsif !env_var.empty? && !raw_env_value.empty?
99
+ SolarWindsAPM.logger.warn do
100
+ "[#{name}/#{__method__}] #{env_var} must be #{valid_env_values.join('/')} (current setting is #{raw_env_value}). Using default value: #{default}."
101
+ end
102
+ return @@config[key] = default
99
103
  end
100
104
 
101
- return @@config[key.to_sym] = value unless (bool && !boolean?(value)) || (!bool && !symbol?(value))
105
+ # Validate final value efficiently
106
+ valid = bool ? boolean?(value) : symbol?(value)
107
+
108
+ unless valid
109
+ SolarWindsAPM.logger.warn do
110
+ "[#{name}/#{__method__}] :#{key} must be #{valid_env_values.join('/')}. Using default value: #{default}."
111
+ end
112
+ return @@config[key] = default
113
+ end
102
114
 
103
- SolarWindsAPM.logger.warn("[#{name}/#{__method__}] :#{key} must be a #{valid_env_values.join('/')}. Using default value: #{default}.")
104
- @@config[key.to_sym] = default
115
+ @@config[key] = value
105
116
  end
106
117
 
107
118
  def self.true?(obj)
@@ -211,7 +222,7 @@ module SolarWindsAPM
211
222
  enable_disable_config('SW_APM_TRIGGER_TRACING_MODE', key, value, :enabled)
212
223
 
213
224
  when :tracing_mode
214
- enable_disable_config(nil, key, value, :enabled)
225
+ enable_disable_config('', key, value, :enabled)
215
226
 
216
227
  when :tag_sql
217
228
  enable_disable_config('SW_APM_TAG_SQL', key, value, false, bool: true)
@@ -242,9 +253,17 @@ module SolarWindsAPM
242
253
  return
243
254
  end
244
255
 
245
- # `tracing: disabled` is the default
246
- disabled = settings.select { |v| !v.key?(:tracing) || v[:tracing] == :disabled }
247
- enabled = settings.select { |v| v[:tracing] == :enabled }
256
+ # `tracing: disabled` is the default; below only separate the enabled and disabled settings
257
+ result = settings.each_with_object({ enabled: [], disabled: [] }) do |setting, acc|
258
+ if setting[:tracing] == :enabled
259
+ acc[:enabled] << setting
260
+ elsif !setting.key?(:tracing) || setting[:tracing] == :disabled
261
+ acc[:disabled] << setting
262
+ end
263
+ end
264
+
265
+ enabled = result[:enabled]
266
+ disabled = result[:disabled]
248
267
 
249
268
  SolarWindsAPM::Config[:enabled_regexps] = compile_regexp(enabled)
250
269
  SolarWindsAPM::Config[:disabled_regexps] = compile_regexp(disabled)
@@ -21,12 +21,15 @@ module SolarWindsAPM
21
21
  HTTP_STATUS_CODE = 'http.status_code'
22
22
  HTTP_URL = 'http.url'
23
23
 
24
+ HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code'
25
+ HTTP_REQUEST_METHOD = 'http.request.method'
26
+
24
27
  INVALID_HTTP_STATUS_CODE = 0
25
28
 
26
29
  def initialize(txn_manager)
27
30
  @txn_manager = txn_manager
28
- @meters = { 'sw.apm.request.metrics' => ::OpenTelemetry.meter_provider.meter('sw.apm.request.metrics') }
29
31
  @metrics = init_response_time_metrics
32
+ @is_lambda = SolarWindsAPM::Utils.determine_lambda
30
33
  @transaction_name = nil
31
34
  end
32
35
 
@@ -40,21 +43,24 @@ module SolarWindsAPM
40
43
  trace_flags = span.context.trace_flags.sampled? ? '01' : '00'
41
44
  @txn_manager&.set_root_context_h(span.context.hex_trace_id, "#{span.context.hex_span_id}-#{trace_flags}")
42
45
  span.add_attributes({ SW_IS_ENTRY_SPAN => true })
46
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_start end" }
43
47
  rescue StandardError => e
44
48
  SolarWindsAPM.logger.info { "[#{self.class}/#{__method__}] processor on_start error: #{e.message}" }
45
49
  end
46
50
 
47
51
  def on_finishing(span)
52
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finishing span attributes: #{span.attributes}" }
48
53
  return if non_entry_span(span: span)
49
54
 
50
55
  @transaction_name = calculate_transaction_names(span)
51
56
  span.set_attribute(SW_TRANSACTION_NAME, @transaction_name)
52
57
  @txn_manager.delete_root_context_h(span.context.hex_trace_id)
58
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finishing end" }
53
59
  end
54
60
 
55
61
  # @param [Span] span the (immutable) {Span} that just ended.
56
62
  def on_finish(span)
57
- SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish span: #{span.to_span_data.inspect}" }
63
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish span attributes: #{span.attributes}" }
58
64
  return if non_entry_span(span: span)
59
65
 
60
66
  record_request_metrics(span)
@@ -63,10 +69,9 @@ module SolarWindsAPM
63
69
  ::OpenTelemetry.meter_provider.metric_readers.each do |reader|
64
70
  reader.pull if reader.respond_to? :pull
65
71
  end
66
-
67
- SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish succeed" }
72
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish end" }
68
73
  rescue StandardError => e
69
- SolarWindsAPM.logger.info { "[#{self.class}/#{__method__}] error processing span on_finish: #{e.message}" }
74
+ SolarWindsAPM.logger.info { "[#{self.class}/#{__method__}] processor on_finish error: #{e.message}" }
70
75
  end
71
76
 
72
77
  # @param [optional Numeric] timeout An optional timeout in seconds.
@@ -94,7 +99,9 @@ module SolarWindsAPM
94
99
  unit: 'ms')
95
100
  end
96
101
 
97
- instrument = @meters['sw.apm.request.metrics'].create_histogram('trace.service.response_time', unit: 'ms', description: 'Duration of each entry span for the service, typically meaning the time taken to process an inbound request.')
102
+ meter = ::OpenTelemetry.meter_provider.meter('sw.apm.request.metrics')
103
+ instrument = meter.create_histogram('trace.service.response_time', unit: 'ms', description: 'Duration of each entry span for the service, typically meaning the time taken to process an inbound request.')
104
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Adding ExponentialBucketHistogram for response time metrics: #{instrument.inspect}" }
98
105
  { response_time: instrument }
99
106
  end
100
107
 
@@ -104,37 +111,43 @@ module SolarWindsAPM
104
111
  SW_TRANSACTION_NAME => @transaction_name
105
112
  }
106
113
 
107
- if span_http?(span)
114
+ is_http_span = span_http?(span)
115
+
116
+ if is_http_span
108
117
  http_status_code = get_http_status_code(span)
109
- meter_attrs['http.status_code'] = http_status_code if http_status_code != 0
110
- meter_attrs['http.method'] = span.attributes[HTTP_METHOD] if span.attributes[HTTP_METHOD]
118
+ meter_attrs[HTTP_STATUS_CODE] = http_status_code if http_status_code != 0
119
+ meter_attrs[HTTP_METHOD] = span.attributes[HTTP_METHOD] if span.attributes[HTTP_METHOD]
120
+ meter_attrs[HTTP_METHOD] = span.attributes[HTTP_REQUEST_METHOD] if span.attributes[HTTP_REQUEST_METHOD]
111
121
  end
112
- SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] meter_attrs: #{meter_attrs.inspect}" }
122
+
123
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] is_http_span: #{is_http_span}; meter_attrs: #{meter_attrs.inspect}" }
113
124
  meter_attrs.compact!
114
125
  meter_attrs
115
126
  end
116
127
 
117
128
  def calculate_lambda_transaction_name(span_name)
118
- (ENV['SW_APM_TRANSACTION_NAME'] || ENV['AWS_LAMBDA_FUNCTION_NAME'] || span_name || 'unknown').slice(0, SolarWindsAPM::Constants::MAX_TXN_NAME_LENGTH)
129
+ txn_name = (ENV['SW_APM_TRANSACTION_NAME'] || ENV['AWS_LAMBDA_FUNCTION_NAME'] || span_name || 'unknown').slice(0, SolarWindsAPM::Constants::MAX_TXN_NAME_LENGTH)
130
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Lambda transaction name: #{txn_name} (from env_txn=#{ENV.fetch('SW_APM_TRANSACTION_NAME', nil)}, lambda_func=#{ENV.fetch('AWS_LAMBDA_FUNCTION_NAME', nil)}, span_name=#{span_name})" }
131
+ txn_name
119
132
  end
120
133
 
121
134
  # Get trans_name and url_tran of this span instance.
122
135
  # Predecessor order: custom SDK > env var SW_APM_TRANSACTION_NAME > automatic naming
123
136
  def calculate_transaction_names(span)
124
- return calculate_lambda_transaction_name(span.name) if SolarWindsAPM::Utils.determine_lambda
137
+ return calculate_lambda_transaction_name(span.name) if @is_lambda
125
138
 
126
139
  trace_span_id = "#{span.context.hex_trace_id}-#{span.context.hex_span_id}"
127
140
  trans_name = @txn_manager.get(trace_span_id)
128
141
  if trans_name
129
- SolarWindsAPM.logger.debug do
130
- "[#{self.class}/#{__method__}] found trans name from txn_manager: #{trans_name} by #{trace_span_id}"
131
- end
142
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Using transaction name from txn_manager: #{trans_name} (#{trace_span_id})" }
132
143
  @txn_manager.del(trace_span_id)
133
144
  elsif !ENV['SW_APM_TRANSACTION_NAME'].to_s.empty?
134
145
  trans_name = ENV.fetch('SW_APM_TRANSACTION_NAME', nil)
146
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Using transaction name from env var: #{trans_name}" }
135
147
  else
136
- trans_name = span.attributes[HTTP_ROUTE] || nil
137
- trans_name = span.name if trans_name.to_s.empty? && span.name
148
+ trans_name = span.attributes[HTTP_ROUTE]
149
+ trans_name = span.name if trans_name.to_s.empty?
150
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Using transaction name from span.attributes: #{span.attributes[HTTP_ROUTE]} or span.name: #{span.name}" }
138
151
  end
139
152
  trans_name.to_s.slice(0, SolarWindsAPM::Constants::MAX_TXN_NAME_LENGTH)
140
153
  end
@@ -163,14 +176,14 @@ module SolarWindsAPM
163
176
 
164
177
  # This span from inbound HTTP request if from a SERVER by some http.method
165
178
  def span_http?(span)
166
- span.kind == ::OpenTelemetry::Trace::SpanKind::SERVER && !span.attributes[HTTP_METHOD].nil?
179
+ (!span.attributes[HTTP_METHOD].nil? || !span.attributes[HTTP_REQUEST_METHOD].nil?) && span.kind == ::OpenTelemetry::Trace::SpanKind::SERVER
167
180
  end
168
181
 
169
182
  # Calculate HTTP status_code from span or default to UNAVAILABLE
170
183
  # Something went wrong in OTel or instrumented service crashed early
171
184
  # if no status_code in attributes of HTTP span
172
185
  def get_http_status_code(span)
173
- span.attributes[HTTP_STATUS_CODE] || INVALID_HTTP_STATUS_CODE
186
+ span.attributes[HTTP_RESPONSE_STATUS_CODE] || span.attributes[HTTP_STATUS_CODE] || INVALID_HTTP_STATUS_CODE
174
187
  end
175
188
 
176
189
  # check if it's entry span based on no parent or parent is remote
@@ -40,6 +40,9 @@ module SolarWindsAPM
40
40
  context = ::OpenTelemetry::Context.new({}) if context.nil?
41
41
  context = inject_extracted_header(carrier, context, getter, XTRACEOPTIONS_HEADER_NAME, INTL_SWO_X_OPTIONS_KEY)
42
42
  inject_extracted_header(carrier, context, getter, XTRACEOPTIONS_SIGNATURE_HEADER_NAME, INTL_SWO_SIGNATURE_KEY)
43
+ rescue StandardError => e
44
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Extraction failed: #{e.message}" }
45
+ context || ::OpenTelemetry::Context.current
43
46
  end
44
47
 
45
48
  # Inject trace context into the supplied carrier.
@@ -80,10 +83,13 @@ module SolarWindsAPM
80
83
  end
81
84
  setter.set(carrier, TRACESTATE_HEADER_NAME, Utils.trace_state_header(trace_state))
82
85
  end
86
+ rescue StandardError => e
87
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Injection failed: #{e.message}" }
83
88
  end
84
89
 
85
- # Returns the predefined propagation fields. If your carrier is reused, you
86
- # should delete the fields returned by this method before calling +inject+.
90
+ # Returns the predefined propagation fields, required by upstream.
91
+ # If your carrier is reused, you should delete the fields returned by
92
+ # this method before calling +inject+.
87
93
  #
88
94
  # @return [Array<String>] a list of fields that will be used by this propagator.
89
95
  def fields
@@ -56,31 +56,21 @@ module SolarWindsAPM
56
56
  xtraceoptions_response)
57
57
  end
58
58
  setter.set(carrier, HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, exposed_headers.join(','))
59
- end
60
-
61
- # Returns the predefined propagation fields. If your carrier is reused, you
62
- # should delete the fields returned by this method before calling +inject+.
63
- #
64
- # @return [Array<String>] a list of fields that will be used by this propagator.
65
- def fields
66
- TRACESTATE_HEADER_NAME
59
+ rescue StandardError => e
60
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Injection failed: #{e.message}" }
67
61
  end
68
62
 
69
63
  private
70
64
 
71
- # sw_xtraceoptions_response_key -> xtrace_options_response
65
+ # SW_XTRACEOPTIONS_RESPONSE_KEY -> xtrace_options_response
72
66
  def recover_response_from_tracestate(span_context)
73
67
  sanitized = span_context.tracestate.value(SW_XTRACEOPTIONS_RESPONSE_KEY)
74
68
  sanitized = '' if sanitized.nil?
75
69
  sanitized = sanitized.gsub(SolarWindsAPM::Constants::INTL_SWO_EQUALS_W3C_SANITIZED,
76
70
  SolarWindsAPM::Constants::INTL_SWO_EQUALS)
77
71
  sanitized = sanitized.gsub(':', SolarWindsAPM::Constants::INTL_SWO_EQUALS)
78
- sanitized = sanitized.gsub(SolarWindsAPM::Constants::INTL_SWO_COMMA_W3C_SANITIZED,
79
- SolarWindsAPM::Constants::INTL_SWO_COMMA)
80
- SolarWindsAPM.logger.debug do
81
- "[#{self.class}/#{__method__}] recover_response_from_tracestate sanitized: #{sanitized.inspect}"
82
- end
83
- sanitized
72
+ sanitized.gsub(SolarWindsAPM::Constants::INTL_SWO_COMMA_W3C_SANITIZED,
73
+ SolarWindsAPM::Constants::INTL_SWO_COMMA)
84
74
  end
85
75
  end
86
76
  end
@@ -11,6 +11,9 @@ require 'opentelemetry-metrics-sdk'
11
11
  require 'opentelemetry-exporter-otlp'
12
12
  require 'opentelemetry-exporter-otlp-metrics'
13
13
  require 'opentelemetry-instrumentation-all'
14
+ require 'opentelemetry-logs-sdk'
15
+ require 'opentelemetry-exporter-otlp-logs'
16
+ require 'opentelemetry-instrumentation-logger'
14
17
 
15
18
  require_relative 'opentelemetry/solarwinds_propagator'
16
19
  require_relative 'opentelemetry/solarwinds_response_propagator'
@@ -25,8 +25,6 @@ module SolarWindsAPM
25
25
 
26
26
  is_lambda = SolarWindsAPM::Utils.determine_lambda
27
27
 
28
- ENV['OTEL_TRACES_EXPORTER'] = ENV['OTEL_TRACES_EXPORTER'].to_s.split(',').tap { |e| e << 'otlp' unless e.include?('otlp') }.join(',')
29
-
30
28
  # add response propagator to rack instrumentation
31
29
  resolve_response_propagator
32
30
 
@@ -74,6 +72,9 @@ module SolarWindsAPM
74
72
  ENV['OTEL_LOG_LEVEL'] = SolarWindsAPM::Config::SW_LOG_LEVEL_MAPPING.dig(log_level, :otel)
75
73
  end
76
74
 
75
+ # disable log bridge by default
76
+ ENV['OTEL_RUBY_INSTRUMENTATION_LOGGER_ENABLED'] = 'false' unless %w[true false].include?(ENV['OTEL_RUBY_INSTRUMENTATION_LOGGER_ENABLED'].to_s)
77
+
77
78
  ::OpenTelemetry::SDK.configure do |c|
78
79
  c.resource = final_attributes
79
80
  c.use_all(@@config_map)
@@ -12,7 +12,7 @@ module SolarWindsAPM
12
12
  attr_reader :rate, :scale
13
13
 
14
14
  def initialize(settings)
15
- @scale = settings[:scale]
15
+ @scale = settings[:scale] || 1_000_000
16
16
  @rate = settings[:rate] || 0
17
17
  @random_generator = Random.new
18
18
  end
@@ -25,6 +25,7 @@ module SolarWindsAPM
25
25
  @pid = nil
26
26
  @thread = nil
27
27
 
28
+ @logger.debug { "[#{self.class}/#{__method__}] HttpSampler initialized: url=#{@url}, service=#{@service}, hostname=#{@hostname}, setting_url=#{@setting_url}" }
28
29
  reset_on_fork
29
30
  end
30
31
 
@@ -57,7 +58,6 @@ module SolarWindsAPM
57
58
  def fetch_with_timeout(url, timeout_seconds = nil)
58
59
  uri = url
59
60
  timeout = timeout_seconds || REQUEST_TIMEOUT
60
-
61
61
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
62
62
 
63
63
  remaining = lambda {
@@ -94,26 +94,33 @@ module SolarWindsAPM
94
94
 
95
95
  # a endless loop within a thread (non-blocking)
96
96
  def settings_request
97
+ @logger.debug { "[#{self.class}/#{__method__}] Starting settings request loop" }
98
+ sleep_duration = GET_SETTING_DURATION
97
99
  loop do
98
- @logger.debug { "Retrieving sampling settings from #{@setting_url}" }
99
-
100
100
  response = fetch_with_timeout(@setting_url)
101
- parsed = response.nil? ? nil : JSON.parse(response.body)
102
101
 
103
- @logger.debug { "parsed settings in json: #{parsed.inspect}" }
102
+ # Check for nil response from timeout
103
+ unless response.is_a?(Net::HTTPSuccess)
104
+ @logger.warn { "[#{self.class}/#{__method__}] Failed to retrieve settings due to timeout." }
105
+ next
106
+ end
107
+
108
+ parsed = JSON.parse(response.body)
104
109
 
105
110
  if update_settings(parsed)
106
111
  # update the settings before the previous ones expire with some time to spare
107
112
  expiry = (parsed['timestamp'].to_i + parsed['ttl'].to_i)
108
113
  expiry_timeout = expiry - REQUEST_TIMEOUT - Time.now.to_i
109
- sleep([0, expiry_timeout].max)
114
+ sleep_duration = [0, expiry_timeout].max
110
115
  else
111
- @logger.warn { 'Retrieved sampling settings are invalid. Ensure proper configuration.' }
112
- sleep(GET_SETTING_DURATION)
116
+ @logger.warn { "[#{self.class}/#{__method__}] Retrieved sampling settings are invalid. Ensure proper configuration." }
113
117
  end
118
+ rescue JSON::ParserError => e
119
+ @logger.warn { "[#{self.class}/#{__method__}] JSON parsing error: #{e.message}" }
114
120
  rescue StandardError => e
115
- @logger.warn { "Failed to retrieve sampling settings (#{e.message}), tracing will be disabled until valid ones are available." }
116
- sleep(GET_SETTING_DURATION)
121
+ @logger.warn { "[#{self.class}/#{__method__}] Failed to retrieve sampling settings (#{e.message}), tracing will be disabled until valid ones are available." }
122
+ ensure
123
+ sleep(sleep_duration)
117
124
  end
118
125
  end
119
126
  end
@@ -15,11 +15,12 @@ module SolarWindsAPM
15
15
 
16
16
  @path = path || DEFAULT_PATH
17
17
  @expiry = Time.now.to_i
18
+ @last_mtime = nil
19
+ @logger.debug { "[#{self.class}/#{__method__}] JsonSampler initialized: path=#{@path}, initial_expiry=#{@expiry}" }
18
20
  loop_check
19
21
  end
20
22
 
21
23
  # only json sampler will need to check if the settings.json file
22
- # updated or not from collector extention
23
24
  def should_sample?(params)
24
25
  loop_check
25
26
  super
@@ -27,26 +28,40 @@ module SolarWindsAPM
27
28
 
28
29
  private
29
30
 
31
+ # multi-thread is rare in lambda environment,
32
+ # here we don't use mutex to guard the execution
30
33
  def loop_check
31
- # Update if we're within 10s of expiry
32
- return if Time.now.to_i + 10 < @expiry
34
+ return if Time.now.to_i < @expiry - 10
33
35
 
36
+ # 1. Read and parse settings from the file.
34
37
  begin
35
- contents = File.read(@path)
36
- unparsed = JSON.parse(contents)
37
- rescue StandardError => e
38
- @logger.debug { "missing or invalid settings file; Error: #{e.message}" }
38
+ current_mtime = File.mtime(@path)
39
+ return if @last_mtime && current_mtime == @last_mtime
40
+
41
+ settings_data = JSON.parse(File.read(@path))
42
+ @last_mtime = current_mtime
43
+ rescue Errno::ENOENT
44
+ # File doesn't exist due to timing, missing collector, etc
45
+ @logger.error { "[#{self.class}##{__method__}] Settings file not found at #{@path}." }
46
+ return
47
+ rescue JSON::ParserError => e
48
+ @logger.error { "[#{self.class}##{__method__}] JSON parsing error in #{@path}: #{e.message}" }
39
49
  return
40
50
  end
41
51
 
42
- unless unparsed.is_a?(Array) && unparsed.length == 1
43
- @logger.debug { "invalid settings file : #{unparsed}" }
52
+ # 2. Validate the structure of the parsed settings.
53
+ unless settings_data.is_a?(Array) && settings_data.length == 1
54
+ @logger.error { "[#{self.class}##{__method__}] Invalid settings file content: #{settings_data.inspect}" }
44
55
  return
45
56
  end
46
57
 
47
- parsed = update_settings(unparsed.first)
48
- @logger.debug { "update_settings: #{parsed}" }
49
- @expiry = parsed[:timestamp].to_i + parsed[:ttl].to_i if parsed
58
+ # 3. Attempt to update the settings.
59
+ if (new_settings = update_settings(settings_data.first))
60
+ @expiry = new_settings[:timestamp].to_i + new_settings[:ttl].to_i
61
+ @logger.debug { "[#{self.class}##{__method__}] Settings #{new_settings} updated successfully. New expiry: #{@expiry}" }
62
+ else
63
+ @logger.debug { "[#{self.class}##{__method__}] Settings update failed, keeping current expiry: #{@expiry}" }
64
+ end
50
65
  end
51
66
  end
52
67
  end