solarwinds_apm 7.0.0 → 7.1.0
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 +1 -0
- data/lib/solarwinds_apm/api/transaction_name.rb +7 -6
- data/lib/solarwinds_apm/config.rb +33 -24
- data/lib/solarwinds_apm/noop/README.md +24 -5
- data/lib/solarwinds_apm/noop/api.rb +12 -5
- data/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +32 -19
- data/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb +8 -2
- data/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb +5 -15
- data/lib/solarwinds_apm/opentelemetry.rb +3 -0
- data/lib/solarwinds_apm/otel_config.rb +3 -2
- data/lib/solarwinds_apm/sampling/dice.rb +5 -3
- data/lib/solarwinds_apm/sampling/http_sampler.rb +58 -19
- data/lib/solarwinds_apm/sampling/json_sampler.rb +27 -12
- data/lib/solarwinds_apm/sampling/oboe_sampler.rb +50 -57
- data/lib/solarwinds_apm/sampling/sampler.rb +46 -58
- data/lib/solarwinds_apm/sampling/sampling_constants.rb +4 -3
- data/lib/solarwinds_apm/sampling/settings.rb +3 -1
- data/lib/solarwinds_apm/sampling/token_bucket.rb +12 -3
- data/lib/solarwinds_apm/sampling/trace_options.rb +33 -16
- data/lib/solarwinds_apm/sampling.rb +0 -1
- data/lib/solarwinds_apm/support/resource_detector.rb +20 -18
- data/lib/solarwinds_apm/support/transaction_settings.rb +12 -5
- data/lib/solarwinds_apm/support/txn_name_manager.rb +0 -2
- data/lib/solarwinds_apm/version.rb +1 -1
- metadata +44 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d565e746144994b5c3a81ee19665ee9b2d4e81fbad2e21da567465455c37e6c0
|
|
4
|
+
data.tar.gz: 949351c94748745d2ba19268a531ba5b4f805568132772dcfb9749b0af3698a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 209a50b78aa94dc85805ef8c6c08db7584966ca62293c53aa9744852170c3c84ac4ef24b2e43e508bad5d1e505f4cd82d72d984a00823232b9a9e13bddb061c6
|
|
7
|
+
data.tar.gz: '0962ab7325e66d3f330361b9f0187f75c07eabcd2d0d400db0d55e75c2124aefa2fe630cbe2eef506f48f58f6e6d3cb6ad1d36f1a816845618b6bd2649e8b951'
|
data/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
The `solarwinds_apm` gem starting from version 6.0.0 is an [OpenTelemetry Ruby](https://opentelemetry.io/docs/instrumentation/ruby/) distribution. It provides automatic instrumentation and custom SolarWinds Observability features for Ruby applications.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
|
+
>
|
|
6
7
|
> [!NOTE]
|
|
7
8
|
> Versions before 7.0.0 only support Linux and will go into no-op mode on other platforms.
|
|
8
9
|
|
|
@@ -21,15 +21,16 @@ module SolarWindsAPM
|
|
|
21
21
|
#
|
|
22
22
|
# === Example:
|
|
23
23
|
#
|
|
24
|
-
# class
|
|
24
|
+
# class OrdersController < ApplicationController
|
|
25
25
|
#
|
|
26
26
|
# def create
|
|
27
|
-
# @
|
|
28
|
-
# @
|
|
27
|
+
# @order = Order.new(params.permit(:item, :quantity))
|
|
28
|
+
# if @order.save
|
|
29
|
+
# custom_name = "orderscontroller.create_for_#{params[:item]}"
|
|
30
|
+
# SolarWindsAPM::API.set_transaction_name(custom_name)
|
|
31
|
+
# end
|
|
29
32
|
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# redirect_to @dogfood
|
|
33
|
+
# redirect_to @order
|
|
33
34
|
# end
|
|
34
35
|
#
|
|
35
36
|
# end
|
|
@@ -6,6 +6,8 @@
|
|
|
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
|
+
require 'set'
|
|
10
|
+
|
|
9
11
|
module SolarWindsAPM
|
|
10
12
|
##
|
|
11
13
|
# This module exposes a nested configuration hash that can be used to
|
|
@@ -24,14 +26,6 @@ module SolarWindsAPM
|
|
|
24
26
|
6 => { stdlib: ::Logger::DEBUG, otel: 'debug' } }.freeze
|
|
25
27
|
|
|
26
28
|
@@config = {}
|
|
27
|
-
@@instrumentation = %i[action_controller action_controller_api action_view
|
|
28
|
-
active_record bunnyclient bunnyconsumer curb
|
|
29
|
-
dalli delayed_jobclient delayed_jobworker
|
|
30
|
-
excon faraday graphql grpc_client grpc_server grape
|
|
31
|
-
httpclient nethttp memcached mongo moped padrino rack redis
|
|
32
|
-
resqueclient resqueworker rest_client
|
|
33
|
-
sequel sidekiqclient sidekiqworker sinatra typhoeus
|
|
34
|
-
curb excon faraday httpclient nethttp rest_client typhoeus]
|
|
35
29
|
|
|
36
30
|
##
|
|
37
31
|
# load_config_file
|
|
@@ -93,21 +87,32 @@ module SolarWindsAPM
|
|
|
93
87
|
SolarWindsAPM.logger.level = SW_LOG_LEVEL_MAPPING.dig(log_level, :stdlib) || ::Logger::INFO # default log level info
|
|
94
88
|
end
|
|
95
89
|
|
|
90
|
+
# e.g. enable_disable_config('STRING', :key, value, false, bool: true)
|
|
96
91
|
def self.enable_disable_config(env_var, key, value, default, bool: false)
|
|
97
|
-
|
|
92
|
+
raw_env_value = ENV.fetch(env_var, '')
|
|
93
|
+
env_value = raw_env_value.downcase
|
|
98
94
|
valid_env_values = bool ? %w[true false] : %w[enabled disabled]
|
|
99
95
|
|
|
100
|
-
if env_var && valid_env_values.include?(env_value)
|
|
96
|
+
if !env_var.empty? && valid_env_values.include?(env_value)
|
|
101
97
|
value = bool ? true?(env_value) : env_value.to_sym
|
|
102
|
-
elsif env_var && !
|
|
103
|
-
SolarWindsAPM.logger.warn
|
|
104
|
-
|
|
98
|
+
elsif !env_var.empty? && !raw_env_value.empty?
|
|
99
|
+
SolarWindsAPM.logger.warn do
|
|
100
|
+
"[#{name}/#{__method__}] #{env_var} must be #{valid_env_values.join('/')} (current setting is #{raw_env_value}). Using default value: #{default}."
|
|
101
|
+
end
|
|
102
|
+
return @@config[key] = default
|
|
105
103
|
end
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
# Validate final value efficiently
|
|
106
|
+
valid = bool ? boolean?(value) : symbol?(value)
|
|
107
|
+
|
|
108
|
+
unless valid
|
|
109
|
+
SolarWindsAPM.logger.warn do
|
|
110
|
+
"[#{name}/#{__method__}] :#{key} must be #{valid_env_values.join('/')}. Using default value: #{default}."
|
|
111
|
+
end
|
|
112
|
+
return @@config[key] = default
|
|
113
|
+
end
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
@@config[key.to_sym] = default
|
|
115
|
+
@@config[key] = value
|
|
111
116
|
end
|
|
112
117
|
|
|
113
118
|
def self.true?(obj)
|
|
@@ -131,8 +136,6 @@ module SolarWindsAPM
|
|
|
131
136
|
def self.print_config
|
|
132
137
|
SolarWindsAPM.logger.debug { "[#{name}/#{__method__}] General configurations list blow:" }
|
|
133
138
|
@@config.each do |k, v|
|
|
134
|
-
next if @@instrumentation.include?(k)
|
|
135
|
-
|
|
136
139
|
SolarWindsAPM.logger.debug do
|
|
137
140
|
"[#{name}/#{__method__}] Config Key/Value: #{k}, #{v.inspect}"
|
|
138
141
|
end
|
|
@@ -148,8 +151,6 @@ module SolarWindsAPM
|
|
|
148
151
|
# This will be called when require 'solarwinds_apm/config' happen
|
|
149
152
|
#
|
|
150
153
|
def self.initialize
|
|
151
|
-
# for config file backward compatibility
|
|
152
|
-
@@instrumentation.each { |inst| @@config[inst] = {} }
|
|
153
154
|
@@config[:transaction_name] = {}
|
|
154
155
|
|
|
155
156
|
# Always load the template, it has all the keys and defaults defined,
|
|
@@ -221,7 +222,7 @@ module SolarWindsAPM
|
|
|
221
222
|
enable_disable_config('SW_APM_TRIGGER_TRACING_MODE', key, value, :enabled)
|
|
222
223
|
|
|
223
224
|
when :tracing_mode
|
|
224
|
-
enable_disable_config(
|
|
225
|
+
enable_disable_config('', key, value, :enabled)
|
|
225
226
|
|
|
226
227
|
when :tag_sql
|
|
227
228
|
enable_disable_config('SW_APM_TAG_SQL', key, value, false, bool: true)
|
|
@@ -252,9 +253,17 @@ module SolarWindsAPM
|
|
|
252
253
|
return
|
|
253
254
|
end
|
|
254
255
|
|
|
255
|
-
# `tracing: disabled` is the default
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
# `tracing: disabled` is the default; below only separate the enabled and disabled settings
|
|
257
|
+
result = settings.each_with_object({ enabled: [], disabled: [] }) do |setting, acc|
|
|
258
|
+
if setting[:tracing] == :enabled
|
|
259
|
+
acc[:enabled] << setting
|
|
260
|
+
elsif !setting.key?(:tracing) || setting[:tracing] == :disabled
|
|
261
|
+
acc[:disabled] << setting
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
enabled = result[:enabled]
|
|
266
|
+
disabled = result[:disabled]
|
|
258
267
|
|
|
259
268
|
SolarWindsAPM::Config[:enabled_regexps] = compile_regexp(enabled)
|
|
260
269
|
SolarWindsAPM::Config[:disabled_regexps] = compile_regexp(disabled)
|
|
@@ -1,12 +1,31 @@
|
|
|
1
|
-
|
|
2
1
|
# Noop Mode
|
|
3
2
|
|
|
4
3
|
Here we can define modules and classes for noop mode.
|
|
5
4
|
|
|
6
|
-
Instead of polluting code with SolarWindsAPM.loaded conditionals
|
|
5
|
+
Instead of polluting code with SolarWindsAPM.loaded conditionals, we load these classes when in noop mode and they expose noop behavior.
|
|
6
|
+
|
|
7
|
+
The following methods require noop implementations based on the API modules:
|
|
8
|
+
|
|
9
|
+
## Currently Implemented Noop Modules in `api.rb`:
|
|
10
|
+
|
|
11
|
+
- **Tracing**:
|
|
12
|
+
- `solarwinds_ready?(wait_milliseconds=3000, integer_response: false)` - Always returns false
|
|
13
|
+
|
|
14
|
+
- **CurrentTraceInfo**:
|
|
15
|
+
- `current_trace_info` - Returns a TraceInfo instance with default/empty values
|
|
16
|
+
- TraceInfo class with noop methods: `for_log`, `hash_for_log`, and attributes: `tracestring`, `trace_id`, `span_id`, `trace_flags`, `do_log`
|
|
17
|
+
|
|
18
|
+
- **CustomMetrics**:
|
|
19
|
+
- `increment_metric(name, count=1, with_hostname=false, tags_kvs={})` - Returns false with deprecation warning
|
|
20
|
+
- `summary_metric(name, value, count=1, with_hostname=false, tags_kvs={})` - Returns false with deprecation warning
|
|
21
|
+
|
|
22
|
+
- **OpenTelemetry**:
|
|
23
|
+
- `in_span(name, attributes: nil, links: nil, start_timestamp: nil, kind: nil, &block)` - Simply yields to the block if given
|
|
7
24
|
|
|
8
|
-
|
|
25
|
+
- **TransactionName**:
|
|
26
|
+
- `set_transaction_name(custom_name=nil)` - Always returns true
|
|
9
27
|
|
|
10
|
-
|
|
28
|
+
- **CustomInstrumentation/Tracer**:
|
|
29
|
+
- `add_tracer(method_name, span_name=nil, options={})` - Always returns nil.
|
|
11
30
|
|
|
12
|
-
|
|
31
|
+
These modules are extended into `SolarWindsAPM::API` to provide consistent behavior when SolarWindsAPM is disabled or in noop mode.
|
|
@@ -79,10 +79,17 @@ module NoopAPI
|
|
|
79
79
|
true
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
module Tracer
|
|
84
|
+
def add_tracer(*); end
|
|
85
|
+
end
|
|
82
86
|
end
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
[
|
|
89
|
+
NoopAPI::Tracing,
|
|
90
|
+
NoopAPI::CurrentTraceInfo,
|
|
91
|
+
NoopAPI::CustomMetrics,
|
|
92
|
+
NoopAPI::OpenTelemetry,
|
|
93
|
+
NoopAPI::TransactionName,
|
|
94
|
+
NoopAPI::Tracer
|
|
95
|
+
].each { |mod| SolarWindsAPM::API.extend(mod) }
|
|
@@ -21,12 +21,15 @@ module SolarWindsAPM
|
|
|
21
21
|
HTTP_STATUS_CODE = 'http.status_code'
|
|
22
22
|
HTTP_URL = 'http.url'
|
|
23
23
|
|
|
24
|
+
HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code'
|
|
25
|
+
HTTP_REQUEST_METHOD = 'http.request.method'
|
|
26
|
+
|
|
24
27
|
INVALID_HTTP_STATUS_CODE = 0
|
|
25
28
|
|
|
26
29
|
def initialize(txn_manager)
|
|
27
30
|
@txn_manager = txn_manager
|
|
28
|
-
@meters = { 'sw.apm.request.metrics' => ::OpenTelemetry.meter_provider.meter('sw.apm.request.metrics') }
|
|
29
31
|
@metrics = init_response_time_metrics
|
|
32
|
+
@is_lambda = SolarWindsAPM::Utils.determine_lambda
|
|
30
33
|
@transaction_name = nil
|
|
31
34
|
end
|
|
32
35
|
|
|
@@ -40,21 +43,24 @@ module SolarWindsAPM
|
|
|
40
43
|
trace_flags = span.context.trace_flags.sampled? ? '01' : '00'
|
|
41
44
|
@txn_manager&.set_root_context_h(span.context.hex_trace_id, "#{span.context.hex_span_id}-#{trace_flags}")
|
|
42
45
|
span.add_attributes({ SW_IS_ENTRY_SPAN => true })
|
|
46
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_start end" }
|
|
43
47
|
rescue StandardError => e
|
|
44
48
|
SolarWindsAPM.logger.info { "[#{self.class}/#{__method__}] processor on_start error: #{e.message}" }
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
def on_finishing(span)
|
|
52
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finishing span attributes: #{span.attributes}" }
|
|
48
53
|
return if non_entry_span(span: span)
|
|
49
54
|
|
|
50
55
|
@transaction_name = calculate_transaction_names(span)
|
|
51
56
|
span.set_attribute(SW_TRANSACTION_NAME, @transaction_name)
|
|
52
57
|
@txn_manager.delete_root_context_h(span.context.hex_trace_id)
|
|
58
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finishing end" }
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
# @param [Span] span the (immutable) {Span} that just ended.
|
|
56
62
|
def on_finish(span)
|
|
57
|
-
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish span: #{span.
|
|
63
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish span attributes: #{span.attributes}" }
|
|
58
64
|
return if non_entry_span(span: span)
|
|
59
65
|
|
|
60
66
|
record_request_metrics(span)
|
|
@@ -63,10 +69,9 @@ module SolarWindsAPM
|
|
|
63
69
|
::OpenTelemetry.meter_provider.metric_readers.each do |reader|
|
|
64
70
|
reader.pull if reader.respond_to? :pull
|
|
65
71
|
end
|
|
66
|
-
|
|
67
|
-
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish succeed" }
|
|
72
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] processor on_finish end" }
|
|
68
73
|
rescue StandardError => e
|
|
69
|
-
SolarWindsAPM.logger.info { "[#{self.class}/#{__method__}]
|
|
74
|
+
SolarWindsAPM.logger.info { "[#{self.class}/#{__method__}] processor on_finish error: #{e.message}" }
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
# @param [optional Numeric] timeout An optional timeout in seconds.
|
|
@@ -94,7 +99,9 @@ module SolarWindsAPM
|
|
|
94
99
|
unit: 'ms')
|
|
95
100
|
end
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
meter = ::OpenTelemetry.meter_provider.meter('sw.apm.request.metrics')
|
|
103
|
+
instrument = meter.create_histogram('trace.service.response_time', unit: 'ms', description: 'Duration of each entry span for the service, typically meaning the time taken to process an inbound request.')
|
|
104
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Adding ExponentialBucketHistogram for response time metrics: #{instrument.inspect}" }
|
|
98
105
|
{ response_time: instrument }
|
|
99
106
|
end
|
|
100
107
|
|
|
@@ -104,37 +111,43 @@ module SolarWindsAPM
|
|
|
104
111
|
SW_TRANSACTION_NAME => @transaction_name
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
|
|
114
|
+
is_http_span = span_http?(span)
|
|
115
|
+
|
|
116
|
+
if is_http_span
|
|
108
117
|
http_status_code = get_http_status_code(span)
|
|
109
|
-
meter_attrs[
|
|
110
|
-
meter_attrs[
|
|
118
|
+
meter_attrs[HTTP_STATUS_CODE] = http_status_code if http_status_code != 0
|
|
119
|
+
meter_attrs[HTTP_METHOD] = span.attributes[HTTP_METHOD] if span.attributes[HTTP_METHOD]
|
|
120
|
+
meter_attrs[HTTP_METHOD] = span.attributes[HTTP_REQUEST_METHOD] if span.attributes[HTTP_REQUEST_METHOD]
|
|
111
121
|
end
|
|
112
|
-
|
|
122
|
+
|
|
123
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] is_http_span: #{is_http_span}; meter_attrs: #{meter_attrs.inspect}" }
|
|
113
124
|
meter_attrs.compact!
|
|
114
125
|
meter_attrs
|
|
115
126
|
end
|
|
116
127
|
|
|
117
128
|
def calculate_lambda_transaction_name(span_name)
|
|
118
|
-
(ENV['SW_APM_TRANSACTION_NAME'] || ENV['AWS_LAMBDA_FUNCTION_NAME'] || span_name || 'unknown').slice(0, SolarWindsAPM::Constants::MAX_TXN_NAME_LENGTH)
|
|
129
|
+
txn_name = (ENV['SW_APM_TRANSACTION_NAME'] || ENV['AWS_LAMBDA_FUNCTION_NAME'] || span_name || 'unknown').slice(0, SolarWindsAPM::Constants::MAX_TXN_NAME_LENGTH)
|
|
130
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Lambda transaction name: #{txn_name} (from env_txn=#{ENV.fetch('SW_APM_TRANSACTION_NAME', nil)}, lambda_func=#{ENV.fetch('AWS_LAMBDA_FUNCTION_NAME', nil)}, span_name=#{span_name})" }
|
|
131
|
+
txn_name
|
|
119
132
|
end
|
|
120
133
|
|
|
121
134
|
# Get trans_name and url_tran of this span instance.
|
|
122
135
|
# Predecessor order: custom SDK > env var SW_APM_TRANSACTION_NAME > automatic naming
|
|
123
136
|
def calculate_transaction_names(span)
|
|
124
|
-
return calculate_lambda_transaction_name(span.name) if
|
|
137
|
+
return calculate_lambda_transaction_name(span.name) if @is_lambda
|
|
125
138
|
|
|
126
139
|
trace_span_id = "#{span.context.hex_trace_id}-#{span.context.hex_span_id}"
|
|
127
140
|
trans_name = @txn_manager.get(trace_span_id)
|
|
128
141
|
if trans_name
|
|
129
|
-
SolarWindsAPM.logger.debug
|
|
130
|
-
"[#{self.class}/#{__method__}] found trans name from txn_manager: #{trans_name} by #{trace_span_id}"
|
|
131
|
-
end
|
|
142
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Using transaction name from txn_manager: #{trans_name} (#{trace_span_id})" }
|
|
132
143
|
@txn_manager.del(trace_span_id)
|
|
133
144
|
elsif !ENV['SW_APM_TRANSACTION_NAME'].to_s.empty?
|
|
134
145
|
trans_name = ENV.fetch('SW_APM_TRANSACTION_NAME', nil)
|
|
146
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Using transaction name from env var: #{trans_name}" }
|
|
135
147
|
else
|
|
136
|
-
trans_name = span.attributes[HTTP_ROUTE]
|
|
137
|
-
trans_name = span.name if trans_name.to_s.empty?
|
|
148
|
+
trans_name = span.attributes[HTTP_ROUTE]
|
|
149
|
+
trans_name = span.name if trans_name.to_s.empty?
|
|
150
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Using transaction name from span.attributes: #{span.attributes[HTTP_ROUTE]} or span.name: #{span.name}" }
|
|
138
151
|
end
|
|
139
152
|
trans_name.to_s.slice(0, SolarWindsAPM::Constants::MAX_TXN_NAME_LENGTH)
|
|
140
153
|
end
|
|
@@ -163,14 +176,14 @@ module SolarWindsAPM
|
|
|
163
176
|
|
|
164
177
|
# This span from inbound HTTP request if from a SERVER by some http.method
|
|
165
178
|
def span_http?(span)
|
|
166
|
-
span.kind == ::OpenTelemetry::Trace::SpanKind::SERVER
|
|
179
|
+
(!span.attributes[HTTP_METHOD].nil? || !span.attributes[HTTP_REQUEST_METHOD].nil?) && span.kind == ::OpenTelemetry::Trace::SpanKind::SERVER
|
|
167
180
|
end
|
|
168
181
|
|
|
169
182
|
# Calculate HTTP status_code from span or default to UNAVAILABLE
|
|
170
183
|
# Something went wrong in OTel or instrumented service crashed early
|
|
171
184
|
# if no status_code in attributes of HTTP span
|
|
172
185
|
def get_http_status_code(span)
|
|
173
|
-
span.attributes[HTTP_STATUS_CODE] || INVALID_HTTP_STATUS_CODE
|
|
186
|
+
span.attributes[HTTP_RESPONSE_STATUS_CODE] || span.attributes[HTTP_STATUS_CODE] || INVALID_HTTP_STATUS_CODE
|
|
174
187
|
end
|
|
175
188
|
|
|
176
189
|
# check if it's entry span based on no parent or parent is remote
|
|
@@ -40,6 +40,9 @@ module SolarWindsAPM
|
|
|
40
40
|
context = ::OpenTelemetry::Context.new({}) if context.nil?
|
|
41
41
|
context = inject_extracted_header(carrier, context, getter, XTRACEOPTIONS_HEADER_NAME, INTL_SWO_X_OPTIONS_KEY)
|
|
42
42
|
inject_extracted_header(carrier, context, getter, XTRACEOPTIONS_SIGNATURE_HEADER_NAME, INTL_SWO_SIGNATURE_KEY)
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Extraction failed: #{e.message}" }
|
|
45
|
+
context || ::OpenTelemetry::Context.current
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
# Inject trace context into the supplied carrier.
|
|
@@ -80,10 +83,13 @@ module SolarWindsAPM
|
|
|
80
83
|
end
|
|
81
84
|
setter.set(carrier, TRACESTATE_HEADER_NAME, Utils.trace_state_header(trace_state))
|
|
82
85
|
end
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Injection failed: #{e.message}" }
|
|
83
88
|
end
|
|
84
89
|
|
|
85
|
-
# Returns the predefined propagation fields
|
|
86
|
-
# should delete the fields returned by
|
|
90
|
+
# Returns the predefined propagation fields, required by upstream.
|
|
91
|
+
# If your carrier is reused, you should delete the fields returned by
|
|
92
|
+
# this method before calling +inject+.
|
|
87
93
|
#
|
|
88
94
|
# @return [Array<String>] a list of fields that will be used by this propagator.
|
|
89
95
|
def fields
|
|
@@ -56,31 +56,21 @@ module SolarWindsAPM
|
|
|
56
56
|
xtraceoptions_response)
|
|
57
57
|
end
|
|
58
58
|
setter.set(carrier, HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, exposed_headers.join(','))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# Returns the predefined propagation fields. If your carrier is reused, you
|
|
62
|
-
# should delete the fields returned by this method before calling +inject+.
|
|
63
|
-
#
|
|
64
|
-
# @return [Array<String>] a list of fields that will be used by this propagator.
|
|
65
|
-
def fields
|
|
66
|
-
TRACESTATE_HEADER_NAME
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] Injection failed: #{e.message}" }
|
|
67
61
|
end
|
|
68
62
|
|
|
69
63
|
private
|
|
70
64
|
|
|
71
|
-
#
|
|
65
|
+
# SW_XTRACEOPTIONS_RESPONSE_KEY -> xtrace_options_response
|
|
72
66
|
def recover_response_from_tracestate(span_context)
|
|
73
67
|
sanitized = span_context.tracestate.value(SW_XTRACEOPTIONS_RESPONSE_KEY)
|
|
74
68
|
sanitized = '' if sanitized.nil?
|
|
75
69
|
sanitized = sanitized.gsub(SolarWindsAPM::Constants::INTL_SWO_EQUALS_W3C_SANITIZED,
|
|
76
70
|
SolarWindsAPM::Constants::INTL_SWO_EQUALS)
|
|
77
71
|
sanitized = sanitized.gsub(':', SolarWindsAPM::Constants::INTL_SWO_EQUALS)
|
|
78
|
-
sanitized
|
|
79
|
-
|
|
80
|
-
SolarWindsAPM.logger.debug do
|
|
81
|
-
"[#{self.class}/#{__method__}] recover_response_from_tracestate sanitized: #{sanitized.inspect}"
|
|
82
|
-
end
|
|
83
|
-
sanitized
|
|
72
|
+
sanitized.gsub(SolarWindsAPM::Constants::INTL_SWO_COMMA_W3C_SANITIZED,
|
|
73
|
+
SolarWindsAPM::Constants::INTL_SWO_COMMA)
|
|
84
74
|
end
|
|
85
75
|
end
|
|
86
76
|
end
|
|
@@ -11,6 +11,9 @@ require 'opentelemetry-metrics-sdk'
|
|
|
11
11
|
require 'opentelemetry-exporter-otlp'
|
|
12
12
|
require 'opentelemetry-exporter-otlp-metrics'
|
|
13
13
|
require 'opentelemetry-instrumentation-all'
|
|
14
|
+
require 'opentelemetry-logs-sdk'
|
|
15
|
+
require 'opentelemetry-exporter-otlp-logs'
|
|
16
|
+
require 'opentelemetry-instrumentation-logger'
|
|
14
17
|
|
|
15
18
|
require_relative 'opentelemetry/solarwinds_propagator'
|
|
16
19
|
require_relative 'opentelemetry/solarwinds_response_propagator'
|
|
@@ -25,8 +25,6 @@ module SolarWindsAPM
|
|
|
25
25
|
|
|
26
26
|
is_lambda = SolarWindsAPM::Utils.determine_lambda
|
|
27
27
|
|
|
28
|
-
ENV['OTEL_TRACES_EXPORTER'] = ENV['OTEL_TRACES_EXPORTER'].to_s.split(',').tap { |e| e << 'otlp' unless e.include?('otlp') }.join(',')
|
|
29
|
-
|
|
30
28
|
# add response propagator to rack instrumentation
|
|
31
29
|
resolve_response_propagator
|
|
32
30
|
|
|
@@ -74,6 +72,9 @@ module SolarWindsAPM
|
|
|
74
72
|
ENV['OTEL_LOG_LEVEL'] = SolarWindsAPM::Config::SW_LOG_LEVEL_MAPPING.dig(log_level, :otel)
|
|
75
73
|
end
|
|
76
74
|
|
|
75
|
+
# disable log bridge by default
|
|
76
|
+
ENV['OTEL_RUBY_INSTRUMENTATION_LOGGER_ENABLED'] = 'false' unless %w[true false].include?(ENV['OTEL_RUBY_INSTRUMENTATION_LOGGER_ENABLED'].to_s)
|
|
77
|
+
|
|
77
78
|
::OpenTelemetry::SDK.configure do |c|
|
|
78
79
|
c.resource = final_attributes
|
|
79
80
|
c.use_all(@@config_map)
|
|
@@ -12,8 +12,9 @@ module SolarWindsAPM
|
|
|
12
12
|
attr_reader :rate, :scale
|
|
13
13
|
|
|
14
14
|
def initialize(settings)
|
|
15
|
-
@scale = settings[:scale]
|
|
15
|
+
@scale = settings[:scale] || 1_000_000
|
|
16
16
|
@rate = settings[:rate] || 0
|
|
17
|
+
@random_generator = Random.new
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def update(settings)
|
|
@@ -23,11 +24,12 @@ module SolarWindsAPM
|
|
|
23
24
|
|
|
24
25
|
# return Boolean
|
|
25
26
|
def roll
|
|
26
|
-
|
|
27
|
+
rand_num = @random_generator.rand
|
|
28
|
+
SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] rand_num=#{rand_num.round(6)}, @scale=#{@scale}, @rate=#{@rate}" }
|
|
29
|
+
(rand_num * @scale) < @rate
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def rate=(rate)
|
|
30
|
-
# Math.max(0, Math.min(this.#scale, n))
|
|
31
33
|
@rate = rate.clamp(0, @scale)
|
|
32
34
|
end
|
|
33
35
|
end
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
module SolarWindsAPM
|
|
10
10
|
class HttpSampler < Sampler
|
|
11
11
|
REQUEST_TIMEOUT = 10 # 10s
|
|
12
|
-
|
|
12
|
+
GET_SETTING_DURATION = 60 # 60s
|
|
13
13
|
|
|
14
14
|
# we don't need hostname as it's for separating browser and local env
|
|
15
15
|
def initialize(config)
|
|
@@ -22,11 +22,32 @@ module SolarWindsAPM
|
|
|
22
22
|
@hostname = hostname
|
|
23
23
|
@setting_url = URI.join(@url, "./v1/settings/#{@service}/#{@hostname}")
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
@pid = nil
|
|
26
|
+
@thread = nil
|
|
27
|
+
|
|
28
|
+
@logger.debug { "[#{self.class}/#{__method__}] HttpSampler initialized: url=#{@url}, service=#{@service}, hostname=#{@hostname}, setting_url=#{@setting_url}" }
|
|
29
|
+
reset_on_fork
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# restart the settings request thread after forking
|
|
33
|
+
def should_sample?(params)
|
|
34
|
+
reset_on_fork
|
|
35
|
+
super
|
|
26
36
|
end
|
|
27
37
|
|
|
28
38
|
private
|
|
29
39
|
|
|
40
|
+
def reset_on_fork
|
|
41
|
+
pid = Process.pid
|
|
42
|
+
return if @pid == pid
|
|
43
|
+
|
|
44
|
+
@pid = pid
|
|
45
|
+
@thread = Thread.new { settings_request }
|
|
46
|
+
@logger.debug { "[#{self.class}/#{__method__}] Restart the settings_request thread in process: #{@pid}." }
|
|
47
|
+
rescue ThreadError => e
|
|
48
|
+
@logger.error { "[#{self.class}/#{__method__}] Unexpected error in HttpSampler#reset_on_fork: #{e.message}" }
|
|
49
|
+
end
|
|
50
|
+
|
|
30
51
|
# Node.js equivalent: Retrieve system hostname
|
|
31
52
|
# e.g. docker -> docker.swo.ubuntu.development; macos -> NHSDFWSSD
|
|
32
53
|
def hostname
|
|
@@ -36,52 +57,70 @@ module SolarWindsAPM
|
|
|
36
57
|
|
|
37
58
|
def fetch_with_timeout(url, timeout_seconds = nil)
|
|
38
59
|
uri = url
|
|
60
|
+
timeout = timeout_seconds || REQUEST_TIMEOUT
|
|
61
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
62
|
+
|
|
63
|
+
remaining = lambda {
|
|
64
|
+
r = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
65
|
+
r.negative? ? 0.0 : r
|
|
66
|
+
}
|
|
67
|
+
|
|
39
68
|
response = nil
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
begin
|
|
42
71
|
::OpenTelemetry::Common::Utilities.untraced do
|
|
43
|
-
Net::HTTP.start(
|
|
72
|
+
Net::HTTP.start(
|
|
73
|
+
uri.host, uri.port,
|
|
74
|
+
use_ssl: uri.scheme == 'https',
|
|
75
|
+
open_timeout: timeout,
|
|
76
|
+
read_timeout: timeout # will shrink later
|
|
77
|
+
) do |http|
|
|
44
78
|
request = Net::HTTP::Get.new(uri)
|
|
45
79
|
request['Authorization'] = @headers
|
|
46
80
|
|
|
81
|
+
# Before issuing request, tighten read_timeout to remaining
|
|
82
|
+
http.read_timeout = remaining.call
|
|
47
83
|
response = http.request(request)
|
|
48
84
|
end
|
|
49
85
|
end
|
|
86
|
+
rescue Net::ReadTimeout, Net::OpenTimeout
|
|
87
|
+
@logger.debug { "Request timed out after #{timeout} seconds" }
|
|
50
88
|
rescue StandardError => e
|
|
51
89
|
@logger.debug { "Error during request: #{e.message}" }
|
|
52
90
|
end
|
|
53
91
|
|
|
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
92
|
response
|
|
61
93
|
end
|
|
62
94
|
|
|
63
95
|
# a endless loop within a thread (non-blocking)
|
|
64
96
|
def settings_request
|
|
97
|
+
@logger.debug { "[#{self.class}/#{__method__}] Starting settings request loop" }
|
|
98
|
+
sleep_duration = GET_SETTING_DURATION
|
|
65
99
|
loop do
|
|
66
|
-
@logger.debug { "Retrieving sampling settings from #{@setting_url}" }
|
|
67
|
-
|
|
68
100
|
response = fetch_with_timeout(@setting_url)
|
|
69
|
-
parsed = response.nil? ? nil : JSON.parse(response.body)
|
|
70
101
|
|
|
71
|
-
|
|
102
|
+
# Check for nil response from timeout
|
|
103
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
104
|
+
@logger.warn { "[#{self.class}/#{__method__}] Failed to retrieve settings due to timeout." }
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
parsed = JSON.parse(response.body)
|
|
72
109
|
|
|
73
110
|
if update_settings(parsed)
|
|
74
111
|
# update the settings before the previous ones expire with some time to spare
|
|
75
112
|
expiry = (parsed['timestamp'].to_i + parsed['ttl'].to_i)
|
|
76
113
|
expiry_timeout = expiry - REQUEST_TIMEOUT - Time.now.to_i
|
|
77
|
-
|
|
114
|
+
sleep_duration = [0, expiry_timeout].max
|
|
78
115
|
else
|
|
79
|
-
@logger.warn {
|
|
80
|
-
sleep(GET_SETTING_DURAION)
|
|
116
|
+
@logger.warn { "[#{self.class}/#{__method__}] Retrieved sampling settings are invalid. Ensure proper configuration." }
|
|
81
117
|
end
|
|
118
|
+
rescue JSON::ParserError => e
|
|
119
|
+
@logger.warn { "[#{self.class}/#{__method__}] JSON parsing error: #{e.message}" }
|
|
82
120
|
rescue StandardError => e
|
|
83
|
-
@logger.warn { "Failed to retrieve sampling settings (#{e.message}), tracing will be disabled until valid ones are available." }
|
|
84
|
-
|
|
121
|
+
@logger.warn { "[#{self.class}/#{__method__}] Failed to retrieve sampling settings (#{e.message}), tracing will be disabled until valid ones are available." }
|
|
122
|
+
ensure
|
|
123
|
+
sleep(sleep_duration)
|
|
85
124
|
end
|
|
86
125
|
end
|
|
87
126
|
end
|