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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/solarwinds_apm/api/current_trace_info.rb +10 -6
  4. data/lib/solarwinds_apm/api/custom_metrics.rb +8 -25
  5. data/lib/solarwinds_apm/api/tracing.rb +12 -27
  6. data/lib/solarwinds_apm/api/transaction_name.rb +6 -10
  7. data/lib/solarwinds_apm/config.rb +1 -1
  8. data/lib/solarwinds_apm/constants.rb +1 -0
  9. data/lib/solarwinds_apm/noop/api.rb +5 -2
  10. data/lib/solarwinds_apm/noop.rb +0 -24
  11. data/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +90 -69
  12. data/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb +0 -2
  13. data/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb +5 -4
  14. data/lib/solarwinds_apm/opentelemetry.rb +5 -7
  15. data/lib/solarwinds_apm/otel_native_config.rb +177 -0
  16. data/lib/solarwinds_apm/patch/README.md +15 -0
  17. data/lib/solarwinds_apm/{noop/metadata.rb → sampling/dice.rb} +19 -17
  18. data/lib/solarwinds_apm/sampling/http_sampler.rb +87 -0
  19. data/lib/solarwinds_apm/sampling/json_sampler.rb +52 -0
  20. data/lib/solarwinds_apm/sampling/metrics.rb +38 -0
  21. data/lib/solarwinds_apm/sampling/oboe_sampler.rb +348 -0
  22. data/lib/solarwinds_apm/sampling/sampler.rb +197 -0
  23. data/lib/solarwinds_apm/sampling/sampling_constants.rb +127 -0
  24. data/lib/solarwinds_apm/sampling/sampling_patch.rb +49 -0
  25. data/lib/solarwinds_apm/sampling/setting_example.txt +1 -0
  26. data/lib/solarwinds_apm/{noop/context.rb → sampling/settings.rb} +14 -25
  27. data/lib/solarwinds_apm/sampling/token_bucket.rb +126 -0
  28. data/lib/solarwinds_apm/sampling/trace_options.rb +100 -0
  29. data/lib/solarwinds_apm/{patch.rb → sampling.rb} +20 -4
  30. data/lib/solarwinds_apm/{noop/span.rb → support/aws_resource_detector.rb} +5 -18
  31. data/lib/solarwinds_apm/support/logger_formatter.rb +1 -1
  32. data/lib/solarwinds_apm/support/logging_log_event.rb +1 -1
  33. data/lib/solarwinds_apm/support/lumberjack_formatter.rb +1 -1
  34. data/lib/solarwinds_apm/support/otlp_endpoint.rb +99 -0
  35. data/lib/solarwinds_apm/support/resource_detector/aws/beanstalk.rb +51 -0
  36. data/lib/solarwinds_apm/support/resource_detector/aws/ec2.rb +145 -0
  37. data/lib/solarwinds_apm/support/resource_detector/aws/ecs.rb +173 -0
  38. data/lib/solarwinds_apm/support/resource_detector/aws/eks.rb +174 -0
  39. data/lib/solarwinds_apm/support/resource_detector/aws/lambda.rb +66 -0
  40. data/lib/solarwinds_apm/support/resource_detector.rb +192 -0
  41. data/lib/solarwinds_apm/support/service_key_checker.rb +12 -6
  42. data/lib/solarwinds_apm/support/transaction_settings.rb +6 -0
  43. data/lib/solarwinds_apm/support/txn_name_manager.rb +54 -9
  44. data/lib/solarwinds_apm/support/utils.rb +9 -0
  45. data/lib/solarwinds_apm/support.rb +3 -4
  46. data/lib/solarwinds_apm/version.rb +4 -4
  47. data/lib/solarwinds_apm.rb +27 -73
  48. metadata +99 -40
  49. data/ext/oboe_metal/extconf.rb +0 -168
  50. data/ext/oboe_metal/lib/liboboe-1.0-aarch64.so.sha256 +0 -1
  51. data/ext/oboe_metal/lib/liboboe-1.0-alpine-aarch64.so.sha256 +0 -1
  52. data/ext/oboe_metal/lib/liboboe-1.0-alpine-x86_64.so.sha256 +0 -1
  53. data/ext/oboe_metal/lib/liboboe-1.0-lambda-aarch64.so.sha256 +0 -1
  54. data/ext/oboe_metal/lib/liboboe-1.0-lambda-x86_64.so.sha256 +0 -1
  55. data/ext/oboe_metal/lib/liboboe-1.0-x86_64.so.sha256 +0 -1
  56. data/ext/oboe_metal/src/VERSION +0 -1
  57. data/ext/oboe_metal/src/bson/bson.h +0 -220
  58. data/ext/oboe_metal/src/bson/platform_hacks.h +0 -91
  59. data/ext/oboe_metal/src/init_solarwinds_apm.cc +0 -18
  60. data/ext/oboe_metal/src/oboe.h +0 -930
  61. data/ext/oboe_metal/src/oboe_api.cpp +0 -793
  62. data/ext/oboe_metal/src/oboe_api.h +0 -621
  63. data/ext/oboe_metal/src/oboe_debug.h +0 -17
  64. data/ext/oboe_metal/src/oboe_swig_wrap.cc +0 -11045
  65. data/lib/oboe_metal.rb +0 -187
  66. data/lib/solarwinds_apm/cert/star.appoptics.com.issuer.crt +0 -24
  67. data/lib/solarwinds_apm/oboe_init_options.rb +0 -222
  68. data/lib/solarwinds_apm/opentelemetry/solarwinds_exporter.rb +0 -239
  69. data/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb +0 -174
  70. data/lib/solarwinds_apm/opentelemetry/solarwinds_sampler.rb +0 -333
  71. data/lib/solarwinds_apm/otel_config.rb +0 -174
  72. data/lib/solarwinds_apm/otel_lambda_config.rb +0 -56
  73. data/lib/solarwinds_apm/patch/dummy_patch.rb +0 -12
  74. data/lib/solarwinds_apm/support/oboe_tracing_mode.rb +0 -33
  75. data/lib/solarwinds_apm/support/support_report.rb +0 -99
  76. data/lib/solarwinds_apm/support/transaction_cache.rb +0 -57
  77. 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