solarwinds_apm 7.0.2 → 7.1.1

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.
@@ -18,7 +18,6 @@ module SolarWindsAPM
18
18
  TRIGGERED_TRACE_ATTRIBUTE = 'TriggeredTrace'
19
19
 
20
20
  TRACESTATE_REGEXP = /^[0-9a-f]{16}-[0-9a-f]{2}$/
21
- BUCKET_INTERVAL = 1000
22
21
  DICE_SCALE = 1_000_000
23
22
 
24
23
  OTEL_SAMPLING_DECISION = ::OpenTelemetry::SDK::Trace::Samplers::Decision
@@ -30,34 +29,36 @@ module SolarWindsAPM
30
29
  @counters = SolarWindsAPM::Metrics::Counter.new
31
30
  @buckets = {
32
31
  SolarWindsAPM::BucketType::DEFAULT =>
33
- SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL)),
32
+ SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, 'DEFUALT')),
34
33
  SolarWindsAPM::BucketType::TRIGGER_RELAXED =>
35
- SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL)),
34
+ SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, 'TRIGGER_RELAXED')),
36
35
  SolarWindsAPM::BucketType::TRIGGER_STRICT =>
37
- SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL))
36
+ SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, 'TRIGGER_STRICT'))
38
37
  }
39
38
  @settings = {} # parsed setting from swo backend
40
-
41
- @buckets.each_value(&:start)
39
+ @settings_mutex = ::Mutex.new
42
40
  end
43
41
 
44
42
  # return sampling result
45
43
  # params: {:trace_id=>, :parent_context=>, :links=>, :name=>, :kind=>, :attributes=>}
46
44
  # propagator -> processor -> sampler
47
45
  def should_sample?(params)
48
- @logger.debug { "should_sample? params: #{params.inspect}" }
49
46
  _, parent_context, _, _, _, attributes = params.values
50
47
 
51
48
  parent_span = ::OpenTelemetry::Trace.current_span(parent_context)
52
49
  type = SolarWindsAPM::SpanType.span_type(parent_span)
53
50
 
54
- @logger.debug { "[#{self.class}/#{__method__}] span type is #{type}" }
51
+ @logger.debug { "[#{self.class}/#{__method__}] should_sample? params: #{params.inspect}; span type is #{type}" }
55
52
 
56
53
  # For local spans, we always trust the parent
57
54
  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)
55
+ if parent_span.context.trace_flags.sampled?
56
+ return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE,
57
+ tracestate: DEFAULT_TRACESTATE)
58
+ else
59
+ return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::DROP,
60
+ tracestate: DEFAULT_TRACESTATE)
61
+ end
61
62
  end
62
63
 
63
64
  sample_state = SampleState.new(OTEL_SAMPLING_DECISION::DROP,
@@ -78,10 +79,9 @@ module SolarWindsAPM
78
79
  # TraceOptions.parse_trace_options return TriggerTraceOptions
79
80
  sample_state.trace_options = ::SolarWindsAPM::TraceOptions.parse_trace_options(sample_state.headers['X-Trace-Options'], @logger)
80
81
 
81
- @logger.debug { "X-Trace-Options present: #{sample_state.trace_options}" }
82
+ @logger.debug { "[#{self.class}/#{__method__}] sample_state.trace_options: #{sample_state.trace_options.inspect}" }
82
83
 
83
84
  if sample_state.headers['X-Trace-Options-Signature']
84
- @logger.debug { 'X-Trace-Options-Signature present; validating' }
85
85
 
86
86
  # this validate_signature is the function from trace_options file
87
87
  sample_state.trace_options.response.auth = TraceOptions.validate_signature(
@@ -92,36 +92,27 @@ module SolarWindsAPM
92
92
  )
93
93
 
94
94
  # 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' }
95
+ if sample_state.trace_options.response.auth != Auth::OK
96
+ @logger.debug { "[#{self.class}/#{__method__}] signature invalid; tracing disabled (auth=#{sample_state.trace_options.response.auth})" }
97
97
 
98
98
  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)
99
+ return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::DROP,
100
+ tracestate: xtracestate,
101
+ attributes: sample_state.attributes)
100
102
  end
101
103
  end
102
104
 
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
105
+ # Apply trace options to span attributes and list ignored keys in response
106
+ sample_state.trace_options.response.trigger_trace = TriggerTrace::NOT_REQUESTED unless sample_state.trace_options.trigger_trace
108
107
  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
108
+ sample_state.trace_options.custom.each { |k, v| sample_state.attributes[k] = v }
115
109
  sample_state.trace_options.response.ignored = sample_state.trace_options[:ignored].map { |k, _| k } if sample_state.trace_options[:ignored].any?
116
110
  end
117
111
 
118
112
  unless sample_state.settings
119
- @logger.debug { 'settings unavailable; sampling disabled' }
113
+ @logger.debug { "[#{self.class}/#{__method__}] settings unavailable; sampling disabled" }
120
114
 
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
115
+ sample_state.trace_options.response.trigger_trace = TriggerTrace::SETTINGS_NOT_AVAILABLE if sample_state.trace_options&.trigger_trace
125
116
 
126
117
  xtracestate = generate_new_tracestate(parent_span, sample_state)
127
118
 
@@ -131,31 +122,23 @@ module SolarWindsAPM
131
122
  end
132
123
 
133
124
  @logger.debug { "[#{self.class}/#{__method__}] sample_state before deciding sampling algo: #{sample_state.inspect}" }
125
+
134
126
  # Decide which sampling algo to use and add sampling attribute to decision attributes
135
127
  # https://swicloud.atlassian.net/wiki/spaces/NIT/pages/3815473156/Tracing+Decision+Tree
136
128
  if sample_state.trace_state && TRACESTATE_REGEXP.match?(sample_state.trace_state)
137
- @logger.debug { 'context is valid for parent-based sampling' }
138
129
  parent_based_algo(sample_state)
139
-
140
130
  elsif sample_state.settings[:flags].anybits?(Flags::SAMPLE_START)
141
131
  if sample_state.trace_options&.trigger_trace
142
- @logger.debug { 'trigger trace requested' }
143
132
  trigger_trace_algo(sample_state)
144
133
  else
145
- @logger.debug { 'defaulting to dice roll' }
146
134
  dice_roll_algo(sample_state)
147
135
  end
148
136
  else
149
- @logger.debug { 'SAMPLE_START is unset; sampling disabled' }
150
137
  disabled_algo(sample_state)
151
138
  end
152
139
 
153
- @logger.debug { "final sampling state: #{sample_state.inspect}" }
154
-
155
140
  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)
141
+ @logger.debug { "[#{self.class}/#{__method__}] final sampling state: #{sample_state.inspect}" }
159
142
 
160
143
  OTEL_SAMPLING_RESULT.new(decision: sample_state.decision,
161
144
  tracestate: xtracestate,
@@ -163,13 +146,10 @@ module SolarWindsAPM
163
146
  end
164
147
 
165
148
  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)
149
+ @logger.debug { "[#{self.class}/#{__method__}] parent_based_algo start" }
168
150
 
169
- # compare the parent_id
170
151
  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
152
+ if sample_state.trace_options&.trigger_trace
173
153
  @logger.debug { 'trigger trace requested but ignored' }
174
154
  sample_state.trace_options.response.trigger_trace = TriggerTrace::IGNORED # 'ignored'
175
155
  end
@@ -198,18 +178,22 @@ module SolarWindsAPM
198
178
  @logger.debug { 'parent is sampled; record and sample' }
199
179
 
200
180
  @counters[:trace_count].add(1)
201
- @counters[:through_trace_count].add(1) # ruby metrics only add incremented value and attributes
181
+ @counters[:through_trace_count].add(1)
202
182
 
203
183
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE
204
184
  end
205
185
  end
186
+
187
+ @logger.debug { "[#{self.class}/#{__method__}] parent_based_algo end" }
206
188
  end
207
189
 
208
190
  def trigger_trace_algo(sample_state)
191
+ @logger.debug { "[#{self.class}/#{__method__}] trigger_trace_algo start" }
192
+
209
193
  if sample_state.settings[:flags].nobits?(Flags::TRIGGERED_TRACE)
210
194
  @logger.debug { 'TRIGGERED_TRACE unset; record only' }
211
195
 
212
- sample_state.trace_options.response.trigger_trace = TriggerTrace::TRIGGER_TRACING_DISABLED # 'trigger-tracing-disabled'
196
+ sample_state.trace_options.response.trigger_trace = TriggerTrace::TRIGGER_TRACING_DISABLED
213
197
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
214
198
  else
215
199
  @logger.debug { 'TRIGGERED_TRACE set; trigger tracing' }
@@ -244,9 +228,12 @@ module SolarWindsAPM
244
228
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
245
229
  end
246
230
  end
231
+ @logger.debug { "[#{self.class}/#{__method__}] trigger_trace_algo end" }
247
232
  end
248
233
 
249
234
  def dice_roll_algo(sample_state)
235
+ @logger.debug { "[#{self.class}/#{__method__}] dice_roll_algo start" }
236
+
250
237
  dice = SolarWindsAPM::Dice.new(rate: sample_state.settings[:sample_rate], scale: DICE_SCALE)
251
238
  sample_state.attributes[SAMPLE_RATE_ATTRIBUTE] = dice.rate
252
239
  sample_state.attributes[SAMPLE_SOURCE_ATTRIBUTE] = sample_state.settings[:sample_source]
@@ -278,9 +265,11 @@ module SolarWindsAPM
278
265
  @logger.debug { 'dice roll failure; record only' }
279
266
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
280
267
  end
268
+ @logger.debug { "[#{self.class}/#{__method__}] dice_roll_algo end" }
281
269
  end
282
270
 
283
271
  def disabled_algo(sample_state)
272
+ @logger.debug { "[#{self.class}/#{__method__}] disabled_algo start" }
284
273
  if sample_state.trace_options&.trigger_trace
285
274
  @logger.debug { 'trigger trace requested but tracing disabled' }
286
275
  sample_state.trace_options.response.trigger_trace = TriggerTrace::TRACING_DISABLED
@@ -293,14 +282,17 @@ module SolarWindsAPM
293
282
  @logger.debug { 'SAMPLE_THROUGH_ALWAYS is set; record' }
294
283
  sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
295
284
  end
285
+ @logger.debug { "[#{self.class}/#{__method__}] disabled_algo end" }
296
286
  end
297
287
 
298
288
  def update_settings(settings)
299
- return unless settings[:timestamp] > (@settings[:timestamp] || 0)
289
+ @settings_mutex.synchronize do
290
+ return unless settings[:timestamp] > (@settings[:timestamp] || 0)
300
291
 
301
- @settings = settings
302
- @buckets.each do |type, bucket|
303
- bucket.update(@settings[:buckets][type]) if @settings[:buckets][type]
292
+ @settings = settings
293
+ @buckets.each do |type, bucket|
294
+ bucket.update(@settings[:buckets][type]) if @settings[:buckets][type]
295
+ end
304
296
  end
305
297
  end
306
298
 
@@ -308,41 +300,41 @@ module SolarWindsAPM
308
300
  # handle_response_headers functionality is replace by generate_new_tracestate
309
301
  def generate_new_tracestate(parent_span, sample_state)
310
302
  if !parent_span.context.valid? || parent_span.context.tracestate.nil?
311
- @logger.debug { 'create new tracestate' }
303
+ action = 'create'
312
304
  decision = sw_from_span_and_decision(parent_span, sample_state.decision)
313
305
  trace_state = ::OpenTelemetry::Trace::Tracestate.from_hash({ 'sw' => decision })
314
306
  else
315
- @logger.debug { 'update tracestate' }
307
+ action = 'update'
316
308
  decision = sw_from_span_and_decision(parent_span, sample_state.decision)
317
309
  trace_state = parent_span.context.tracestate.set_value('sw', decision)
318
310
  end
319
311
 
320
312
  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
313
  trace_state = trace_state.set_value('xtrace_options_response', stringified_trace_options)
324
- @logger.debug { "[#{self.class}/#{__method__}] new trace_state: #{trace_state.inspect}" }
314
+ @logger.debug { "[#{self.class}/#{__method__}] Tracestate #{action}: decision=#{decision[-2, 2]}, xtrace_resp=#{stringified_trace_options}, trace_state=#{trace_state.inspect}" }
325
315
  trace_state
326
316
  end
327
317
 
328
318
  def sw_from_span_and_decision(parent_span, otel_decision)
329
319
  trace_flag = otel_decision == OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE ? '01' : '00'
330
- [parent_span.context.hex_span_id, trace_flag].join('-')
320
+ "#{parent_span.context.hex_span_id}-#{trace_flag}"
331
321
  end
332
322
 
333
323
  def get_settings(params)
334
- return if @settings.empty?
335
-
336
- expiry = (@settings[:timestamp] + @settings[:ttl]) * 1000
337
- time_now = Time.now.to_i * 1000
338
- if time_now > expiry
339
- @logger.debug { 'settings expired, removing' }
340
- @settings = {}
341
- return
324
+ @settings_mutex.synchronize do
325
+ return if @settings.empty?
326
+
327
+ expiry = (@settings[:timestamp] + @settings[:ttl]) * 1000
328
+ time_now = Time.now.to_i * 1000
329
+ if time_now > expiry
330
+ @logger.debug { "[#{self.class}/#{__method__}] settings expired, removing" }
331
+ @settings = {}
332
+ return
333
+ end
334
+ sampling_setting = SolarWindsAPM::SamplingSettings.merge(@settings, local_settings(params))
335
+ @logger.debug { "[#{self.class}/#{__method__}] sampling_setting: #{sampling_setting.inspect}" }
336
+ sampling_setting
342
337
  end
343
- sampling_setting = SolarWindsAPM::SamplingSettings.merge(@settings, local_settings(params))
344
- @logger.debug { "sampling_setting: #{sampling_setting.inspect}" }
345
- sampling_setting
346
338
  end
347
339
  end
348
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,7 @@ module SolarWindsAPM
55
55
 
56
56
  TokenBucketSettings = Struct.new(:capacity, # Number
57
57
  :rate, # Number
58
- :interval) # Number
58
+ :type) # String
59
59
 
60
60
  module SampleSource
61
61
  LOCAL_DEFAULT = 2
@@ -105,11 +105,11 @@ module SolarWindsAPM
105
105
  end
106
106
 
107
107
  def self.valid_trace_id?(trace_id)
108
- !!(trace_id =~ VALID_TRACEID_REGEX) && trace_id != INVALID_TRACEID
108
+ VALID_TRACEID_REGEX.match?(trace_id) && trace_id != INVALID_TRACEID
109
109
  end
110
110
 
111
111
  def self.valid_span_id?(span_id)
112
- !!(span_id =~ VALID_SPANID_REGEX) && span_id != INVALID_SPANID
112
+ VALID_SPANID_REGEX.match?(span_id) && span_id != INVALID_SPANID
113
113
  end
114
114
 
115
115
  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
@@ -10,106 +10,88 @@
10
10
  # capacity is updated through update_settings
11
11
  module SolarWindsAPM
12
12
  class TokenBucket
13
- # Maximum value of a signed 32-bit integer
14
- MAX_INTERVAL = (2**31) - 1
15
-
16
- attr_reader :capacity, :rate, :interval, :tokens
13
+ attr_reader :type
17
14
 
18
15
  def initialize(token_bucket_settings)
19
- self.capacity = token_bucket_settings.capacity || 0
20
- self.rate = token_bucket_settings.rate || 0
21
- self.interval = token_bucket_settings.interval || MAX_INTERVAL
22
- self.tokens = @capacity
23
- @timer = nil
16
+ @capacity = token_bucket_settings.capacity || 0
17
+ @rate = token_bucket_settings.rate || 0
18
+ @tokens = @capacity
19
+ @last_update_time = Time.now.to_f
20
+ @type = token_bucket_settings.type
21
+ @lock = ::Mutex.new
24
22
  end
25
23
 
26
- # oboe sampler update_settings will update the token
27
- # (thread safe as update_settings is guarded by mutex from oboe sampler)
28
- def update(settings)
29
- settings.instance_of?(Hash) ? update_from_hash(settings) : update_from_hash(tb_to_hash(settings))
24
+ def capacity
25
+ @lock.synchronize { @capacity }
30
26
  end
31
27
 
32
- def update_from_hash(settings)
33
- if settings[:capacity]
34
- difference = settings[:capacity] - @capacity
35
- self.capacity = settings[:capacity]
36
- self.tokens = @tokens + difference
37
- end
38
-
39
- self.rate = settings[:rate] if settings[:rate]
40
- start
28
+ def rate
29
+ @lock.synchronize { @rate }
41
30
  end
42
31
 
43
- def tb_to_hash(settings)
44
- { capacity: settings.capacity,
45
- rate: settings.rate,
46
- interval: settings.interval }
47
- end
48
-
49
- def capacity=(capacity)
50
- @capacity = [0, capacity].max
51
- end
52
-
53
- def rate=(rate)
54
- @rate = [0, rate].max
55
- end
56
-
57
- # self.interval= sets the @interval and @sleep_interval
58
- # @sleep_interval is used in the timer thread to sleep between replenishing the bucket
59
- def interval=(interval)
60
- @interval = interval.clamp(0, MAX_INTERVAL)
61
- @sleep_interval = @interval / 1000.0
32
+ def tokens
33
+ @lock.synchronize do
34
+ calculate_tokens
35
+ @tokens
36
+ end
62
37
  end
63
38
 
64
- def tokens=(tokens)
65
- @tokens = tokens.clamp(0, @capacity)
39
+ # oboe sampler update_settings will update the token
40
+ def update(settings)
41
+ settings.instance_of?(Hash) ? update_from_hash(settings) : update_from_hash(tb_to_hash(settings))
66
42
  end
67
43
 
68
44
  # Attempts to consume tokens from the bucket
69
- # @param n [Integer] Number of tokens to consume
45
+ # @param token [Integer] Number of tokens to consume
70
46
  # @return [Boolean] Whether there were enough tokens
71
- # TODO: we need to include thread-safety here since sampler is shared across threads
72
- # and we may have multiple threads trying to consume tokens at the same time
73
47
  def consume(token = 1)
74
- if @tokens >= token
75
- self.tokens = @tokens - token
76
- SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Consumed #{token} from total #{@tokens} (#{(@tokens.to_f / @capacity * 100).round(1)}% remaining)" }
77
- true
78
- else
79
- SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Token consumption failed: requested=#{token}, available=#{@tokens}, capacity=#{@capacity}" }
80
- false
81
- end
82
- end
83
-
84
- # Starts replenishing the bucket
85
- def start
86
- return if running
87
-
88
- @timer = Thread.new do
89
- loop do
90
- task
91
- sleep(@sleep_interval)
48
+ @lock.synchronize do
49
+ calculate_tokens
50
+ if @tokens >= token
51
+ @tokens -= token
52
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] #{@type} Consumed #{token} (#{(@tokens.to_f / @capacity * 100).round(1)}% remaining)" }
53
+ true
54
+ else
55
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] #{@type} Token consumption failed: requested=#{token}, available=#{@tokens}, capacity=#{@capacity}" }
56
+ false
92
57
  end
93
58
  end
94
59
  end
95
60
 
96
- # Stops replenishing the bucket
97
- def stop
98
- return unless running
61
+ private
99
62
 
100
- @timer.kill
101
- @timer = nil
63
+ def calculate_tokens
64
+ now = Time.now.to_f
65
+ elapsed = now - @last_update_time
66
+ @last_update_time = now
67
+ @tokens += elapsed * @rate
68
+ @tokens = [@tokens, @capacity].min
102
69
  end
103
70
 
104
- # Whether the bucket is actively being replenished
105
- def running
106
- !@timer.nil? && @timer.alive?
107
- end
71
+ # settings is from json sampler
72
+ def update_from_hash(settings)
73
+ @lock.synchronize do
74
+ calculate_tokens
75
+
76
+ if settings[:capacity]
77
+ new_capacity = [0, settings[:capacity]].max
78
+ difference = new_capacity - @capacity
79
+ @capacity = new_capacity
80
+ @tokens += difference
81
+ @tokens = [0, @tokens].max
82
+ end
108
83
 
109
- private
84
+ @rate = [0, settings[:rate]].max if settings[:rate]
85
+ end
86
+ end
110
87
 
111
- def task
112
- self.tokens = tokens + @rate
88
+ # settings is from http sampler
89
+ def tb_to_hash(settings)
90
+ tb_hash = {}
91
+ tb_hash[:capacity] = settings.capacity if settings.respond_to?(:capacity)
92
+ tb_hash[:rate] = settings.rate if settings.respond_to?(:rate)
93
+ tb_hash[:type] = settings.type if settings.respond_to?(:type)
94
+ tb_hash
113
95
  end
114
96
  end
115
97
  end