solarwinds_apm 6.1.1 → 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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +53 -4
  3. data/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb +0 -4
  4. data/lib/solarwinds_apm/api/current_trace_info.rb +10 -6
  5. data/lib/solarwinds_apm/api/custom_instrumentation.rb +80 -0
  6. data/lib/solarwinds_apm/api/custom_metrics.rb +8 -25
  7. data/lib/solarwinds_apm/api/tracing.rb +12 -27
  8. data/lib/solarwinds_apm/api/transaction_name.rb +6 -10
  9. data/lib/solarwinds_apm/api.rb +2 -0
  10. data/lib/solarwinds_apm/config.rb +1 -1
  11. data/lib/solarwinds_apm/constants.rb +1 -0
  12. data/lib/solarwinds_apm/noop/api.rb +5 -2
  13. data/lib/solarwinds_apm/noop.rb +0 -24
  14. data/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +90 -69
  15. data/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb +0 -2
  16. data/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb +5 -4
  17. data/lib/solarwinds_apm/opentelemetry.rb +5 -7
  18. data/lib/solarwinds_apm/otel_native_config.rb +177 -0
  19. data/lib/solarwinds_apm/patch/README.md +15 -0
  20. data/lib/solarwinds_apm/patch/tag_sql/sw_dbo_utils.rb +35 -0
  21. data/lib/solarwinds_apm/patch/tag_sql/sw_mysql2_patch.rb +1 -12
  22. data/lib/solarwinds_apm/patch/tag_sql/sw_pg_patch.rb +39 -0
  23. data/lib/solarwinds_apm/patch/tag_sql_patch.rb +2 -0
  24. data/lib/solarwinds_apm/{noop/metadata.rb → sampling/dice.rb} +19 -17
  25. data/lib/solarwinds_apm/sampling/http_sampler.rb +87 -0
  26. data/lib/solarwinds_apm/sampling/json_sampler.rb +52 -0
  27. data/lib/solarwinds_apm/sampling/metrics.rb +38 -0
  28. data/lib/solarwinds_apm/sampling/oboe_sampler.rb +348 -0
  29. data/lib/solarwinds_apm/sampling/sampler.rb +197 -0
  30. data/lib/solarwinds_apm/sampling/sampling_constants.rb +127 -0
  31. data/lib/solarwinds_apm/sampling/sampling_patch.rb +49 -0
  32. data/lib/solarwinds_apm/sampling/setting_example.txt +1 -0
  33. data/lib/solarwinds_apm/{noop/context.rb → sampling/settings.rb} +14 -25
  34. data/lib/solarwinds_apm/sampling/token_bucket.rb +126 -0
  35. data/lib/solarwinds_apm/sampling/trace_options.rb +100 -0
  36. data/lib/solarwinds_apm/{patch.rb → sampling.rb} +20 -4
  37. data/lib/solarwinds_apm/{noop/span.rb → support/aws_resource_detector.rb} +5 -18
  38. data/lib/solarwinds_apm/support/logger_formatter.rb +1 -1
  39. data/lib/solarwinds_apm/support/logging_log_event.rb +1 -1
  40. data/lib/solarwinds_apm/support/lumberjack_formatter.rb +1 -1
  41. data/lib/solarwinds_apm/support/otlp_endpoint.rb +99 -0
  42. data/lib/solarwinds_apm/support/resource_detector/aws/beanstalk.rb +51 -0
  43. data/lib/solarwinds_apm/support/resource_detector/aws/ec2.rb +145 -0
  44. data/lib/solarwinds_apm/support/resource_detector/aws/ecs.rb +173 -0
  45. data/lib/solarwinds_apm/support/resource_detector/aws/eks.rb +174 -0
  46. data/lib/solarwinds_apm/support/resource_detector/aws/lambda.rb +66 -0
  47. data/lib/solarwinds_apm/support/resource_detector.rb +192 -0
  48. data/lib/solarwinds_apm/support/service_key_checker.rb +12 -6
  49. data/lib/solarwinds_apm/support/transaction_settings.rb +6 -0
  50. data/lib/solarwinds_apm/support/txn_name_manager.rb +54 -9
  51. data/lib/solarwinds_apm/support/utils.rb +9 -0
  52. data/lib/solarwinds_apm/support.rb +3 -4
  53. data/lib/solarwinds_apm/version.rb +4 -4
  54. data/lib/solarwinds_apm.rb +27 -73
  55. metadata +105 -50
  56. data/ext/oboe_metal/extconf.rb +0 -168
  57. data/ext/oboe_metal/lib/liboboe-1.0-aarch64.so.sha256 +0 -1
  58. data/ext/oboe_metal/lib/liboboe-1.0-alpine-aarch64.so.sha256 +0 -1
  59. data/ext/oboe_metal/lib/liboboe-1.0-alpine-x86_64.so.sha256 +0 -1
  60. data/ext/oboe_metal/lib/liboboe-1.0-lambda-aarch64.so.sha256 +0 -1
  61. data/ext/oboe_metal/lib/liboboe-1.0-lambda-x86_64.so.sha256 +0 -1
  62. data/ext/oboe_metal/lib/liboboe-1.0-x86_64.so.sha256 +0 -1
  63. data/ext/oboe_metal/src/VERSION +0 -1
  64. data/ext/oboe_metal/src/bson/bson.h +0 -220
  65. data/ext/oboe_metal/src/bson/platform_hacks.h +0 -91
  66. data/ext/oboe_metal/src/init_solarwinds_apm.cc +0 -18
  67. data/ext/oboe_metal/src/oboe.h +0 -930
  68. data/ext/oboe_metal/src/oboe_api.cpp +0 -793
  69. data/ext/oboe_metal/src/oboe_api.h +0 -621
  70. data/ext/oboe_metal/src/oboe_debug.h +0 -17
  71. data/ext/oboe_metal/src/oboe_swig_wrap.cc +0 -10954
  72. data/lib/oboe_metal.rb +0 -187
  73. data/lib/solarwinds_apm/cert/star.appoptics.com.issuer.crt +0 -24
  74. data/lib/solarwinds_apm/oboe_init_options.rb +0 -222
  75. data/lib/solarwinds_apm/opentelemetry/solarwinds_exporter.rb +0 -239
  76. data/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb +0 -174
  77. data/lib/solarwinds_apm/opentelemetry/solarwinds_sampler.rb +0 -333
  78. data/lib/solarwinds_apm/otel_config.rb +0 -174
  79. data/lib/solarwinds_apm/otel_lambda_config.rb +0 -56
  80. data/lib/solarwinds_apm/patch/dummy_patch.rb +0 -12
  81. data/lib/solarwinds_apm/support/oboe_tracing_mode.rb +0 -33
  82. data/lib/solarwinds_apm/support/support_report.rb +0 -99
  83. data/lib/solarwinds_apm/support/swomarginalia/LICENSE +0 -20
  84. data/lib/solarwinds_apm/support/swomarginalia/README.md +0 -46
  85. data/lib/solarwinds_apm/support/swomarginalia/comment.rb +0 -206
  86. data/lib/solarwinds_apm/support/swomarginalia/formatter.rb +0 -20
  87. data/lib/solarwinds_apm/support/swomarginalia/load_swomarginalia.rb +0 -55
  88. data/lib/solarwinds_apm/support/swomarginalia/railtie.rb +0 -24
  89. data/lib/solarwinds_apm/support/swomarginalia/swomarginalia.rb +0 -89
  90. data/lib/solarwinds_apm/support/transaction_cache.rb +0 -57
  91. data/lib/solarwinds_apm/support/x_trace_options.rb +0 -138
@@ -0,0 +1,87 @@
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 HttpSampler < Sampler
11
+ REQUEST_TIMEOUT = 10 # 10s
12
+ GET_SETTING_DURAION = 60 # 60s
13
+
14
+ # we don't need hostname as it's for separating browser and local env
15
+ def initialize(config)
16
+ super(config, SolarWindsAPM.logger)
17
+
18
+ @url = config[:collector]
19
+ @service = URI.encode_www_form_component(config[:service]) # service name "Hello world" -> "Hello%20world"
20
+ @headers = config[:headers]
21
+
22
+ @hostname = hostname
23
+ @setting_url = URI.join(@url, "./v1/settings/#{@service}/#{@hostname}")
24
+
25
+ Thread.new { settings_request }
26
+ end
27
+
28
+ private
29
+
30
+ # Node.js equivalent: Retrieve system hostname
31
+ # e.g. docker -> docker.swo.ubuntu.development; macos -> NHSDFWSSD
32
+ def hostname
33
+ host = Socket.gethostname
34
+ URI.encode_www_form_component(host)
35
+ end
36
+
37
+ def fetch_with_timeout(url, timeout_seconds = nil)
38
+ uri = url
39
+ response = nil
40
+
41
+ thread = Thread.new do
42
+ ::OpenTelemetry::Common::Utilities.untraced do
43
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
44
+ request = Net::HTTP::Get.new(uri)
45
+ request['Authorization'] = @headers
46
+
47
+ response = http.request(request)
48
+ end
49
+ end
50
+ rescue StandardError => e
51
+ @logger.debug { "Error during request: #{e.message}" }
52
+ end
53
+
54
+ thread_join = thread.join(timeout_seconds || REQUEST_TIMEOUT)
55
+ if thread_join.nil?
56
+ @logger.debug { "Request timed out after #{timeout_seconds} seconds" }
57
+ thread.kill
58
+ end
59
+
60
+ response
61
+ end
62
+
63
+ # a endless loop within a thread (non-blocking)
64
+ def settings_request
65
+ loop do
66
+ @logger.debug { "Retrieving sampling settings from #{@setting_url}" }
67
+
68
+ response = fetch_with_timeout(@setting_url)
69
+ parsed = response.nil? ? nil : JSON.parse(response.body)
70
+
71
+ @logger.debug { "parsed settings in json: #{parsed.inspect}" }
72
+
73
+ if update_settings(parsed)
74
+ # update the settings before the previous ones expire with some time to spare
75
+ expiry = (parsed['timestamp'].to_i + parsed['ttl'].to_i)
76
+ expiry_timeout = expiry - REQUEST_TIMEOUT - Time.now.to_i
77
+ sleep([0, expiry_timeout].max)
78
+ else
79
+ @logger.warn { 'Retrieved sampling settings are invalid. Ensure proper configuration.' }
80
+ sleep(GET_SETTING_DURAION)
81
+ end
82
+ rescue StandardError => e
83
+ @logger.warn { "Failed to retrieve sampling settings (#{e.message}), tracing will be disabled until valid ones are available." }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,52 @@
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 JsonSampler < Sampler
11
+ DEFAULT_PATH = File.join(Dir.tmpdir, 'solarwinds-apm-settings.json')
12
+
13
+ def initialize(config, path = nil)
14
+ super(config, SolarWindsAPM.logger)
15
+
16
+ @path = path || DEFAULT_PATH
17
+ @expiry = Time.now.to_i
18
+ loop_check
19
+ end
20
+
21
+ # only json sampler will need to check if the settings.json file
22
+ # updated or not from collector extention
23
+ def should_sample?(params)
24
+ loop_check
25
+ super
26
+ end
27
+
28
+ private
29
+
30
+ def loop_check
31
+ # Update if we're within 10s of expiry
32
+ return if Time.now.to_i + 10 < @expiry
33
+
34
+ begin
35
+ contents = File.read(@path)
36
+ unparsed = JSON.parse(contents)
37
+ rescue StandardError => e
38
+ @logger.debug { "missing or invalid settings file; Error: #{e.message}" }
39
+ return
40
+ end
41
+
42
+ unless unparsed.is_a?(Array) && unparsed.length == 1
43
+ @logger.debug { "invalid settings file : #{unparsed}" }
44
+ return
45
+ end
46
+
47
+ parsed = update_settings(unparsed.first)
48
+ @logger.debug { "update_settings: #{parsed}" }
49
+ @expiry = parsed[:timestamp].to_i + parsed[:ttl].to_i if parsed
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,38 @@
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
+ module Metrics
11
+ class Counter
12
+ # counter = Counter.new
13
+ # counter[:request_count].update(1)
14
+ def initialize
15
+ @meter = ::OpenTelemetry.meter_provider.meter('sw.apm.sampling.metrics')
16
+
17
+ @counter = {
18
+ request_count:
19
+ @meter.create_counter('trace.service.request_count', unit: '{request}', description: 'Count of all requests.'),
20
+ sample_count:
21
+ @meter.create_counter('trace.service.samplecount', unit: '{request}', description: 'Count of requests that went through sampling, which excludes those with a valid upstream decision or trigger traced.'),
22
+ trace_count:
23
+ @meter.create_counter('trace.service.tracecount', unit: '{trace}', description: 'Count of all traces.'),
24
+ through_trace_count:
25
+ @meter.create_counter('trace.service.through_trace_count', unit: '{request}', description: 'Count of requests with a valid upstream decision, thus passed through sampling.'),
26
+ triggered_trace_count:
27
+ @meter.create_counter('trace.service.triggered_trace_count', unit: '{trace}', description: 'Count of triggered traces.'),
28
+ token_bucket_exhaustion_count:
29
+ @meter.create_counter('trace.service.tokenbucket_exhaustion_count', unit: '{request}', description: 'Count of requests that were not traced due to token bucket rate limiting.')
30
+ }
31
+ end
32
+
33
+ def [](key)
34
+ @counter[key]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -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