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,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
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolarWindsAPM
|
4
|
+
module MetricsExporter
|
5
|
+
module Patch
|
6
|
+
# do not send metrics if no data_points present
|
7
|
+
def export(metrics, timeout: nil)
|
8
|
+
metrics.reject! { |m| m.data_points.empty? }
|
9
|
+
return ::OpenTelemetry::SDK::Metrics::Export::SUCCESS unless metrics.any? { |m| m.data_points.any? }
|
10
|
+
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module SolarWindsAPM
|
18
|
+
module Span
|
19
|
+
module Patch
|
20
|
+
def finish(end_timestamp: nil)
|
21
|
+
@mutex.synchronize do
|
22
|
+
if @ended
|
23
|
+
::OpenTelemetry.logger.warn('Calling finish on an ended Span.')
|
24
|
+
return self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@span_processors.each do |processor|
|
29
|
+
processor.on_finishing(self) if processor.respond_to?(:on_finishing)
|
30
|
+
end
|
31
|
+
|
32
|
+
@mutex.synchronize do
|
33
|
+
@end_timestamp = relative_timestamp(end_timestamp)
|
34
|
+
@attributes = validated_attributes(@attributes).freeze
|
35
|
+
@events.freeze
|
36
|
+
@links.freeze
|
37
|
+
@ended = true
|
38
|
+
end
|
39
|
+
@span_processors.each { |processor| processor.on_finish(self) }
|
40
|
+
self
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.prepend(SolarWindsAPM::MetricsExporter::Patch)
|
47
|
+
|
48
|
+
# issue: https://github.com/open-telemetry/opentelemetry-ruby/issues/1824
|
49
|
+
OpenTelemetry::SDK::Trace::Span.prepend(SolarWindsAPM::Span::Patch)
|
@@ -0,0 +1 @@
|
|
1
|
+
{"value"=>1000000, "flags"=>"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", "timestamp"=>1738523568, "ttl"=>120, "arguments"=>{"BucketCapacity"=>2, "BucketRate"=>1, "TriggerRelaxedBucketCapacity"=>20, "TriggerRelaxedBucketRate"=>1, "TriggerStrictBucketCapacity"=>6, "TriggerStrictBucketRate"=>0.1, "SignatureKey"=>"0sftj1EYX7JJp01DblJwkccYCIZ91fbU"}}
|
@@ -6,34 +6,23 @@
|
|
6
6
|
#
|
7
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
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# end
|
9
|
+
module SolarWindsAPM
|
10
|
+
module SamplingSettings
|
11
|
+
def self.merge(remote, local)
|
12
|
+
flags = local[:tracing_mode] || remote[:flags]
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
# toString would return the current trace context as string
|
21
|
-
#
|
22
|
-
def self.toString
|
23
|
-
'99-00000000000000000000000000000000-0000000000000000-00'
|
24
|
-
end
|
14
|
+
if local[:trigger_mode] == :enabled
|
15
|
+
flags |= SolarWindsAPM::Flags::TRIGGERED_TRACE
|
16
|
+
elsif local[:trigger_mode] == :disabled
|
17
|
+
flags &= ~ SolarWindsAPM::Flags::TRIGGERED_TRACE
|
18
|
+
end
|
25
19
|
|
26
|
-
|
27
|
-
|
28
|
-
|
20
|
+
if remote[:flags].anybits?(SolarWindsAPM::Flags::OVERRIDE)
|
21
|
+
flags &= remote[:flags]
|
22
|
+
flags |= SolarWindsAPM::Flags::OVERRIDE
|
23
|
+
end
|
29
24
|
|
30
|
-
|
31
|
-
[-1, -1, -1, 0, 0.0, 0.0, -1, -1, '', '', 4]
|
25
|
+
remote.dup.tap { |merged| merged[:flags] = flags }
|
32
26
|
end
|
33
|
-
|
34
|
-
##
|
35
|
-
# noop version of :clear
|
36
|
-
#
|
37
|
-
def self.clear; end
|
38
27
|
end
|
39
28
|
end
|
@@ -0,0 +1,126 @@
|
|
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
|
+
# Bucket is used to consume that determine if capacity is enough
|
10
|
+
# capacity is updated through update_settings
|
11
|
+
module SolarWindsAPM
|
12
|
+
class TokenBucket
|
13
|
+
# Maximum value of a signed 32-bit integer
|
14
|
+
MAX_INTERVAL = (2**31) - 1
|
15
|
+
|
16
|
+
attr_reader :capacity, :rate, :interval, :tokens
|
17
|
+
|
18
|
+
def initialize(token_bucket_settings)
|
19
|
+
self.capacity = token_bucket_settings.capacity || 0
|
20
|
+
self.rate = token_bucket_settings.rate || 0
|
21
|
+
self.interval = token_bucket_settings.interval || MAX_INTERVAL
|
22
|
+
self.tokens = @capacity
|
23
|
+
@timer = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# used call from update_settings e.g. bucket.update(bucket_settings)
|
27
|
+
def update(settings)
|
28
|
+
settings.instance_of?(Hash) ? update_from_hash(settings) : update_from_token_bucket_settings(settings)
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_from_hash(settings)
|
32
|
+
if settings[:capacity]
|
33
|
+
difference = settings[:capacity] - @capacity
|
34
|
+
self.capacity = settings[:capacity]
|
35
|
+
self.tokens = @tokens + difference
|
36
|
+
end
|
37
|
+
|
38
|
+
self.rate = settings[:rate] if settings[:rate]
|
39
|
+
|
40
|
+
return unless settings[:interval]
|
41
|
+
|
42
|
+
self.interval = settings[:interval]
|
43
|
+
return unless running
|
44
|
+
|
45
|
+
stop
|
46
|
+
start
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_from_token_bucket_settings(settings)
|
50
|
+
if settings.capacity
|
51
|
+
difference = settings.capacity - @capacity
|
52
|
+
self.capacity = settings.capacity
|
53
|
+
self.tokens = @tokens + difference
|
54
|
+
end
|
55
|
+
|
56
|
+
self.rate = settings.rate if settings.rate
|
57
|
+
|
58
|
+
return unless settings.interval
|
59
|
+
|
60
|
+
self.interval = settings.interval
|
61
|
+
return unless running
|
62
|
+
|
63
|
+
stop
|
64
|
+
start
|
65
|
+
end
|
66
|
+
|
67
|
+
def capacity=(capacity)
|
68
|
+
@capacity = [0, capacity].max
|
69
|
+
end
|
70
|
+
|
71
|
+
def rate=(rate)
|
72
|
+
@rate = [0, rate].max
|
73
|
+
end
|
74
|
+
|
75
|
+
def interval=(interval)
|
76
|
+
@interval = interval.clamp(0, MAX_INTERVAL)
|
77
|
+
end
|
78
|
+
|
79
|
+
def tokens=(tokens)
|
80
|
+
@tokens = tokens.clamp(0, @capacity)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Attempts to consume tokens from the bucket
|
84
|
+
# @param n [Integer] Number of tokens to consume
|
85
|
+
# @return [Boolean] Whether there were enough tokens
|
86
|
+
def consume(token = 1)
|
87
|
+
if @tokens >= token
|
88
|
+
self.tokens = @tokens - token
|
89
|
+
true
|
90
|
+
else
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Starts replenishing the bucket
|
96
|
+
def start
|
97
|
+
return if running
|
98
|
+
|
99
|
+
@timer = Thread.new do
|
100
|
+
loop do
|
101
|
+
task
|
102
|
+
sleep(@interval / 1000.0)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Stops replenishing the bucket
|
108
|
+
def stop
|
109
|
+
return unless running
|
110
|
+
|
111
|
+
@timer.kill
|
112
|
+
@timer = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
# Whether the bucket is actively being replenished
|
116
|
+
def running
|
117
|
+
!@timer.nil?
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def task
|
123
|
+
self.tokens = tokens + @rate
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,100 @@
|
|
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 TraceOptions
|
11
|
+
TRIGGER_TRACE_KEY = 'trigger-trace'
|
12
|
+
TIMESTAMP_KEY = 'ts'
|
13
|
+
SW_KEYS_KEY = 'sw-keys'
|
14
|
+
|
15
|
+
CUSTOM_KEY_REGEX = /^custom-[^\s]*$/
|
16
|
+
|
17
|
+
def self.parse_trace_options(header, logger)
|
18
|
+
trace_options = TriggerTraceOptions.new(nil, nil, nil, {}, [], TraceOptionsResponse.new(nil, nil, []))
|
19
|
+
|
20
|
+
kvs = header.split(';').map do |kv|
|
21
|
+
key, *values = kv.split('=').map(&:strip)
|
22
|
+
value = values.any? ? values.join('=') : nil
|
23
|
+
[key, value]
|
24
|
+
end
|
25
|
+
|
26
|
+
kvs.reject! { |key, _| key.nil? || key.empty? }
|
27
|
+
|
28
|
+
kvs.each do |k, v|
|
29
|
+
case k
|
30
|
+
when TRIGGER_TRACE_KEY
|
31
|
+
if v || trace_options.trigger_trace
|
32
|
+
logger.debug { 'invalid trace option for trigger trace' }
|
33
|
+
trace_options.ignored << [k, v]
|
34
|
+
next
|
35
|
+
end
|
36
|
+
trace_options.trigger_trace = true
|
37
|
+
when TIMESTAMP_KEY
|
38
|
+
if v.nil? || trace_options.timestamp
|
39
|
+
logger.debug { 'invalid trace option for timestamp' }
|
40
|
+
trace_options.ignored << [k, v]
|
41
|
+
next
|
42
|
+
end
|
43
|
+
|
44
|
+
unless numeric_integer?(v)
|
45
|
+
logger.debug { 'invalid trace option for timestamp, should be an integer' }
|
46
|
+
trace_options.ignored << [k, v]
|
47
|
+
next
|
48
|
+
end
|
49
|
+
trace_options.timestamp = v.to_i
|
50
|
+
when SW_KEYS_KEY
|
51
|
+
if v.nil? || trace_options.sw_keys
|
52
|
+
logger.debug { 'invalid trace option for sw keys' }
|
53
|
+
trace_options.ignored << [k, v]
|
54
|
+
next
|
55
|
+
end
|
56
|
+
trace_options.sw_keys = v
|
57
|
+
when CUSTOM_KEY_REGEX
|
58
|
+
if v.nil? || trace_options.custom[k]
|
59
|
+
logger.debug { "invalid trace option for custom key #{k}" }
|
60
|
+
trace_options.ignored << [k, v]
|
61
|
+
next
|
62
|
+
end
|
63
|
+
trace_options.custom[k] = v
|
64
|
+
else
|
65
|
+
trace_options.ignored << [k, v]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
trace_options
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.numeric_integer?(str)
|
73
|
+
true if Integer(str)
|
74
|
+
rescue StandardError
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
# combine the array to string separate by ;
|
79
|
+
# tracestate doesn't accept value with k=v, here we use k:v
|
80
|
+
# but it will be replaced with = when inject in respond header
|
81
|
+
def self.stringify_trace_options_response(trace_options_response)
|
82
|
+
return if trace_options_response.nil?
|
83
|
+
|
84
|
+
kvs = {
|
85
|
+
auth: trace_options_response.auth,
|
86
|
+
'trigger-trace': trace_options_response.trigger_trace,
|
87
|
+
ignored: trace_options_response.ignored.empty? ? nil : trace_options_response.ignored.join(',')
|
88
|
+
}
|
89
|
+
kvs.compact.map { |k, v| "#{k}:#{v}" }.join(';')
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.validate_signature(header, signature, key, timestamp)
|
93
|
+
return Auth::NO_SIGNATURE_KEY unless key
|
94
|
+
return Auth::BAD_TIMESTAMP unless timestamp && (Time.now.to_i - timestamp).abs <= 5 * 60
|
95
|
+
|
96
|
+
digest = OpenSSL::HMAC.hexdigest('SHA1', key, header)
|
97
|
+
signature == digest ? Auth::OK : Auth::BAD_SIGNATURE
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -6,8 +6,24 @@
|
|
6
6
|
#
|
7
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
8
|
|
9
|
-
|
9
|
+
require 'json'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'tempfile'
|
12
|
+
require 'uri'
|
13
|
+
require 'opentelemetry-sdk'
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
15
|
+
require_relative 'sampling/sampling_constants'
|
16
|
+
require_relative 'sampling/dice'
|
17
|
+
require_relative 'sampling/settings'
|
18
|
+
require_relative 'sampling/token_bucket'
|
19
|
+
require_relative 'sampling/trace_options'
|
20
|
+
require_relative 'sampling/metrics'
|
21
|
+
|
22
|
+
# HttpSampler/JsonSampler < Sampler < OboeSampler
|
23
|
+
require_relative 'sampling/oboe_sampler'
|
24
|
+
require_relative 'sampling/sampler'
|
25
|
+
require_relative 'sampling/http_sampler'
|
26
|
+
require_relative 'sampling/json_sampler'
|
27
|
+
|
28
|
+
# Patching
|
29
|
+
require_relative 'sampling/sampling_patch'
|