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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/solarwinds_apm/api/transaction_name.rb +7 -6
- data/lib/solarwinds_apm/config.rb +31 -12
- data/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +32 -19
- data/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb +8 -2
- data/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb +5 -15
- data/lib/solarwinds_apm/opentelemetry.rb +3 -0
- data/lib/solarwinds_apm/otel_config.rb +3 -2
- data/lib/solarwinds_apm/sampling/dice.rb +1 -1
- data/lib/solarwinds_apm/sampling/http_sampler.rb +17 -10
- data/lib/solarwinds_apm/sampling/json_sampler.rb +27 -12
- data/lib/solarwinds_apm/sampling/oboe_sampler.rb +62 -70
- data/lib/solarwinds_apm/sampling/sampler.rb +46 -58
- data/lib/solarwinds_apm/sampling/sampling_constants.rb +3 -3
- data/lib/solarwinds_apm/sampling/settings.rb +2 -0
- data/lib/solarwinds_apm/sampling/token_bucket.rb +58 -76
- data/lib/solarwinds_apm/sampling/trace_options.rb +33 -16
- data/lib/solarwinds_apm/sampling.rb +0 -1
- data/lib/solarwinds_apm/support/resource_detector.rb +11 -16
- data/lib/solarwinds_apm/support/transaction_settings.rb +12 -5
- data/lib/solarwinds_apm/version.rb +2 -2
- metadata +44 -2
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 { "
|
|
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
|
|
96
|
-
@logger.debug {
|
|
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,
|
|
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
|
-
|
|
104
|
-
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
289
|
+
@settings_mutex.synchronize do
|
|
290
|
+
return unless settings[:timestamp] > (@settings[:timestamp] || 0)
|
|
300
291
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__}]
|
|
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
|
-
|
|
320
|
+
"#{parent_span.context.hex_span_id}-#{trace_flag}"
|
|
331
321
|
end
|
|
332
322
|
|
|
333
323
|
def get_settings(params)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
tracing_mode = trans_settings.calculate_trace_mode == 1 ? TracingMode::ALWAYS : TracingMode::NEVER
|
|
70
|
+
settings[:tracing_mode] = tracing_mode
|
|
71
|
+
end
|
|
70
72
|
|
|
71
|
-
|
|
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
|
-
|
|
81
|
-
header
|
|
82
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 { "
|
|
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 { "
|
|
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
|
|
152
|
-
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
|
|
141
|
+
return unless flags.is_a?(String)
|
|
155
142
|
|
|
156
|
-
flags =
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
33
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
97
|
-
def stop
|
|
98
|
-
return unless running
|
|
61
|
+
private
|
|
99
62
|
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
#
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
84
|
+
@rate = [0, settings[:rate]].max if settings[:rate]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
110
87
|
|
|
111
|
-
|
|
112
|
-
|
|
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
|