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.
@@ -30,13 +30,14 @@ module SolarWindsAPM
30
30
  @counters = SolarWindsAPM::Metrics::Counter.new
31
31
  @buckets = {
32
32
  SolarWindsAPM::BucketType::DEFAULT =>
33
- SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL)),
33
+ SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL, 'DEFUALT')),
34
34
  SolarWindsAPM::BucketType::TRIGGER_RELAXED =>
35
- SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL)),
35
+ SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL, 'TRIGGER_RELAXED')),
36
36
  SolarWindsAPM::BucketType::TRIGGER_STRICT =>
37
- SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL))
37
+ SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL, 'TRIGGER_STRICT'))
38
38
  }
39
39
  @settings = {} # parsed setting from swo backend
40
+ @settings_mutex = ::Mutex.new
40
41
 
41
42
  @buckets.each_value(&:start)
42
43
  end
@@ -45,19 +46,22 @@ module SolarWindsAPM
45
46
  # params: {:trace_id=>, :parent_context=>, :links=>, :name=>, :kind=>, :attributes=>}
46
47
  # propagator -> processor -> sampler
47
48
  def should_sample?(params)
48
- @logger.debug { "should_sample? params: #{params.inspect}" }
49
49
  _, parent_context, _, _, _, attributes = params.values
50
50
 
51
51
  parent_span = ::OpenTelemetry::Trace.current_span(parent_context)
52
52
  type = SolarWindsAPM::SpanType.span_type(parent_span)
53
53
 
54
- @logger.debug { "[#{self.class}/#{__method__}] span type is #{type}" }
54
+ @logger.debug { "[#{self.class}/#{__method__}] should_sample? params: #{params.inspect}; span type is #{type}" }
55
55
 
56
56
  # For local spans, we always trust the parent
57
57
  if type == SolarWindsAPM::SpanType::LOCAL
58
- return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE, tracestate: DEFAULT_TRACESTATE) if parent_span.context.trace_flags.sampled?
59
-
60
- return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::DROP, tracestate: DEFAULT_TRACESTATE)
58
+ if parent_span.context.trace_flags.sampled?
59
+ return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE,
60
+ tracestate: DEFAULT_TRACESTATE)
61
+ else
62
+ return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::DROP,
63
+ tracestate: DEFAULT_TRACESTATE)
64
+ end
61
65
  end
62
66
 
63
67
  sample_state = SampleState.new(OTEL_SAMPLING_DECISION::DROP,
@@ -78,10 +82,9 @@ module SolarWindsAPM
78
82
  # TraceOptions.parse_trace_options return TriggerTraceOptions
79
83
  sample_state.trace_options = ::SolarWindsAPM::TraceOptions.parse_trace_options(sample_state.headers['X-Trace-Options'], @logger)
80
84
 
81
- @logger.debug { "X-Trace-Options present: #{sample_state.trace_options}" }
85
+ @logger.debug { "[#{self.class}/#{__method__}] sample_state.trace_options: #{sample_state.trace_options.inspect}" }
82
86
 
83
87
  if sample_state.headers['X-Trace-Options-Signature']
84
- @logger.debug { 'X-Trace-Options-Signature present; validating' }
85
88
 
86
89
  # this validate_signature is the function from trace_options file
87
90
  sample_state.trace_options.response.auth = TraceOptions.validate_signature(
@@ -92,36 +95,27 @@ module SolarWindsAPM
92
95
  )
93
96
 
94
97
  # If the request has an invalid signature, drop the trace
95
- if sample_state.trace_options.response.auth != Auth::OK # Auth::OK is a string from trace_options.rb: 'ok'
96
- @logger.debug { 'X-Trace-Options-Signature invalid; tracing disabled' }
98
+ if sample_state.trace_options.response.auth != Auth::OK
99
+ @logger.debug { "[#{self.class}/#{__method__}] signature invalid; tracing disabled (auth=#{sample_state.trace_options.response.auth})" }
97
100
 
98
101
  xtracestate = generate_new_tracestate(parent_span, sample_state)
99
- return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::DROP, tracestate: xtracestate, attributes: sample_state.attributes)
102
+ return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::DROP,
103
+ tracestate: xtracestate,
104
+ attributes: sample_state.attributes)
100
105
  end
101
106
  end
102
107
 
103
- unless sample_state.trace_options.trigger_trace
104
- sample_state.trace_options.response.trigger_trace = TriggerTrace::NOT_REQUESTED # 'not-requested'
105
- end
106
-
107
- # Apply trace options to span attributes
108
+ # Apply trace options to span attributes and list ignored keys in response
109
+ sample_state.trace_options.response.trigger_trace = TriggerTrace::NOT_REQUESTED unless sample_state.trace_options.trigger_trace
108
110
  sample_state.attributes[SW_KEYS_ATTRIBUTE] = sample_state.trace_options[:sw_keys] if sample_state.trace_options[:sw_keys]
109
-
110
- sample_state.trace_options.custom.each do |k, v|
111
- sample_state.attributes[k] = v
112
- end
113
-
114
- # List ignored keys in response
111
+ sample_state.trace_options.custom.each { |k, v| sample_state.attributes[k] = v }
115
112
  sample_state.trace_options.response.ignored = sample_state.trace_options[:ignored].map { |k, _| k } if sample_state.trace_options[:ignored].any?
116
113
  end
117
114
 
118
115
  unless sample_state.settings
119
- @logger.debug { 'settings unavailable; sampling disabled' }
116
+ @logger.debug { "[#{self.class}/#{__method__}] settings unavailable; sampling disabled" }
120
117
 
121
- if sample_state.trace_options&.trigger_trace
122
- @logger.debug { 'trigger trace requested but unavailable' }
123
- sample_state.trace_options.response.trigger_trace = TriggerTrace::SETTINGS_NOT_AVAILABLE # 'settings-not-available'
124
- end
118
+ sample_state.trace_options.response.trigger_trace = TriggerTrace::SETTINGS_NOT_AVAILABLE if sample_state.trace_options&.trigger_trace
125
119
 
126
120
  xtracestate = generate_new_tracestate(parent_span, sample_state)
127
121
 
@@ -131,31 +125,23 @@ module SolarWindsAPM
131
125
  end
132
126
 
133
127
  @logger.debug { "[#{self.class}/#{__method__}] sample_state before deciding sampling algo: #{sample_state.inspect}" }
128
+
134
129
  # Decide which sampling algo to use and add sampling attribute to decision attributes
135
130
  # https://swicloud.atlassian.net/wiki/spaces/NIT/pages/3815473156/Tracing+Decision+Tree
136
131
  if sample_state.trace_state && TRACESTATE_REGEXP.match?(sample_state.trace_state)
137
- @logger.debug { 'context is valid for parent-based sampling' }
138
132
  parent_based_algo(sample_state)
139
-
140
133
  elsif sample_state.settings[:flags].anybits?(Flags::SAMPLE_START)
141
134
  if sample_state.trace_options&.trigger_trace
142
- @logger.debug { 'trigger trace requested' }
143
135
  trigger_trace_algo(sample_state)
144
136
  else
145
- @logger.debug { 'defaulting to dice roll' }
146
137
  dice_roll_algo(sample_state)
147
138
  end
148
139
  else
149
- @logger.debug { 'SAMPLE_START is unset; sampling disabled' }
150
140
  disabled_algo(sample_state)
151
141
  end
152
142
 
153
- @logger.debug { "final sampling state: #{sample_state.inspect}" }
154
-
155
143
  xtracestate = generate_new_tracestate(parent_span, sample_state)
156
-
157
- # if need to set 'sw.w3c.tracestate' to attributes
158
- # sample_state.attributes['sw.w3c.tracestate'] = ::SolarWindsAPM::Utils.trace_state_header(xtracestate)
144
+ @logger.debug { "[#{self.class}/#{__method__}] final sampling state: #{sample_state.inspect}" }
159
145
 
160
146
  OTEL_SAMPLING_RESULT.new(decision: sample_state.decision,
161
147
  tracestate: xtracestate,
@@ -163,13 +149,10 @@ module SolarWindsAPM
163
149
  end
164
150
 
165
151
  def parent_based_algo(sample_state)
166
- # original js code: const [context] = s.params
167
- # the context is used for metrics e.g. this.#counters.throughTraceCount.add(1, {}, context)
152
+ @logger.debug { "[#{self.class}/#{__method__}] parent_based_algo start" }
168
153
 
169
- # compare the parent_id
170
154
  sample_state.attributes[PARENT_ID_ATTRIBUTE] = sample_state.trace_state[0, 16]
171
-
172
- if sample_state.trace_options&.trigger_trace # need to implement trace_options
155
+ if sample_state.trace_options&.trigger_trace
173
156
  @logger.debug { 'trigger trace requested but ignored' }
174
157
  sample_state.trace_options.response.trigger_trace = TriggerTrace::IGNORED # 'ignored'
175
158
  end
@@ -198,18 +181,22 @@ module SolarWindsAPM
198
181
  @logger.debug { 'parent is sampled; record and sample' }
199
182
 
200
183
  @counters[:trace_count].add(1)
201
- @counters[:through_trace_count].add(1) # ruby metrics only add incremented value and attributes
184
+ @counters[:through_trace_count].add(1)
202
185
 
203
186
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE
204
187
  end
205
188
  end
189
+
190
+ @logger.debug { "[#{self.class}/#{__method__}] parent_based_algo end" }
206
191
  end
207
192
 
208
193
  def trigger_trace_algo(sample_state)
194
+ @logger.debug { "[#{self.class}/#{__method__}] trigger_trace_algo start" }
195
+
209
196
  if sample_state.settings[:flags].nobits?(Flags::TRIGGERED_TRACE)
210
197
  @logger.debug { 'TRIGGERED_TRACE unset; record only' }
211
198
 
212
- sample_state.trace_options.response.trigger_trace = TriggerTrace::TRIGGER_TRACING_DISABLED # 'trigger-tracing-disabled'
199
+ sample_state.trace_options.response.trigger_trace = TriggerTrace::TRIGGER_TRACING_DISABLED
213
200
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
214
201
  else
215
202
  @logger.debug { 'TRIGGERED_TRACE set; trigger tracing' }
@@ -244,9 +231,12 @@ module SolarWindsAPM
244
231
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
245
232
  end
246
233
  end
234
+ @logger.debug { "[#{self.class}/#{__method__}] trigger_trace_algo end" }
247
235
  end
248
236
 
249
237
  def dice_roll_algo(sample_state)
238
+ @logger.debug { "[#{self.class}/#{__method__}] dice_roll_algo start" }
239
+
250
240
  dice = SolarWindsAPM::Dice.new(rate: sample_state.settings[:sample_rate], scale: DICE_SCALE)
251
241
  sample_state.attributes[SAMPLE_RATE_ATTRIBUTE] = dice.rate
252
242
  sample_state.attributes[SAMPLE_SOURCE_ATTRIBUTE] = sample_state.settings[:sample_source]
@@ -278,9 +268,11 @@ module SolarWindsAPM
278
268
  @logger.debug { 'dice roll failure; record only' }
279
269
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
280
270
  end
271
+ @logger.debug { "[#{self.class}/#{__method__}] dice_roll_algo end" }
281
272
  end
282
273
 
283
274
  def disabled_algo(sample_state)
275
+ @logger.debug { "[#{self.class}/#{__method__}] disabled_algo start" }
284
276
  if sample_state.trace_options&.trigger_trace
285
277
  @logger.debug { 'trigger trace requested but tracing disabled' }
286
278
  sample_state.trace_options.response.trigger_trace = TriggerTrace::TRACING_DISABLED
@@ -293,14 +285,17 @@ module SolarWindsAPM
293
285
  @logger.debug { 'SAMPLE_THROUGH_ALWAYS is set; record' }
294
286
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
295
287
  end
288
+ @logger.debug { "[#{self.class}/#{__method__}] disabled_algo end" }
296
289
  end
297
290
 
298
291
  def update_settings(settings)
299
292
  return unless settings[:timestamp] > (@settings[:timestamp] || 0)
300
293
 
301
- @settings = settings
302
- @buckets.each do |type, bucket|
303
- bucket.update(@settings[:buckets][type]) if @settings[:buckets][type]
294
+ @settings_mutex.synchronize do
295
+ @settings = settings
296
+ @buckets.each do |type, bucket|
297
+ bucket.update(@settings[:buckets][type]) if @settings[:buckets][type]
298
+ end
304
299
  end
305
300
  end
306
301
 
@@ -308,26 +303,24 @@ module SolarWindsAPM
308
303
  # handle_response_headers functionality is replace by generate_new_tracestate
309
304
  def generate_new_tracestate(parent_span, sample_state)
310
305
  if !parent_span.context.valid? || parent_span.context.tracestate.nil?
311
- @logger.debug { 'create new tracestate' }
306
+ action = 'create'
312
307
  decision = sw_from_span_and_decision(parent_span, sample_state.decision)
313
308
  trace_state = ::OpenTelemetry::Trace::Tracestate.from_hash({ 'sw' => decision })
314
309
  else
315
- @logger.debug { 'update tracestate' }
310
+ action = 'update'
316
311
  decision = sw_from_span_and_decision(parent_span, sample_state.decision)
317
312
  trace_state = parent_span.context.tracestate.set_value('sw', decision)
318
313
  end
319
314
 
320
315
  stringified_trace_options = SolarWindsAPM::TraceOptions.stringify_trace_options_response(sample_state.trace_options&.response)
321
- @logger.debug { "[#{self.class}/#{__method__}] stringified_trace_options: #{stringified_trace_options}" }
322
-
323
316
  trace_state = trace_state.set_value('xtrace_options_response', stringified_trace_options)
324
- @logger.debug { "[#{self.class}/#{__method__}] new trace_state: #{trace_state.inspect}" }
317
+ @logger.debug { "[#{self.class}/#{__method__}] Tracestate #{action}: decision=#{decision[-2, 2]}, xtrace_resp=#{stringified_trace_options}, trace_state=#{trace_state.inspect}" }
325
318
  trace_state
326
319
  end
327
320
 
328
321
  def sw_from_span_and_decision(parent_span, otel_decision)
329
322
  trace_flag = otel_decision == OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE ? '01' : '00'
330
- [parent_span.context.hex_span_id, trace_flag].join('-')
323
+ "#{parent_span.context.hex_span_id}-#{trace_flag}"
331
324
  end
332
325
 
333
326
  def get_settings(params)
@@ -336,12 +329,12 @@ module SolarWindsAPM
336
329
  expiry = (@settings[:timestamp] + @settings[:ttl]) * 1000
337
330
  time_now = Time.now.to_i * 1000
338
331
  if time_now > expiry
339
- @logger.debug { 'settings expired, removing' }
332
+ @logger.debug { "[#{self.class}/#{__method__}] settings expired, removing" }
340
333
  @settings = {}
341
334
  return
342
335
  end
343
336
  sampling_setting = SolarWindsAPM::SamplingSettings.merge(@settings, local_settings(params))
344
- @logger.debug { "sampling_setting: #{sampling_setting.inspect}" }
337
+ @logger.debug { "[#{self.class}/#{__method__}] sampling_setting: #{sampling_setting.inspect}" }
345
338
  sampling_setting
346
339
  end
347
340
  end
@@ -21,6 +21,9 @@ module SolarWindsAPM
21
21
  ATTR_URL_PATH = 'url.path'
22
22
  ATTR_HTTP_TARGET = RUBY_SEM_CON::HTTP_TARGET
23
23
 
24
+ SW_XTRACEOPTIONS_KEY = 'sw_xtraceoptions'
25
+ SW_SIGNATURE_KEY = 'sw_signature'
26
+
24
27
  # tracing_mode is getting from SolarWindsAPM::Config
25
28
  def initialize(config, logger)
26
29
  super(logger)
@@ -28,25 +31,22 @@ module SolarWindsAPM
28
31
  @trigger_mode = config[:trigger_trace_enabled]
29
32
  @transaction_settings = config[:transaction_settings]
30
33
  @ready = false
34
+ @logger.debug { "[#{self.class}/#{__method__}] Sampler initialized: tracing_mode=#{@tracing_mode}, trigger_mode=#{@trigger_mode}, transaction_settings_count=#{@transaction_settings.inspect}" }
31
35
  end
32
36
 
33
- # wait for getting the first settings
34
37
  def wait_until_ready(timeout = 10)
35
- thread = Thread.new { settings_ready }
36
- thread.join(timeout) || (thread.kill
37
- false)
38
- @ready
39
- end
40
-
41
- def settings_ready(timeout = 10)
42
- deadline = Time.now
43
- loop do
44
- break unless @settings.empty?
45
-
38
+ deadline = Time.now + timeout
39
+ while Time.now < deadline
40
+ # The @settings hash is populated by another thread (e.g., HttpSampler)
41
+ unless @settings.empty?
42
+ @ready = !@settings[:signature_key].nil?
43
+ return @ready
44
+ end
46
45
  sleep 0.1
47
- break if (Time.now - deadline).round(0) >= timeout
48
46
  end
49
- @ready = true unless @settings[:signature_key].nil?
47
+
48
+ @logger.warn { "[#{self.class}/#{__method__}] Timed out waiting for settings after #{timeout} seconds." }
49
+ @ready # Will be false if timeout is reached
50
50
  end
51
51
 
52
52
  def resolve_tracing_mode(config)
@@ -58,59 +58,45 @@ module SolarWindsAPM
58
58
  def local_settings(params)
59
59
  _trace_id, _parent_context, _links, span_name, span_kind, attributes = params.values
60
60
  settings = { tracing_mode: @tracing_mode, trigger_mode: @trigger_mode }
61
- return settings if @transaction_settings.nil? || @transaction_settings.empty?
62
61
 
63
- @logger.debug { "Current @transaction_settings: #{@transaction_settings.inspect}" }
64
- http_metadata = http_span_metadata(span_kind, attributes)
65
- @logger.debug { "http_metadata: #{http_metadata.inspect}" }
62
+ if @transaction_settings.nil? || @transaction_settings.empty?
63
+ @logger.debug { "[#{self.class}/#{__method__}] No transaction settings, using defaults settings: #{settings.inspect}" }
64
+ else
65
+ http_metadata = http_span_metadata(span_kind, attributes)
66
+ # below is for filter out unwanted transaction
67
+ trans_settings = ::SolarWindsAPM::TransactionSettings.new(url_path: http_metadata[:url], name: span_name, kind: span_kind)
68
+ tracing_mode = trans_settings.calculate_trace_mode == 1 ? TracingMode::ALWAYS : TracingMode::NEVER
66
69
 
67
- # below is for filter out unwanted transaction
68
- trans_settings = ::SolarWindsAPM::TransactionSettings.new(url_path: http_metadata[:url], name: span_name, kind: span_kind)
69
- tracing_mode = trans_settings.calculate_trace_mode == 1 ? TracingMode::ALWAYS : TracingMode::NEVER
70
+ settings[:tracing_mode] = tracing_mode
71
+ end
70
72
 
71
- settings[:tracing_mode] = tracing_mode
73
+ @logger.debug { "[#{self.class}/#{__method__}] Transaction settings after calculation #{settings.inspect}" }
72
74
  settings
73
75
  end
74
76
 
75
77
  # if context have sw-related value, it should be stored in context
76
- # named sw_xtraceoptions in header propagator
77
- # original x_trace_options will parse headers in the class, apm-js separate the task
78
- # apm-js will make headers as hash
78
+ # named sw_xtraceoptions and sw_signature in header from propagator
79
79
  def request_headers(params)
80
- parent_context = params[:parent_context]
81
- header = obtain_sw_value(parent_context, 'sw_xtraceoptions')
82
- signature = obtain_sw_value(parent_context, 'sw_signature')
83
- @logger.debug { "[#{self.class}/#{__method__}] trace_options option_header: #{header}; trace_options sw_signature: #{signature}" }
84
-
85
- {
86
- 'X-Trace-Options' => header,
87
- 'X-Trace-Options-Signature' => signature
88
- }
80
+ header, signature = obtain_traceoptions_headers_signature(params[:parent_context])
81
+ @logger.debug { "[#{self.class}/#{__method__}] trace_options header: #{header.inspect}, signature: #{signature.inspect} from parent_context: #{params[:parent_context].inspect}" }
82
+ { 'X-Trace-Options' => header, 'X-Trace-Options-Signature' => signature }
89
83
  end
90
84
 
91
- def obtain_sw_value(context, type)
92
- sw_value = nil
93
- instance_variable = context&.instance_variable_get('@entries')
94
- instance_variable&.each do |key, value|
95
- next unless key.instance_of?(::String)
96
-
97
- sw_value = value if key == type
98
- end
99
- sw_value
85
+ def obtain_traceoptions_headers_signature(context)
86
+ header = context.value(SW_XTRACEOPTIONS_KEY)
87
+ signature = context.value(SW_SIGNATURE_KEY)
88
+ [header, signature]
100
89
  end
101
90
 
102
91
  def update_settings(settings)
103
92
  parsed = parse_settings(settings)
104
93
  if parsed
105
- @logger.debug { "valid settings #{parsed.inspect} from setting #{settings.inspect}" }
106
-
107
- super(parsed) # call oboe_sampler update_settings function to update the buckets
108
-
94
+ @logger.debug { "[#{self.class}/#{__method__}] Valid settings #{parsed.inspect} from setting #{settings.inspect}" }
109
95
  @logger.warn { "Warning from parsed settings: #{parsed[:warning]}" } if parsed[:warning]
110
-
96
+ super(parsed) # call oboe_sampler update_settings function to update the buckets
111
97
  parsed
112
98
  else
113
- @logger.debug { "invalid settings: #{settings.inspect}" }
99
+ @logger.debug { "[#{self.class}/#{__method__}] Invalid settings: #{settings.inspect}" }
114
100
  nil
115
101
  end
116
102
  end
@@ -136,24 +122,25 @@ module SolarWindsAPM
136
122
  url: url
137
123
  }
138
124
 
139
- @logger.debug { "Retrieved http metadata: #{http_metadata.inspect}" }
125
+ @logger.debug { "[#{self.class}/#{__method__}] Retrieved http metadata: #{http_metadata.inspect}" }
140
126
  http_metadata
141
127
  end
142
128
 
143
129
  def parse_settings(unparsed)
144
130
  return unless unparsed.is_a?(Hash)
145
131
 
146
- return unless unparsed['value'].is_a?(Numeric) &&
147
- unparsed['timestamp'].is_a?(Numeric) &&
148
- unparsed['ttl'].is_a?(Numeric)
149
-
150
132
  sample_rate = unparsed['value']
151
- timestamp = unparsed['timestamp']
152
- ttl = unparsed['ttl']
133
+ timestamp = unparsed['timestamp']
134
+ ttl = unparsed['ttl']
135
+ flags = unparsed['flags']
136
+
137
+ return unless sample_rate.is_a?(Numeric) &&
138
+ timestamp.is_a?(Numeric) &&
139
+ ttl.is_a?(Numeric)
153
140
 
154
- return unless unparsed['flags'].is_a?(String)
141
+ return unless flags.is_a?(String)
155
142
 
156
- flags = unparsed['flags'].split(',').reduce(Flags::OK) do |final_flag, f|
143
+ flags = flags.split(',').reduce(Flags::OK) do |final_flag, f|
157
144
  flag = {
158
145
  'OVERRIDE' => Flags::OVERRIDE,
159
146
  'SAMPLE_START' => Flags::SAMPLE_START,
@@ -167,6 +154,7 @@ module SolarWindsAPM
167
154
 
168
155
  buckets = {}
169
156
  signature_key = nil
157
+ warning = nil
170
158
 
171
159
  if unparsed['arguments'].is_a?(Hash)
172
160
  args = unparsed['arguments']
@@ -55,7 +55,8 @@ module SolarWindsAPM
55
55
 
56
56
  TokenBucketSettings = Struct.new(:capacity, # Number
57
57
  :rate, # Number
58
- :interval) # Number
58
+ :interval, # Number
59
+ :type) # String
59
60
 
60
61
  module SampleSource
61
62
  LOCAL_DEFAULT = 2
@@ -105,11 +106,11 @@ module SolarWindsAPM
105
106
  end
106
107
 
107
108
  def self.valid_trace_id?(trace_id)
108
- !!(trace_id =~ VALID_TRACEID_REGEX) && trace_id != INVALID_TRACEID
109
+ VALID_TRACEID_REGEX.match?(trace_id) && trace_id != INVALID_TRACEID
109
110
  end
110
111
 
111
112
  def self.valid_span_id?(span_id)
112
- !!(span_id =~ VALID_SPANID_REGEX) && span_id != INVALID_SPANID
113
+ VALID_SPANID_REGEX.match?(span_id) && span_id != INVALID_SPANID
113
114
  end
114
115
 
115
116
  def self.span_context_valid?(span_context)
@@ -9,6 +9,7 @@
9
9
  module SolarWindsAPM
10
10
  module SamplingSettings
11
11
  def self.merge(remote, local)
12
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] SamplingSettings merge with remote: #{remote.inspect}; local: #{local.inspect}" }
12
13
  flags = local[:tracing_mode] || remote[:flags]
13
14
 
14
15
  if local[:trigger_mode] == :enabled
@@ -22,6 +23,7 @@ module SolarWindsAPM
22
23
  flags |= SolarWindsAPM::Flags::OVERRIDE
23
24
  end
24
25
 
26
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] final flags: #{flags}" }
25
27
  remote.merge(flags: flags)
26
28
  end
27
29
  end
@@ -13,17 +13,19 @@ module SolarWindsAPM
13
13
  # Maximum value of a signed 32-bit integer
14
14
  MAX_INTERVAL = (2**31) - 1
15
15
 
16
- attr_reader :capacity, :rate, :interval, :tokens
16
+ attr_reader :capacity, :rate, :interval, :tokens, :type
17
17
 
18
18
  def initialize(token_bucket_settings)
19
19
  self.capacity = token_bucket_settings.capacity || 0
20
20
  self.rate = token_bucket_settings.rate || 0
21
21
  self.interval = token_bucket_settings.interval || MAX_INTERVAL
22
22
  self.tokens = @capacity
23
+ @type = token_bucket_settings.type
23
24
  @timer = nil
24
25
  end
25
26
 
26
- # used call from update_settings e.g. bucket.update(bucket_settings)
27
+ # oboe sampler update_settings will update the token
28
+ # (thread safe as update_settings is guarded by mutex from oboe sampler)
27
29
  def update(settings)
28
30
  settings.instance_of?(Hash) ? update_from_hash(settings) : update_from_token_bucket_settings(settings)
29
31
  end
@@ -72,8 +74,11 @@ module SolarWindsAPM
72
74
  @rate = [0, rate].max
73
75
  end
74
76
 
77
+ # self.interval= sets the @interval and @sleep_interval
78
+ # @sleep_interval is used in the timer thread to sleep between replenishing the bucket
75
79
  def interval=(interval)
76
80
  @interval = interval.clamp(0, MAX_INTERVAL)
81
+ @sleep_interval = @interval / 1000.0
77
82
  end
78
83
 
79
84
  def tokens=(tokens)
@@ -83,11 +88,15 @@ module SolarWindsAPM
83
88
  # Attempts to consume tokens from the bucket
84
89
  # @param n [Integer] Number of tokens to consume
85
90
  # @return [Boolean] Whether there were enough tokens
91
+ # TODO: we need to include thread-safety here since sampler is shared across threads
92
+ # and we may have multiple threads trying to consume tokens at the same time
86
93
  def consume(token = 1)
87
94
  if @tokens >= token
88
95
  self.tokens = @tokens - token
96
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] #{@type} Consumed #{token} from total #{@tokens} (#{(@tokens.to_f / @capacity * 100).round(1)}% remaining)" }
89
97
  true
90
98
  else
99
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] #{@type} Token consumption failed: requested=#{token}, available=#{@tokens}, capacity=#{@capacity}" }
91
100
  false
92
101
  end
93
102
  end
@@ -99,7 +108,7 @@ module SolarWindsAPM
99
108
  @timer = Thread.new do
100
109
  loop do
101
110
  task
102
- sleep(@interval / 1000.0)
111
+ sleep(@sleep_interval)
103
112
  end
104
113
  end
105
114
  end
@@ -9,63 +9,66 @@
9
9
  module SolarWindsAPM
10
10
  class TraceOptions
11
11
  TRIGGER_TRACE_KEY = 'trigger-trace'
12
- TIMESTAMP_KEY = 'ts'
13
- SW_KEYS_KEY = 'sw-keys'
14
-
15
- CUSTOM_KEY_REGEX = /^custom-[^\s]*$/
12
+ TIMESTAMP_KEY = 'ts'
13
+ SW_KEYS_KEY = 'sw-keys'
14
+ CUSTOM_KEY_REGEX = /^custom-[^\s]*$/
16
15
 
17
16
  def self.parse_trace_options(header, logger)
17
+ logger.debug { "[#{self.class}/#{__method__}] Parsing trace options header: #{header&.slice(0, 100)}..." }
18
18
  trace_options = TriggerTraceOptions.new(nil, nil, nil, {}, [], TraceOptionsResponse.new(nil, nil, []))
19
19
 
20
- kvs = header.split(';').map do |kv|
20
+ kvs = header.split(';').filter_map do |kv|
21
21
  key, *values = kv.split('=').map(&:strip)
22
+ next if key.nil? || key.empty?
23
+
22
24
  value = values.any? ? values.join('=') : nil
23
25
  [key, value]
24
26
  end
25
27
 
26
- kvs.reject! { |key, _| key.nil? || key.empty? }
28
+ logger.debug { "[#{self.class}/#{__method__}] Parsed kvs #{kvs.inspect}" }
27
29
 
28
30
  kvs.each do |k, v|
29
31
  case k
30
32
  when TRIGGER_TRACE_KEY
31
33
  if v || trace_options.trigger_trace
32
- logger.debug { 'invalid trace option for trigger trace' }
34
+ logger.debug { "[#{self.class}/#{__method__}] invalid trace option for trigger trace: value=#{v}, already_set=#{trace_options.trigger_trace}" }
33
35
  trace_options.ignored << [k, v]
34
36
  next
35
37
  end
36
38
  trace_options.trigger_trace = true
37
39
  when TIMESTAMP_KEY
38
40
  if v.nil? || trace_options.timestamp
39
- logger.debug { 'invalid trace option for timestamp' }
41
+ logger.debug { "[#{self.class}/#{__method__}] invalid trace option for timestamp: value=#{v}, already_set=#{trace_options.timestamp}" }
40
42
  trace_options.ignored << [k, v]
41
43
  next
42
44
  end
43
45
 
44
46
  unless numeric_integer?(v)
45
- logger.debug { 'invalid trace option for timestamp, should be an integer' }
47
+ logger.debug { "[#{self.class}/#{__method__}] invalid trace option for timestamp, should be an integer: #{v}" }
46
48
  trace_options.ignored << [k, v]
47
49
  next
48
50
  end
49
51
  trace_options.timestamp = v.to_i
50
52
  when SW_KEYS_KEY
51
53
  if v.nil? || trace_options.sw_keys
52
- logger.debug { 'invalid trace option for sw keys' }
54
+ logger.debug { "[#{self.class}/#{__method__}] invalid trace option for sw keys: value=#{v}, already_set=#{trace_options.sw_keys}" }
53
55
  trace_options.ignored << [k, v]
54
56
  next
55
57
  end
56
58
  trace_options.sw_keys = v
57
59
  when CUSTOM_KEY_REGEX
58
60
  if v.nil? || trace_options.custom[k]
59
- logger.debug { "invalid trace option for custom key #{k}" }
61
+ logger.debug { "[#{self.class}/#{__method__}] invalid trace option for custom key #{k}: value=#{v}, already_set=#{trace_options.custom[k]}" }
60
62
  trace_options.ignored << [k, v]
61
63
  next
62
64
  end
63
65
  trace_options.custom[k] = v
64
66
  else
67
+ logger.debug { "[#{self.class}/#{__method__}] Unknown key ignored: #{k}=#{v}" }
65
68
  trace_options.ignored << [k, v]
66
69
  end
67
70
  end
68
-
71
+ logger.debug { "[#{self.class}/#{__method__}] Parsing complete: trigger_trace=#{trace_options.trigger_trace}, timestamp=#{trace_options.timestamp}, sw_keys=#{trace_options.sw_keys}, custom_keys=#{trace_options.custom}, ignored=#{trace_options.ignored}" }
69
72
  trace_options
70
73
  end
71
74
 
@@ -86,15 +89,29 @@ module SolarWindsAPM
86
89
  'trigger-trace': trace_options_response.trigger_trace,
87
90
  ignored: trace_options_response.ignored.empty? ? nil : trace_options_response.ignored.join(',')
88
91
  }
89
- kvs.compact.map { |k, v| "#{k}:#{v}" }.join(';')
92
+
93
+ kvs.compact!
94
+ result = kvs.map { |k, v| "#{k}:#{v}" }.join(';')
95
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Stringified trace options response: #{result}" }
96
+ result
90
97
  end
91
98
 
92
99
  def self.validate_signature(header, signature, key, timestamp)
93
- return Auth::NO_SIGNATURE_KEY unless key
94
- return Auth::BAD_TIMESTAMP unless timestamp && (Time.now.to_i - timestamp).abs <= 5 * 60
100
+ unless key
101
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Signature validation failed: no signature key available" }
102
+ return Auth::NO_SIGNATURE_KEY
103
+ end
104
+
105
+ unless timestamp && (Time.now.to_i - timestamp).abs <= 5 * 60
106
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Signature validation failed: bad timestamp (diff more than 300s)" }
107
+ return Auth::BAD_TIMESTAMP
108
+ end
95
109
 
96
110
  digest = OpenSSL::HMAC.hexdigest('SHA1', key, header)
97
- signature == digest ? Auth::OK : Auth::BAD_SIGNATURE
111
+ is_valid = signature == digest
112
+
113
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Signature validation result: #{is_valid ? 'valid' : 'invalid'}" }
114
+ is_valid ? Auth::OK : Auth::BAD_SIGNATURE
98
115
  end
99
116
  end
100
117
  end
@@ -10,7 +10,6 @@ require 'json'
10
10
  require 'fileutils'
11
11
  require 'tempfile'
12
12
  require 'uri'
13
- require 'opentelemetry-sdk'
14
13
 
15
14
  require_relative 'sampling/sampling_constants'
16
15
  require_relative 'sampling/dice'