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 +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 +50 -57
- data/lib/solarwinds_apm/sampling/sampler.rb +46 -58
- data/lib/solarwinds_apm/sampling/sampling_constants.rb +4 -3
- data/lib/solarwinds_apm/sampling/settings.rb +2 -0
- data/lib/solarwinds_apm/sampling/token_bucket.rb +12 -3
- 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
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 { "
|
|
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
|
|
96
|
-
@logger.debug {
|
|
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,
|
|
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
|
-
|
|
104
|
-
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
@
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__}]
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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,8 @@ module SolarWindsAPM
|
|
|
55
55
|
|
|
56
56
|
TokenBucketSettings = Struct.new(:capacity, # Number
|
|
57
57
|
:rate, # Number
|
|
58
|
-
:interval
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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(@
|
|
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
|
|
13
|
-
SW_KEYS_KEY
|
|
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(';').
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
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
|