solarwinds_apm 6.1.2 → 7.0.0.prev1
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 +4 -2
- data/lib/solarwinds_apm/api/current_trace_info.rb +10 -6
- data/lib/solarwinds_apm/api/custom_metrics.rb +8 -25
- data/lib/solarwinds_apm/api/tracing.rb +12 -27
- data/lib/solarwinds_apm/api/transaction_name.rb +6 -10
- data/lib/solarwinds_apm/config.rb +1 -1
- data/lib/solarwinds_apm/constants.rb +1 -0
- data/lib/solarwinds_apm/noop/api.rb +5 -2
- data/lib/solarwinds_apm/noop.rb +0 -24
- data/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +90 -69
- data/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb +0 -2
- data/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb +5 -4
- data/lib/solarwinds_apm/opentelemetry.rb +5 -7
- data/lib/solarwinds_apm/otel_native_config.rb +177 -0
- data/lib/solarwinds_apm/patch/README.md +15 -0
- data/lib/solarwinds_apm/{noop/metadata.rb → sampling/dice.rb} +19 -17
- data/lib/solarwinds_apm/sampling/http_sampler.rb +87 -0
- data/lib/solarwinds_apm/sampling/json_sampler.rb +52 -0
- data/lib/solarwinds_apm/sampling/metrics.rb +38 -0
- data/lib/solarwinds_apm/sampling/oboe_sampler.rb +348 -0
- data/lib/solarwinds_apm/sampling/sampler.rb +197 -0
- data/lib/solarwinds_apm/sampling/sampling_constants.rb +127 -0
- data/lib/solarwinds_apm/sampling/sampling_patch.rb +49 -0
- data/lib/solarwinds_apm/sampling/setting_example.txt +1 -0
- data/lib/solarwinds_apm/{noop/context.rb → sampling/settings.rb} +14 -25
- data/lib/solarwinds_apm/sampling/token_bucket.rb +126 -0
- data/lib/solarwinds_apm/sampling/trace_options.rb +100 -0
- data/lib/solarwinds_apm/{patch.rb → sampling.rb} +20 -4
- data/lib/solarwinds_apm/{noop/span.rb → support/aws_resource_detector.rb} +5 -18
- data/lib/solarwinds_apm/support/logger_formatter.rb +1 -1
- data/lib/solarwinds_apm/support/logging_log_event.rb +1 -1
- data/lib/solarwinds_apm/support/lumberjack_formatter.rb +1 -1
- data/lib/solarwinds_apm/support/otlp_endpoint.rb +99 -0
- data/lib/solarwinds_apm/support/resource_detector/aws/beanstalk.rb +51 -0
- data/lib/solarwinds_apm/support/resource_detector/aws/ec2.rb +145 -0
- data/lib/solarwinds_apm/support/resource_detector/aws/ecs.rb +173 -0
- data/lib/solarwinds_apm/support/resource_detector/aws/eks.rb +174 -0
- data/lib/solarwinds_apm/support/resource_detector/aws/lambda.rb +66 -0
- data/lib/solarwinds_apm/support/resource_detector.rb +192 -0
- data/lib/solarwinds_apm/support/service_key_checker.rb +12 -6
- data/lib/solarwinds_apm/support/transaction_settings.rb +6 -0
- data/lib/solarwinds_apm/support/txn_name_manager.rb +54 -9
- data/lib/solarwinds_apm/support/utils.rb +9 -0
- data/lib/solarwinds_apm/support.rb +3 -4
- data/lib/solarwinds_apm/version.rb +4 -4
- data/lib/solarwinds_apm.rb +27 -73
- metadata +99 -40
- data/ext/oboe_metal/extconf.rb +0 -168
- data/ext/oboe_metal/lib/liboboe-1.0-aarch64.so.sha256 +0 -1
- data/ext/oboe_metal/lib/liboboe-1.0-alpine-aarch64.so.sha256 +0 -1
- data/ext/oboe_metal/lib/liboboe-1.0-alpine-x86_64.so.sha256 +0 -1
- data/ext/oboe_metal/lib/liboboe-1.0-lambda-aarch64.so.sha256 +0 -1
- data/ext/oboe_metal/lib/liboboe-1.0-lambda-x86_64.so.sha256 +0 -1
- data/ext/oboe_metal/lib/liboboe-1.0-x86_64.so.sha256 +0 -1
- data/ext/oboe_metal/src/VERSION +0 -1
- data/ext/oboe_metal/src/bson/bson.h +0 -220
- data/ext/oboe_metal/src/bson/platform_hacks.h +0 -91
- data/ext/oboe_metal/src/init_solarwinds_apm.cc +0 -18
- data/ext/oboe_metal/src/oboe.h +0 -930
- data/ext/oboe_metal/src/oboe_api.cpp +0 -793
- data/ext/oboe_metal/src/oboe_api.h +0 -621
- data/ext/oboe_metal/src/oboe_debug.h +0 -17
- data/ext/oboe_metal/src/oboe_swig_wrap.cc +0 -11045
- data/lib/oboe_metal.rb +0 -187
- data/lib/solarwinds_apm/cert/star.appoptics.com.issuer.crt +0 -24
- data/lib/solarwinds_apm/oboe_init_options.rb +0 -222
- data/lib/solarwinds_apm/opentelemetry/solarwinds_exporter.rb +0 -239
- data/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb +0 -174
- data/lib/solarwinds_apm/opentelemetry/solarwinds_sampler.rb +0 -333
- data/lib/solarwinds_apm/otel_config.rb +0 -174
- data/lib/solarwinds_apm/otel_lambda_config.rb +0 -56
- data/lib/solarwinds_apm/patch/dummy_patch.rb +0 -12
- data/lib/solarwinds_apm/support/oboe_tracing_mode.rb +0 -33
- data/lib/solarwinds_apm/support/support_report.rb +0 -99
- data/lib/solarwinds_apm/support/transaction_cache.rb +0 -57
- data/lib/solarwinds_apm/support/x_trace_options.rb +0 -138
@@ -0,0 +1,348 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# © 2023 SolarWinds Worldwide, LLC. All rights reserved.
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
8
|
+
|
9
|
+
module SolarWindsAPM
|
10
|
+
class OboeSampler
|
11
|
+
SW_KEYS_ATTRIBUTE = 'SWKeys'
|
12
|
+
# SW_TRACESTATE_CAPTURE_KEY = 'sw.w3c.tracestate'
|
13
|
+
PARENT_ID_ATTRIBUTE = 'sw.tracestate_parent_id' # used in parent_base_algo
|
14
|
+
SAMPLE_RATE_ATTRIBUTE = 'SampleRate'
|
15
|
+
SAMPLE_SOURCE_ATTRIBUTE = 'SampleSource'
|
16
|
+
BUCKET_CAPACITY_ATTRIBUTE = 'BucketCapacity'
|
17
|
+
BUCKET_RATE_ATTRIBUTE = 'BucketRate'
|
18
|
+
TRIGGERED_TRACE_ATTRIBUTE = 'TriggeredTrace'
|
19
|
+
|
20
|
+
TRACESTATE_REGEXP = /^[0-9a-f]{16}-[0-9a-f]{2}$/
|
21
|
+
BUCKET_INTERVAL = 1000
|
22
|
+
DICE_SCALE = 1_000_000
|
23
|
+
|
24
|
+
OTEL_SAMPLING_DECISION = ::OpenTelemetry::SDK::Trace::Samplers::Decision
|
25
|
+
OTEL_SAMPLING_RESULT = ::OpenTelemetry::SDK::Trace::Samplers::Result
|
26
|
+
DEFAULT_TRACESTATE = ::OpenTelemetry::Trace::Tracestate::DEFAULT
|
27
|
+
|
28
|
+
def initialize(logger)
|
29
|
+
@logger = logger
|
30
|
+
@counters = SolarWindsAPM::Metrics::Counter.new
|
31
|
+
@buckets = {
|
32
|
+
SolarWindsAPM::BucketType::DEFAULT =>
|
33
|
+
SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL)),
|
34
|
+
SolarWindsAPM::BucketType::TRIGGER_RELAXED =>
|
35
|
+
SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL)),
|
36
|
+
SolarWindsAPM::BucketType::TRIGGER_STRICT =>
|
37
|
+
SolarWindsAPM::TokenBucket.new(SolarWindsAPM::TokenBucketSettings.new(nil, nil, BUCKET_INTERVAL))
|
38
|
+
}
|
39
|
+
@settings = {} # parsed setting from swo backend
|
40
|
+
|
41
|
+
@buckets.each_value(&:start)
|
42
|
+
end
|
43
|
+
|
44
|
+
# return sampling result
|
45
|
+
# params: {:trace_id=>, :parent_context=>, :links=>, :name=>, :kind=>, :attributes=>}
|
46
|
+
# propagator -> processor -> sampler
|
47
|
+
def should_sample?(params)
|
48
|
+
@logger.debug { "should_sample? params: #{params.inspect}" }
|
49
|
+
_, parent_context, _, _, _, attributes = params.values
|
50
|
+
|
51
|
+
parent_span = ::OpenTelemetry::Trace.current_span(parent_context)
|
52
|
+
type = SolarWindsAPM::SpanType.span_type(parent_span)
|
53
|
+
|
54
|
+
@logger.debug { "[#{self.class}/#{__method__}] span type is #{type}" }
|
55
|
+
|
56
|
+
# For local spans, we always trust the parent
|
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)
|
61
|
+
end
|
62
|
+
|
63
|
+
sample_state = SampleState.new(OTEL_SAMPLING_DECISION::DROP,
|
64
|
+
attributes || {},
|
65
|
+
params,
|
66
|
+
get_settings(params),
|
67
|
+
parent_span.context.tracestate['sw'], # get tracestate with sw=xxxx
|
68
|
+
request_headers(params),
|
69
|
+
nil) # this is either TriggerTraceOptions or TraceOptionsResponse
|
70
|
+
|
71
|
+
@logger.debug { "[#{self.class}/#{__method__}] sample_state at start: #{sample_state.inspect}" }
|
72
|
+
|
73
|
+
@counters[:request_count].add(1)
|
74
|
+
|
75
|
+
# adding trigger trace attributes to sample_state attribute as part of decision
|
76
|
+
if sample_state.headers['X-Trace-Options']
|
77
|
+
|
78
|
+
# TraceOptions.parse_trace_options return TriggerTraceOptions
|
79
|
+
sample_state.trace_options = ::SolarWindsAPM::TraceOptions.parse_trace_options(sample_state.headers['X-Trace-Options'], @logger)
|
80
|
+
|
81
|
+
@logger.debug { "X-Trace-Options present: #{sample_state.trace_options}" }
|
82
|
+
|
83
|
+
if sample_state.headers['X-Trace-Options-Signature']
|
84
|
+
@logger.debug { 'X-Trace-Options-Signature present; validating' }
|
85
|
+
|
86
|
+
# this validate_signature is the function from trace_options file
|
87
|
+
sample_state.trace_options.response.auth = TraceOptions.validate_signature(
|
88
|
+
sample_state.headers['X-Trace-Options'],
|
89
|
+
sample_state.headers['X-Trace-Options-Signature'],
|
90
|
+
sample_state.settings[:signature_key],
|
91
|
+
sample_state.trace_options.timestamp
|
92
|
+
)
|
93
|
+
|
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' }
|
97
|
+
|
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)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
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
|
+
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
|
115
|
+
sample_state.trace_options.response.ignored = sample_state.trace_options[:ignored].map { |k, _| k } if sample_state.trace_options[:ignored].any?
|
116
|
+
end
|
117
|
+
|
118
|
+
unless sample_state.settings
|
119
|
+
@logger.debug { 'settings unavailable; sampling disabled' }
|
120
|
+
|
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
|
125
|
+
|
126
|
+
xtracestate = generate_new_tracestate(parent_span, sample_state)
|
127
|
+
|
128
|
+
return OTEL_SAMPLING_RESULT.new(decision: OTEL_SAMPLING_DECISION::DROP,
|
129
|
+
tracestate: xtracestate,
|
130
|
+
attributes: sample_state.attributes)
|
131
|
+
end
|
132
|
+
|
133
|
+
@logger.debug { "[#{self.class}/#{__method__}] sample_state before deciding sampling algo: #{sample_state.inspect}" }
|
134
|
+
# Decide which sampling algo to use and add sampling attribute to decision attributes
|
135
|
+
# https://swicloud.atlassian.net/wiki/spaces/NIT/pages/3815473156/Tracing+Decision+Tree
|
136
|
+
if sample_state.trace_state && TRACESTATE_REGEXP.match?(sample_state.trace_state)
|
137
|
+
@logger.debug { 'context is valid for parent-based sampling' }
|
138
|
+
parent_based_algo(sample_state)
|
139
|
+
|
140
|
+
elsif sample_state.settings[:flags].anybits?(Flags::SAMPLE_START)
|
141
|
+
if sample_state.trace_options&.trigger_trace
|
142
|
+
@logger.debug { 'trigger trace requested' }
|
143
|
+
trigger_trace_algo(sample_state)
|
144
|
+
else
|
145
|
+
@logger.debug { 'defaulting to dice roll' }
|
146
|
+
dice_roll_algo(sample_state)
|
147
|
+
end
|
148
|
+
else
|
149
|
+
@logger.debug { 'SAMPLE_START is unset; sampling disabled' }
|
150
|
+
disabled_algo(sample_state)
|
151
|
+
end
|
152
|
+
|
153
|
+
@logger.debug { "final sampling state: #{sample_state.inspect}" }
|
154
|
+
|
155
|
+
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)
|
159
|
+
|
160
|
+
OTEL_SAMPLING_RESULT.new(decision: sample_state.decision,
|
161
|
+
tracestate: xtracestate,
|
162
|
+
attributes: sample_state.attributes)
|
163
|
+
end
|
164
|
+
|
165
|
+
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)
|
168
|
+
|
169
|
+
# compare the parent_id
|
170
|
+
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
|
173
|
+
@logger.debug { 'trigger trace requested but ignored' }
|
174
|
+
sample_state.trace_options.response.trigger_trace = TriggerTrace::IGNORED # 'ignored'
|
175
|
+
end
|
176
|
+
|
177
|
+
if sample_state.settings[:flags].nobits?(Flags::SAMPLE_THROUGH_ALWAYS)
|
178
|
+
@logger.debug { 'SAMPLE_THROUGH_ALWAYS is unset; sampling disabled' }
|
179
|
+
|
180
|
+
if sample_state.settings[:flags].nobits?(Flags::SAMPLE_START)
|
181
|
+
@logger.debug { 'SAMPLE_START is unset; don\'t record' }
|
182
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::DROP
|
183
|
+
else
|
184
|
+
@logger.debug { 'SAMPLE_START is set; record' }
|
185
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
|
186
|
+
end
|
187
|
+
else
|
188
|
+
@logger.debug { 'SAMPLE_THROUGH_ALWAYS is set; parent-based sampling' }
|
189
|
+
|
190
|
+
flags = sample_state.trace_state[-2, 2].to_i(16)
|
191
|
+
sampled = flags & (::OpenTelemetry::Trace::TraceFlags::SAMPLED.sampled? ? 1 : 0)
|
192
|
+
|
193
|
+
if sampled.zero?
|
194
|
+
@logger.debug { 'parent is not sampled; record only' }
|
195
|
+
|
196
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
|
197
|
+
else
|
198
|
+
@logger.debug { 'parent is sampled; record and sample' }
|
199
|
+
|
200
|
+
@counters[:trace_count].add(1)
|
201
|
+
@counters[:through_trace_count].add(1) # ruby metrics only add incremented value and attributes
|
202
|
+
|
203
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def trigger_trace_algo(sample_state)
|
209
|
+
if sample_state.settings[:flags].nobits?(Flags::TRIGGERED_TRACE)
|
210
|
+
@logger.debug { 'TRIGGERED_TRACE unset; record only' }
|
211
|
+
|
212
|
+
sample_state.trace_options.response.trigger_trace = TriggerTrace::TRIGGER_TRACING_DISABLED # 'trigger-tracing-disabled'
|
213
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
|
214
|
+
else
|
215
|
+
@logger.debug { 'TRIGGERED_TRACE set; trigger tracing' }
|
216
|
+
# If there's an auth response present, it's a valid signed request
|
217
|
+
# Otherwise, this code wouldn't be reached
|
218
|
+
if sample_state.trace_options.response.auth
|
219
|
+
@logger.debug { 'signed request; using relaxed rate' }
|
220
|
+
|
221
|
+
bucket = @buckets[BucketType::TRIGGER_RELAXED]
|
222
|
+
else
|
223
|
+
@logger.debug { 'unsigned request; using strict rate' }
|
224
|
+
|
225
|
+
bucket = @buckets[BucketType::TRIGGER_STRICT]
|
226
|
+
end
|
227
|
+
|
228
|
+
@logger.debug { "trigger_trace_algo bucket: #{bucket.inspect}" }
|
229
|
+
sample_state.attributes[TRIGGERED_TRACE_ATTRIBUTE] = true
|
230
|
+
sample_state.attributes[BUCKET_CAPACITY_ATTRIBUTE] = bucket.capacity
|
231
|
+
sample_state.attributes[BUCKET_RATE_ATTRIBUTE] = bucket.rate
|
232
|
+
|
233
|
+
if bucket.consume
|
234
|
+
@logger.debug { 'sufficient capacity; record and sample' }
|
235
|
+
@counters[:triggered_trace_count].add(1)
|
236
|
+
@counters[:trace_count].add(1)
|
237
|
+
|
238
|
+
sample_state.trace_options.response.trigger_trace = TriggerTrace::OK
|
239
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE
|
240
|
+
else
|
241
|
+
@logger.debug { 'insufficient capacity; record only' }
|
242
|
+
|
243
|
+
sample_state.trace_options.response.trigger_trace = TriggerTrace::RATE_EXCEEDED
|
244
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def dice_roll_algo(sample_state)
|
250
|
+
dice = SolarWindsAPM::Dice.new(rate: sample_state.settings[:sample_rate], scale: DICE_SCALE)
|
251
|
+
sample_state.attributes[SAMPLE_RATE_ATTRIBUTE] = dice.rate
|
252
|
+
sample_state.attributes[SAMPLE_SOURCE_ATTRIBUTE] = sample_state.settings[:sample_source]
|
253
|
+
|
254
|
+
@counters[:sample_count].add(1)
|
255
|
+
|
256
|
+
if dice.roll
|
257
|
+
@logger.debug { 'dice roll success; checking capacity' }
|
258
|
+
|
259
|
+
bucket = @buckets[BucketType::DEFAULT]
|
260
|
+
sample_state.attributes[BUCKET_CAPACITY_ATTRIBUTE] = bucket.capacity
|
261
|
+
sample_state.attributes[BUCKET_RATE_ATTRIBUTE] = bucket.rate
|
262
|
+
|
263
|
+
@logger.debug { "dice_roll_algo bucket: #{bucket.inspect}" }
|
264
|
+
if bucket.consume
|
265
|
+
@logger.debug { 'sufficient capacity; record and sample' }
|
266
|
+
|
267
|
+
@counters[:trace_count].add(1)
|
268
|
+
|
269
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE
|
270
|
+
else
|
271
|
+
@logger.debug { 'insufficient capacity; record only' }
|
272
|
+
|
273
|
+
@counters[:token_bucket_exhaustion_count].add(1)
|
274
|
+
|
275
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
|
276
|
+
end
|
277
|
+
else
|
278
|
+
@logger.debug { 'dice roll failure; record only' }
|
279
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def disabled_algo(sample_state)
|
284
|
+
if sample_state.trace_options&.trigger_trace
|
285
|
+
@logger.debug { 'trigger trace requested but tracing disabled' }
|
286
|
+
sample_state.trace_options.response.trigger_trace = TriggerTrace::TRACING_DISABLED
|
287
|
+
end
|
288
|
+
|
289
|
+
if sample_state.settings[:flags].nobits?(Flags::SAMPLE_THROUGH_ALWAYS)
|
290
|
+
@logger.debug { "SAMPLE_THROUGH_ALWAYS is unset; don't record" }
|
291
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::DROP
|
292
|
+
else
|
293
|
+
@logger.debug { 'SAMPLE_THROUGH_ALWAYS is set; record' }
|
294
|
+
sample_state.decision = OTEL_SAMPLING_DECISION::RECORD_ONLY
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def update_settings(settings)
|
299
|
+
return unless settings[:timestamp] > (@settings[:timestamp] || 0)
|
300
|
+
|
301
|
+
@settings = settings
|
302
|
+
@buckets.each do |type, bucket|
|
303
|
+
bucket.update(@settings[:buckets][type]) if @settings[:buckets][type]
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# old sampler seems set the response headers through tracestate
|
308
|
+
# handle_response_headers functionality is replace by generate_new_tracestate
|
309
|
+
def generate_new_tracestate(parent_span, sample_state)
|
310
|
+
if !parent_span.context.valid? || parent_span.context.tracestate.nil?
|
311
|
+
@logger.debug { 'create new tracestate' }
|
312
|
+
decision = sw_from_span_and_decision(parent_span, sample_state.decision)
|
313
|
+
trace_state = ::OpenTelemetry::Trace::Tracestate.from_hash({ 'sw' => decision })
|
314
|
+
else
|
315
|
+
@logger.debug { 'update tracestate' }
|
316
|
+
decision = sw_from_span_and_decision(parent_span, sample_state.decision)
|
317
|
+
trace_state = parent_span.context.tracestate.set_value('sw', decision)
|
318
|
+
end
|
319
|
+
|
320
|
+
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
|
+
trace_state = trace_state.set_value('xtrace_options_response', stringified_trace_options)
|
324
|
+
@logger.debug { "[#{self.class}/#{__method__}] new trace_state: #{trace_state.inspect}" }
|
325
|
+
trace_state
|
326
|
+
end
|
327
|
+
|
328
|
+
def sw_from_span_and_decision(parent_span, otel_decision)
|
329
|
+
trace_flag = otel_decision == OTEL_SAMPLING_DECISION::RECORD_AND_SAMPLE ? '01' : '00'
|
330
|
+
[parent_span.context.hex_span_id, trace_flag].join('-')
|
331
|
+
end
|
332
|
+
|
333
|
+
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 = nil
|
341
|
+
return
|
342
|
+
end
|
343
|
+
sampling_setting = SolarWindsAPM::SamplingSettings.merge(@settings, local_settings(params))
|
344
|
+
@logger.debug { "sampling_setting: #{sampling_setting.inspect}" }
|
345
|
+
sampling_setting
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# © 2023 SolarWinds Worldwide, LLC. All rights reserved.
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
8
|
+
|
9
|
+
module SolarWindsAPM
|
10
|
+
class Sampler < OboeSampler
|
11
|
+
RUBY_SEM_CON = ::OpenTelemetry::SemanticConventions::Trace
|
12
|
+
|
13
|
+
ATTR_HTTP_REQUEST_METHOD = 'http.request.method'
|
14
|
+
ATTR_HTTP_METHOD = RUBY_SEM_CON::HTTP_METHOD
|
15
|
+
ATTR_HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code'
|
16
|
+
ATTR_HTTP_STATUS_CODE = RUBY_SEM_CON::HTTP_STATUS_CODE
|
17
|
+
ATTR_URL_SCHEME = 'url.scheme'
|
18
|
+
ATTR_HTTP_SCHEME = RUBY_SEM_CON::HTTP_SCHEME
|
19
|
+
ATTR_SERVER_ADDRESS = 'server.address'
|
20
|
+
ATTR_NET_HOST_NAME = RUBY_SEM_CON::NET_HOST_NAME
|
21
|
+
ATTR_URL_PATH = 'url.path'
|
22
|
+
ATTR_HTTP_TARGET = RUBY_SEM_CON::HTTP_TARGET
|
23
|
+
|
24
|
+
# tracing_mode is getting from SolarWindsAPM::Config
|
25
|
+
def initialize(config, logger)
|
26
|
+
super(logger)
|
27
|
+
@tracing_mode = resolve_tracing_mode(config)
|
28
|
+
@trigger_mode = config[:trigger_trace_enabled]
|
29
|
+
@transaction_settings = config[:transaction_settings]
|
30
|
+
@ready = false
|
31
|
+
end
|
32
|
+
|
33
|
+
# wait for getting the first settings
|
34
|
+
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
|
+
|
46
|
+
sleep 0.1
|
47
|
+
break if (Time.now - deadline).round(0) >= timeout
|
48
|
+
end
|
49
|
+
@ready = true unless @settings[:signature_key].nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
def resolve_tracing_mode(config)
|
53
|
+
return unless config.key?(:tracing_mode) && !config[:tracing_mode].nil?
|
54
|
+
|
55
|
+
config[:tracing_mode] ? TracingMode::ALWAYS : TracingMode::NEVER
|
56
|
+
end
|
57
|
+
|
58
|
+
def local_settings(params)
|
59
|
+
_trace_id, _parent_context, _links, span_name, span_kind, attributes = params.values
|
60
|
+
settings = { tracing_mode: @tracing_mode, trigger_mode: @trigger_mode }
|
61
|
+
return settings if @transaction_settings.nil? || @transaction_settings.empty?
|
62
|
+
|
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}" }
|
66
|
+
|
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
|
+
|
71
|
+
settings[:tracing_mode] = tracing_mode
|
72
|
+
settings
|
73
|
+
end
|
74
|
+
|
75
|
+
# 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
|
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
|
+
}
|
89
|
+
end
|
90
|
+
|
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
|
100
|
+
end
|
101
|
+
|
102
|
+
def update_settings(settings)
|
103
|
+
parsed = parse_settings(settings)
|
104
|
+
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
|
+
|
109
|
+
@logger.warn { "Warning from parsed settings: #{parsed[:warning]}" } if parsed[:warning]
|
110
|
+
|
111
|
+
parsed
|
112
|
+
else
|
113
|
+
@logger.debug { "invalid settings: #{settings.inspect}" }
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def http_span_metadata(kind, attributes)
|
119
|
+
return { http: false } unless kind == ::OpenTelemetry::Trace::SpanKind::SERVER &&
|
120
|
+
(attributes.key?(ATTR_HTTP_REQUEST_METHOD) || attributes.key?(ATTR_HTTP_METHOD))
|
121
|
+
|
122
|
+
method_ = (attributes[ATTR_HTTP_REQUEST_METHOD] || attributes[ATTR_HTTP_METHOD]).to_s
|
123
|
+
status = (attributes[ATTR_HTTP_RESPONSE_STATUS_CODE] || attributes[ATTR_HTTP_STATUS_CODE] || 0).to_i
|
124
|
+
scheme = (attributes[ATTR_URL_SCHEME] || attributes[ATTR_HTTP_SCHEME] || 'http').to_s
|
125
|
+
hostname = (attributes[ATTR_SERVER_ADDRESS] || attributes[ATTR_NET_HOST_NAME] || 'localhost').to_s
|
126
|
+
path = (attributes[ATTR_URL_PATH] || attributes[ATTR_HTTP_TARGET]).to_s
|
127
|
+
url = "#{scheme}://#{hostname}#{path}"
|
128
|
+
|
129
|
+
http_metadata = {
|
130
|
+
http: true,
|
131
|
+
method: method_,
|
132
|
+
status: status,
|
133
|
+
scheme: scheme,
|
134
|
+
hostname: hostname,
|
135
|
+
path: path,
|
136
|
+
url: url
|
137
|
+
}
|
138
|
+
|
139
|
+
@logger.debug { "Retrieved http metadata: #{http_metadata.inspect}" }
|
140
|
+
http_metadata
|
141
|
+
end
|
142
|
+
|
143
|
+
def parse_settings(unparsed)
|
144
|
+
return unless unparsed.is_a?(Hash)
|
145
|
+
|
146
|
+
return unless unparsed['value'].is_a?(Numeric) &&
|
147
|
+
unparsed['timestamp'].is_a?(Numeric) &&
|
148
|
+
unparsed['ttl'].is_a?(Numeric)
|
149
|
+
|
150
|
+
sample_rate = unparsed['value']
|
151
|
+
timestamp = unparsed['timestamp']
|
152
|
+
ttl = unparsed['ttl']
|
153
|
+
|
154
|
+
return unless unparsed['flags'].is_a?(String)
|
155
|
+
|
156
|
+
flags = unparsed['flags'].split(',').reduce(Flags::OK) do |final_flag, f|
|
157
|
+
flag = {
|
158
|
+
'OVERRIDE' => Flags::OVERRIDE,
|
159
|
+
'SAMPLE_START' => Flags::SAMPLE_START,
|
160
|
+
'SAMPLE_THROUGH_ALWAYS' => Flags::SAMPLE_THROUGH_ALWAYS,
|
161
|
+
'TRIGGER_TRACE' => Flags::TRIGGERED_TRACE
|
162
|
+
}[f]
|
163
|
+
|
164
|
+
final_flag |= flag if flag
|
165
|
+
final_flag
|
166
|
+
end
|
167
|
+
|
168
|
+
buckets = {}
|
169
|
+
signature_key = nil
|
170
|
+
|
171
|
+
if unparsed['arguments'].is_a?(Hash)
|
172
|
+
args = unparsed['arguments']
|
173
|
+
|
174
|
+
buckets[BucketType::DEFAULT] = { capacity: args['BucketCapacity'], rate: args['BucketRate'] } if args['BucketCapacity'].is_a?(Numeric) && args['BucketRate'].is_a?(Numeric)
|
175
|
+
|
176
|
+
buckets['trigger_relaxed'] = { capacity: args['TriggerRelaxedBucketCapacity'], rate: args['TriggerRelaxedBucketRate'] } if args['TriggerRelaxedBucketCapacity'].is_a?(Numeric) && args['TriggerRelaxedBucketRate'].is_a?(Numeric)
|
177
|
+
|
178
|
+
buckets['trigger_strict'] = { capacity: args['TriggerStrictBucketCapacity'], rate: args['TriggerStrictBucketRate'] } if args['TriggerStrictBucketCapacity'].is_a?(Numeric) && args['TriggerStrictBucketRate'].is_a?(Numeric)
|
179
|
+
|
180
|
+
signature_key = args['SignatureKey'] if args['SignatureKey'].is_a?(String)
|
181
|
+
end
|
182
|
+
|
183
|
+
warning = unparsed['warning'] if unparsed['warning'].is_a?(String)
|
184
|
+
|
185
|
+
{
|
186
|
+
sample_source: SampleSource::REMOTE,
|
187
|
+
sample_rate: sample_rate,
|
188
|
+
flags: flags,
|
189
|
+
timestamp: timestamp,
|
190
|
+
ttl: ttl,
|
191
|
+
buckets: buckets,
|
192
|
+
signature_key: signature_key,
|
193
|
+
warning: warning
|
194
|
+
}
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# © 2023 SolarWinds Worldwide, LLC. All rights reserved.
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
8
|
+
|
9
|
+
module SolarWindsAPM
|
10
|
+
TriggerTraceOptions = Struct.new(
|
11
|
+
:trigger_trace,
|
12
|
+
:timestamp,
|
13
|
+
:sw_keys,
|
14
|
+
:custom, # Hash
|
15
|
+
:ignored, # Array
|
16
|
+
:response # TraceOptionsResponse
|
17
|
+
)
|
18
|
+
|
19
|
+
TraceOptionsResponse = Struct.new(
|
20
|
+
:auth, # Auth
|
21
|
+
:trigger_trace, # TriggerTrace
|
22
|
+
:ignored # Array
|
23
|
+
)
|
24
|
+
|
25
|
+
module Auth
|
26
|
+
OK = 'ok'
|
27
|
+
BAD_TIMESTAMP = 'bad-timestamp'
|
28
|
+
BAD_SIGNATURE = 'bad-signature'
|
29
|
+
NO_SIGNATURE_KEY = 'no-signature-key'
|
30
|
+
end
|
31
|
+
|
32
|
+
module TriggerTrace
|
33
|
+
OK = 'ok'
|
34
|
+
NOT_REQUESTED = 'not-requested'
|
35
|
+
IGNORED = 'ignored'
|
36
|
+
TRACING_DISABLED = 'tracing-disabled'
|
37
|
+
TRIGGER_TRACING_DISABLED = 'trigger-tracing-disabled'
|
38
|
+
RATE_EXCEEDED = 'rate-exceeded'
|
39
|
+
SETTINGS_NOT_AVAILABLE = 'settings-not-available'
|
40
|
+
end
|
41
|
+
|
42
|
+
Settings = Struct.new(:sample_rate,
|
43
|
+
:sample_source,
|
44
|
+
:flags,
|
45
|
+
:buckets, # BucketSettings
|
46
|
+
:signature_key,
|
47
|
+
:timestamp,
|
48
|
+
:ttl)
|
49
|
+
|
50
|
+
LocalSettings = Struct.new(:tracing_mode, # TracingMode
|
51
|
+
:trigger_mode) # {:enabled, :disabled}
|
52
|
+
|
53
|
+
BucketSettings = Struct.new(:capacity, # Number
|
54
|
+
:rate) # Number
|
55
|
+
|
56
|
+
TokenBucketSettings = Struct.new(:capacity, # Number
|
57
|
+
:rate, # Number
|
58
|
+
:interval) # Number
|
59
|
+
|
60
|
+
module SampleSource
|
61
|
+
LOCAL_DEFAULT = 2
|
62
|
+
REMOTE = 6
|
63
|
+
end
|
64
|
+
|
65
|
+
module Flags
|
66
|
+
OK = 0x0
|
67
|
+
INVALID = 0x1
|
68
|
+
OVERRIDE = 0x2
|
69
|
+
SAMPLE_START = 0x4
|
70
|
+
SAMPLE_THROUGH_ALWAYS = 0x10
|
71
|
+
TRIGGERED_TRACE = 0x20
|
72
|
+
end
|
73
|
+
|
74
|
+
module TracingMode
|
75
|
+
ALWAYS = Flags::SAMPLE_START | Flags::SAMPLE_THROUGH_ALWAYS
|
76
|
+
NEVER = 0x0
|
77
|
+
end
|
78
|
+
|
79
|
+
module BucketType
|
80
|
+
DEFAULT = ''
|
81
|
+
TRIGGER_RELAXED = 'trigger_relaxed'
|
82
|
+
TRIGGER_STRICT = 'trigger_strict'
|
83
|
+
end
|
84
|
+
|
85
|
+
module SpanType
|
86
|
+
ROOT = 'root'
|
87
|
+
ENTRY = 'entry'
|
88
|
+
LOCAL = 'local'
|
89
|
+
|
90
|
+
VALID_TRACEID_REGEX = /^[0-9a-f]{32}$/i
|
91
|
+
VALID_SPANID_REGEX = /^[0-9a-f]{16}$/i
|
92
|
+
|
93
|
+
INVALID_SPANID = '0000000000000000'
|
94
|
+
INVALID_TRACEID = '00000000000000000000000000000000'
|
95
|
+
|
96
|
+
def self.span_type(parent_span)
|
97
|
+
parent_span_context = parent_span&.context
|
98
|
+
if parent_span_context.nil? || !span_context_valid?(parent_span_context)
|
99
|
+
ROOT
|
100
|
+
elsif parent_span_context.remote?
|
101
|
+
ENTRY
|
102
|
+
else
|
103
|
+
LOCAL
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.valid_trace_id?(trace_id)
|
108
|
+
!!(trace_id =~ VALID_TRACEID_REGEX) && trace_id != INVALID_TRACEID
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.valid_span_id?(span_id)
|
112
|
+
!!(span_id =~ VALID_SPANID_REGEX) && span_id != INVALID_SPANID
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.span_context_valid?(span_context)
|
116
|
+
valid_trace_id?(span_context.hex_trace_id) && valid_span_id?(span_context.hex_span_id)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
SampleState = Struct.new(:decision, # SamplingDecision
|
121
|
+
:attributes, # Attributes
|
122
|
+
:params, # SampleParams
|
123
|
+
:settings, # Settings
|
124
|
+
:trace_state, # String
|
125
|
+
:headers, # RequestHeaders
|
126
|
+
:trace_options) # TraceOptions & { response: TraceOptionsResponse })
|
127
|
+
end
|