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,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
- # noop version of SolarWindsAPM::Context
11
- #
12
- # module SolarWindsAPM
13
- # end
9
+ module SolarWindsAPM
10
+ module SamplingSettings
11
+ def self.merge(remote, local)
12
+ flags = local[:tracing_mode] || remote[:flags]
14
13
 
15
- module Oboe_metal # rubocop:disable Naming/ClassAndModuleCamelCase
16
- # Context for noop
17
- class Context
18
- ##
19
- # noop version of :toString
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
- def self.isReady(*)
27
- false
28
- end
20
+ if remote[:flags].anybits?(SolarWindsAPM::Flags::OVERRIDE)
21
+ flags &= remote[:flags]
22
+ flags |= SolarWindsAPM::Flags::OVERRIDE
23
+ end
29
24
 
30
- def self.getDecisions(*)
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
- # This file is for loading any customized patch for upstream
9
+ require 'json'
10
+ require 'fileutils'
11
+ require 'tempfile'
12
+ require 'uri'
13
+ require 'opentelemetry-sdk'
10
14
 
11
- # e.g.
12
- # require_relative './patch/dummy_patch'
13
- # OpenTelemetry::Instrumentation::Registry.prepend(SolarWindsAPM::Patch::DummyPatch) if defined? OpenTelemetry::Instrumentation::Registry && OpenTelemetry::Instrumentation::Registry::VERSION <= '0.3.0'
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'