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.
- checksums.yaml +4 -4
- data/README.md +53 -4
- data/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb +0 -4
- data/lib/solarwinds_apm/api/current_trace_info.rb +10 -6
- data/lib/solarwinds_apm/api/custom_instrumentation.rb +80 -0
- 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/api.rb +2 -0
- 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/patch/tag_sql/sw_dbo_utils.rb +35 -0
- data/lib/solarwinds_apm/patch/tag_sql/sw_mysql2_patch.rb +1 -12
- data/lib/solarwinds_apm/patch/tag_sql/sw_pg_patch.rb +39 -0
- data/lib/solarwinds_apm/patch/tag_sql_patch.rb +2 -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 +105 -50
- 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 -10954
- 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/swomarginalia/LICENSE +0 -20
- data/lib/solarwinds_apm/support/swomarginalia/README.md +0 -46
- data/lib/solarwinds_apm/support/swomarginalia/comment.rb +0 -206
- data/lib/solarwinds_apm/support/swomarginalia/formatter.rb +0 -20
- data/lib/solarwinds_apm/support/swomarginalia/load_swomarginalia.rb +0 -55
- data/lib/solarwinds_apm/support/swomarginalia/railtie.rb +0 -24
- data/lib/solarwinds_apm/support/swomarginalia/swomarginalia.rb +0 -89
- data/lib/solarwinds_apm/support/transaction_cache.rb +0 -57
- 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
|