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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b86f18a58a518205b6f5d0d8e4054e821d2e2cc22b86366ff0ab13d0b2542322
4
- data.tar.gz: 8e6a2c4dbbee0e2689b36086f1f003237d1e6f0adefd36cb13038a782e449e50
3
+ metadata.gz: d565e746144994b5c3a81ee19665ee9b2d4e81fbad2e21da567465455c37e6c0
4
+ data.tar.gz: 949351c94748745d2ba19268a531ba5b4f805568132772dcfb9749b0af3698a8
5
5
  SHA512:
6
- metadata.gz: 1884c401f28f7b57e0df2e2b0784b28964e50785555542376ad0527d72a7c43636115d48d332908a200fff3f2dd42160d8f4e79ec21e3e302e41ae048848f0eb
7
- data.tar.gz: 339c983072fc99986d575593a1bbcd84b5ff9728c61a47279de70ba010416b5fdad7cfa2d9b457c206049b1533ad87212f7b4ac26091f3dbcd7c68ad10a4edf1
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 DogfoodsController < ApplicationController
24
+ # class OrdersController < ApplicationController
25
25
  #
26
26
  # def create
27
- # @dogfood = Dogfood.new(params.permit(:brand, :name))
28
- # @dogfood.save
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
- # SolarWindsAPM::API.set_transaction_name("dogfoodscontroller.create_for_#{params[:brand]}")
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
- env_value = ENV[env_var.to_s]&.downcase
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 && !env_value.to_s.empty?
103
- SolarWindsAPM.logger.warn("[#{name}/#{__method__}] #{env_var} must be #{valid_env_values.join('/')} (current setting is #{ENV.fetch(env_var, nil)}). Using default value: #{default}.")
104
- return @@config[key.to_sym] = default
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
- return @@config[key.to_sym] = value unless (bool && !boolean?(value)) || (!bool && !symbol?(value))
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
- SolarWindsAPM.logger.warn("[#{name}/#{__method__}] :#{key} must be a #{valid_env_values.join('/')}. Using default value: #{default}.")
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(nil, key, value, :enabled)
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
- disabled = settings.select { |v| !v.key?(:tracing) || v[:tracing] == :disabled }
257
- enabled = settings.select { |v| v[:tracing] == :enabled }
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
- we load these classes when in noop mode and they expose noop behavior.
25
+ - **TransactionName**:
26
+ - `set_transaction_name(custom_name=nil)` - Always returns true
9
27
 
10
- so far only one class is needed:
28
+ - **CustomInstrumentation/Tracer**:
29
+ - `add_tracer(method_name, span_name=nil, options={})` - Always returns nil.
11
30
 
12
- - SolarWindsAPM::Context and its toString() method from oboe
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
- SolarWindsAPM::API.extend(NoopAPI::Tracing)
85
- SolarWindsAPM::API.extend(NoopAPI::CurrentTraceInfo)
86
- SolarWindsAPM::API.extend(NoopAPI::CustomMetrics)
87
- SolarWindsAPM::API.extend(NoopAPI::OpenTelemetry)
88
- SolarWindsAPM::API.extend(NoopAPI::TransactionName)
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.to_span_data.inspect}" }
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__}] error processing span on_finish: #{e.message}" }
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
- instrument = @meters['sw.apm.request.metrics'].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.')
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
- if span_http?(span)
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['http.status_code'] = http_status_code if http_status_code != 0
110
- meter_attrs['http.method'] = span.attributes[HTTP_METHOD] if span.attributes[HTTP_METHOD]
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
- SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] meter_attrs: #{meter_attrs.inspect}" }
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 SolarWindsAPM::Utils.determine_lambda
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 do
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] || nil
137
- trans_name = span.name if trans_name.to_s.empty? && span.name
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 && !span.attributes[HTTP_METHOD].nil?
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. If your carrier is reused, you
86
- # should delete the fields returned by this method before calling +inject+.
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
- end
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
- # sw_xtraceoptions_response_key -> xtrace_options_response
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 = sanitized.gsub(SolarWindsAPM::Constants::INTL_SWO_COMMA_W3C_SANITIZED,
79
- SolarWindsAPM::Constants::INTL_SWO_COMMA)
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
- (rand * @scale) < @rate
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
- GET_SETTING_DURAION = 60 # 60s
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
- Thread.new { settings_request }
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
- thread = Thread.new do
70
+ begin
42
71
  ::OpenTelemetry::Common::Utilities.untraced do
43
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
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
- @logger.debug { "parsed settings in json: #{parsed.inspect}" }
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
- sleep([0, expiry_timeout].max)
114
+ sleep_duration = [0, expiry_timeout].max
78
115
  else
79
- @logger.warn { 'Retrieved sampling settings are invalid. Ensure proper configuration.' }
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
- sleep(GET_SETTING_DURAION)
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