solarwinds_apm 6.1.2 → 7.0.0.prev2

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -3
  3. data/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb +1 -30
  4. data/lib/solarwinds_apm/api/current_trace_info.rb +10 -6
  5. data/lib/solarwinds_apm/api/custom_metrics.rb +8 -25
  6. data/lib/solarwinds_apm/api/tracing.rb +12 -27
  7. data/lib/solarwinds_apm/api/transaction_name.rb +6 -10
  8. data/lib/solarwinds_apm/config.rb +7 -1
  9. data/lib/solarwinds_apm/constants.rb +1 -0
  10. data/lib/solarwinds_apm/noop/api.rb +5 -2
  11. data/lib/solarwinds_apm/noop.rb +0 -24
  12. data/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +116 -66
  13. data/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb +0 -2
  14. data/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb +5 -4
  15. data/lib/solarwinds_apm/opentelemetry.rb +5 -7
  16. data/lib/solarwinds_apm/otel_native_config.rb +180 -0
  17. data/lib/solarwinds_apm/patch/README.md +15 -0
  18. data/lib/solarwinds_apm/{noop/metadata.rb → sampling/dice.rb} +19 -17
  19. data/lib/solarwinds_apm/sampling/http_sampler.rb +87 -0
  20. data/lib/solarwinds_apm/sampling/json_sampler.rb +52 -0
  21. data/lib/solarwinds_apm/sampling/metrics.rb +38 -0
  22. data/lib/solarwinds_apm/sampling/oboe_sampler.rb +348 -0
  23. data/lib/solarwinds_apm/sampling/sampler.rb +197 -0
  24. data/lib/solarwinds_apm/sampling/sampling_constants.rb +127 -0
  25. data/lib/solarwinds_apm/sampling/sampling_patch.rb +49 -0
  26. data/lib/solarwinds_apm/sampling/setting_example.txt +1 -0
  27. data/lib/solarwinds_apm/{noop/context.rb → sampling/settings.rb} +14 -25
  28. data/lib/solarwinds_apm/sampling/token_bucket.rb +126 -0
  29. data/lib/solarwinds_apm/sampling/trace_options.rb +100 -0
  30. data/lib/solarwinds_apm/{patch.rb → sampling.rb} +20 -4
  31. data/lib/solarwinds_apm/support/logger_formatter.rb +1 -1
  32. data/lib/solarwinds_apm/support/logging_log_event.rb +1 -1
  33. data/lib/solarwinds_apm/support/lumberjack_formatter.rb +1 -1
  34. data/lib/solarwinds_apm/support/otlp_endpoint.rb +99 -0
  35. data/lib/solarwinds_apm/support/resource_detector.rb +192 -0
  36. data/lib/solarwinds_apm/support/service_key_checker.rb +12 -6
  37. data/lib/solarwinds_apm/support/transaction_settings.rb +6 -0
  38. data/lib/solarwinds_apm/support/txn_name_manager.rb +54 -9
  39. data/lib/solarwinds_apm/support/utils.rb +9 -0
  40. data/lib/solarwinds_apm/support.rb +2 -4
  41. data/lib/solarwinds_apm/version.rb +4 -4
  42. data/lib/solarwinds_apm.rb +27 -73
  43. metadata +107 -40
  44. data/ext/oboe_metal/extconf.rb +0 -168
  45. data/ext/oboe_metal/lib/liboboe-1.0-aarch64.so.sha256 +0 -1
  46. data/ext/oboe_metal/lib/liboboe-1.0-alpine-aarch64.so.sha256 +0 -1
  47. data/ext/oboe_metal/lib/liboboe-1.0-alpine-x86_64.so.sha256 +0 -1
  48. data/ext/oboe_metal/lib/liboboe-1.0-lambda-aarch64.so.sha256 +0 -1
  49. data/ext/oboe_metal/lib/liboboe-1.0-lambda-x86_64.so.sha256 +0 -1
  50. data/ext/oboe_metal/lib/liboboe-1.0-x86_64.so.sha256 +0 -1
  51. data/ext/oboe_metal/src/VERSION +0 -1
  52. data/ext/oboe_metal/src/bson/bson.h +0 -220
  53. data/ext/oboe_metal/src/bson/platform_hacks.h +0 -91
  54. data/ext/oboe_metal/src/init_solarwinds_apm.cc +0 -18
  55. data/ext/oboe_metal/src/oboe.h +0 -930
  56. data/ext/oboe_metal/src/oboe_api.cpp +0 -793
  57. data/ext/oboe_metal/src/oboe_api.h +0 -621
  58. data/ext/oboe_metal/src/oboe_debug.h +0 -17
  59. data/ext/oboe_metal/src/oboe_swig_wrap.cc +0 -11045
  60. data/lib/oboe_metal.rb +0 -187
  61. data/lib/solarwinds_apm/cert/star.appoptics.com.issuer.crt +0 -24
  62. data/lib/solarwinds_apm/noop/span.rb +0 -25
  63. data/lib/solarwinds_apm/oboe_init_options.rb +0 -222
  64. data/lib/solarwinds_apm/opentelemetry/solarwinds_exporter.rb +0 -239
  65. data/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb +0 -174
  66. data/lib/solarwinds_apm/opentelemetry/solarwinds_sampler.rb +0 -333
  67. data/lib/solarwinds_apm/otel_config.rb +0 -174
  68. data/lib/solarwinds_apm/otel_lambda_config.rb +0 -56
  69. data/lib/solarwinds_apm/patch/dummy_patch.rb +0 -12
  70. data/lib/solarwinds_apm/support/oboe_tracing_mode.rb +0 -33
  71. data/lib/solarwinds_apm/support/support_report.rb +0 -99
  72. data/lib/solarwinds_apm/support/transaction_cache.rb +0 -57
  73. data/lib/solarwinds_apm/support/x_trace_options.rb +0 -138
@@ -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'
@@ -47,4 +47,4 @@ end
47
47
 
48
48
  # To use the trace context in log, ::Logger::Formatter.new must be defined
49
49
  # e.g. config.log_formatter = ::Logger::Formatter.new
50
- Logger::Formatter.prepend(SolarWindsAPM::Logger::Formatter) if SolarWindsAPM.loaded
50
+ Logger::Formatter.prepend(SolarWindsAPM::Logger::Formatter)
@@ -23,4 +23,4 @@ module SolarWindsAPM
23
23
  end
24
24
  end
25
25
 
26
- Logging::LogEvent.prepend(SolarWindsAPM::Logging::LogEvent) if SolarWindsAPM.loaded && defined?(Logging::LogEvent)
26
+ Logging::LogEvent.prepend(SolarWindsAPM::Logging::LogEvent) if defined?(Logging::LogEvent)
@@ -23,4 +23,4 @@ module SolarWindsAPM
23
23
  end
24
24
  end
25
25
 
26
- Lumberjack::LogEntry.prepend(SolarWindsAPM::Lumberjack::LogEntry) if SolarWindsAPM.loaded && defined?(Lumberjack::LogEntry)
26
+ Lumberjack::LogEntry.prepend(SolarWindsAPM::Lumberjack::LogEntry) if defined?(Lumberjack::LogEntry)
@@ -0,0 +1,99 @@
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
+ require_relative 'service_key_checker'
10
+
11
+ module SolarWindsAPM
12
+ # OTLPEndPoint
13
+ class OTLPEndPoint
14
+ attr_reader :token, :service_name
15
+
16
+ SWO_APM_ENDPOINT_REGEX = /^apm\.collector\.([a-z]{2}-\d{2})\.([^.]+)\.solarwinds\.com(?::\d+)?$/
17
+ SWO_APM_ENDPOINT_DEFAULT = 'apm.collector.na-01.cloud.solarwinds.com:443'
18
+
19
+ SWO_OTLP_GENERAL_ENDPOINT_REGEX = %r{^https://otel\.collector\.[a-z0-9-]+\.[a-z0-9-]+\.solarwinds\.com(?::\d+)?$}
20
+ SWO_OTLP_SIGNAL_ENDPOINT_REGEX = %r{^https://otel\.collector\.[a-z0-9-]+\.[a-z0-9-]+\.solarwinds\.com(?::\d+)?/v1/(?:logs|metrics|traces)$}
21
+ SWO_OTLP_ENDPOINT_DEFAULT = 'https://otel.collector.na-01.cloud.solarwinds.com:443'
22
+
23
+ OTEL_SIGNAL_TYPE = %w[TRACES METRICS LOGS].freeze
24
+
25
+ def initialize
26
+ @token = nil
27
+ @service_name = nil
28
+ end
29
+
30
+ def config_otlp_token_and_endpoint
31
+ matches = ENV['SW_APM_COLLECTOR'].to_s.match(SWO_APM_ENDPOINT_REGEX)
32
+
33
+ resolve_get_setting_endpoint(matches)
34
+
35
+ service_key_checker = SolarWindsAPM::ServiceKeyChecker.new('ssl', SolarWindsAPM::Utils.determine_lambda)
36
+ @token = service_key_checker.token
37
+ @service_name = service_key_checker.service_name
38
+
39
+ OTEL_SIGNAL_TYPE.each do |data_type|
40
+ config_token(data_type)
41
+ configure_otlp_endpoint(data_type, matches)
42
+ end
43
+ end
44
+
45
+ # APM Libraries should only set the bearer token header as a convenience if:
46
+ # The OTEL config for exporter OTLP headers is not already set, i.e. explicitly configured by the end user, AND
47
+ # The OTLP export endpoint is SWO, i.e. host is otel.collector.*.*.solarwinds.com
48
+ def config_token(data_type)
49
+ data_type_upper = data_type.upcase
50
+
51
+ return unless @token
52
+
53
+ if ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_HEADERS"].to_s.empty? && ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"].to_s.match?(SWO_OTLP_SIGNAL_ENDPOINT_REGEX)
54
+ ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_HEADERS"] = "authorization=Bearer #{@token}"
55
+ elsif ENV['OTEL_EXPORTER_OTLP_HEADERS'].to_s.empty? && ENV['OTEL_EXPORTER_OTLP_ENDPOINT'].to_s.match?(SWO_OTLP_GENERAL_ENDPOINT_REGEX)
56
+ ENV['OTEL_EXPORTER_OTLP_HEADERS'] = "authorization=Bearer #{@token}"
57
+ end
58
+
59
+ ENV['OTEL_EXPORTER_OTLP_HEADERS'] = "authorization=Bearer #{@token}" if ENV['OTEL_EXPORTER_OTLP_HEADERS'].to_s.empty? && ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_HEADERS"].to_s.empty?
60
+ end
61
+
62
+ def configure_otlp_endpoint(data_type, matches)
63
+ data_type_upper = data_type.upcase
64
+ data_type_lower = data_type.downcase
65
+ otlp_endpoint_source = if ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"]
66
+ "#{data_type_lower}_endpoint"
67
+ elsif ENV['OTEL_EXPORTER_OTLP_ENDPOINT']
68
+ 'general_endpoint'
69
+ else
70
+ 'no_endpoint'
71
+ end
72
+
73
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] otlp_endpoint_source: #{otlp_endpoint_source}" }
74
+
75
+ return unless otlp_endpoint_source == 'no_endpoint'
76
+
77
+ if matches&.size == 3
78
+ region = matches[1]
79
+ env = matches[2]
80
+ ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"] = "https://otel.collector.#{region}.#{env}.solarwinds.com:443/v1/#{data_type_lower}"
81
+ else
82
+ ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"] = "https://otel.collector.na-01.cloud.solarwinds.com:443/v1/#{data_type_lower}"
83
+ end
84
+ end
85
+
86
+ # only valid value is apm.collector.*.*.solarwinds.com
87
+ # If SW APM config for collector is not set, the fallback: apm.collector.na-01.cloud.solarwinds.com
88
+ def resolve_get_setting_endpoint(matches)
89
+ return if matches&.size == 3
90
+
91
+ unless ENV['SW_APM_COLLECTOR'].to_s.empty?
92
+ SolarWindsAPM.logger.warn do
93
+ "[#{self.class}/#{__method__}] SW_APM_COLLECTOR format invalid: #{ENV.fetch('SW_APM_COLLECTOR', nil)}. Valid format: apm.collector.*.*.solarwinds.com"
94
+ end
95
+ end
96
+ ENV['SW_APM_COLLECTOR'] = SWO_APM_ENDPOINT_DEFAULT
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,192 @@
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
+ require 'net/http'
10
+ require 'uri'
11
+ require 'json'
12
+ require 'socket'
13
+ require 'securerandom'
14
+ require 'opentelemetry/resource/detector/azure'
15
+ require 'opentelemetry/resource/detector/container'
16
+ require 'opentelemetry/resource/detector/aws/ec2'
17
+
18
+ module SolarWindsAPM
19
+ # ResourceDetector
20
+ # Usage:
21
+ # require 'opentelemetry/sdk'
22
+ # require 'opentelemetry/resource/detector'
23
+ # OpenTelemetry::SDK.configure do |c|
24
+ # c.resource = SolarWindsAPM::ResourceDetector.detect
25
+ # end
26
+ module ResourceDetector
27
+ K8S_PODNAME_PATH = '/etc/hostname'
28
+ K8S_NAMESPACE_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/namespace'
29
+ K8S_NAMESPACE_PATH_WIN = 'C:\\var\\run\\secrets\\kubernetes.io\\serviceaccount\\namespace'
30
+ K8S_MOUNTINFO_FILE = '/proc/self/mountinfo'
31
+ UID_REGEX = /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/i
32
+
33
+ SW_K8S_NAMESPACE_ENV = 'SW_K8S_POD_NAMESPACE'
34
+ SW_K8S_UID_ENV = 'SW_K8S_POD_UID'
35
+ SW_K8S_NAME_ENV = 'SW_K8S_POD_NAME'
36
+
37
+ UAMS_CLIENT_PATH = '/opt/solarwinds/uamsclient/var/uamsclientid'
38
+ UAMS_CLIENT_PATH_WIN = 'C:\\ProgramData\\SolarWinds\\UAMSClient\\uamsclientid'
39
+ UAMS_CLIENT_URL = 'http://127.0.0.1:2113/info/uamsclient'
40
+ UAMS_CLIENT_ID_FIELD = 'uamsclient_id'
41
+
42
+ def self.detect
43
+ uuid_attr = { ::OpenTelemetry::SemanticConventions::Resource::SERVICE_INSTANCE_ID => random_uuid }
44
+ attributes = ::OpenTelemetry::SDK::Resources::Resource.create(uuid_attr)
45
+ attributes = attributes.merge(detect_uams_client_id)
46
+ attributes = attributes.merge(detect_k8s_attributes)
47
+ attributes = attributes.merge(detect_ec2)
48
+ attributes = attributes.merge(detect_azure)
49
+ attributes.merge(detect_container)
50
+ end
51
+
52
+ def self.detect_uams_client_id
53
+ uams_client_final_path = windows? ? UAMS_CLIENT_PATH_WIN : UAMS_CLIENT_PATH
54
+ uams_client_id = nil
55
+ begin
56
+ uams_client_id = File.read(uams_client_final_path).strip
57
+ rescue StandardError => e
58
+ SolarWindsAPM.logger.debug "#{self.class}/#{__method__}] uams file retrieve error #{e.message}."
59
+ end
60
+
61
+ if uams_client_id.nil?
62
+ begin
63
+ url = URI(UAMS_CLIENT_URL)
64
+
65
+ response = nil
66
+ ::OpenTelemetry::Common::Utilities.untraced do
67
+ http = Net::HTTP.new(url.host, url.port)
68
+ request = Net::HTTP::Get.new(url)
69
+ response = http.request(request)
70
+ end
71
+
72
+ raise 'Response returned non-200 status code' unless response&.code.to_i == 200
73
+
74
+ uams_metadata = JSON.parse(response.body)
75
+ uams_client_id = uams_metadata&.fetch(UAMS_CLIENT_ID_FIELD)
76
+ rescue StandardError => e
77
+ SolarWindsAPM.logger.debug "#{self.class}/#{__method__}] uams api retrieve error #{e.message}."
78
+ end
79
+ end
80
+
81
+ resource_attributes = {
82
+ 'sw.uams.client.id' => uams_client_id,
83
+ 'host.id' => uams_client_id
84
+ }
85
+
86
+ SolarWindsAPM.logger.debug "#{self.class}/#{__method__}] retrieved resource_attributes: #{resource_attributes.inspect}."
87
+ ::OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
88
+ rescue StandardError => e
89
+ SolarWindsAPM.logger.debug "#{self.class}/#{__method__}] detect_uams_client_id failed. Error: #{e.message}."
90
+ ::OpenTelemetry::SDK::Resources::Resource.create({})
91
+ end
92
+
93
+ def self.detect_k8s_attributes
94
+ unless ENV['KUBERNETES_SERVICE_HOST'] && ENV['KUBERNETES_SERVICE_PORT']
95
+ SolarWindsAPM.logger.debug { "Can't read environment variable (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT). It's likely not in kubernetes pod environment. No K8S resource detection." }
96
+ return ::OpenTelemetry::SDK::Resources::Resource.create({})
97
+ end
98
+
99
+ pod_name = ENV.fetch(SW_K8S_NAME_ENV, nil)
100
+ if pod_name.nil?
101
+ pod_name = Socket.gethostname
102
+ else
103
+ SolarWindsAPM.logger.debug { "read pod name from env #{pod_name}" }
104
+ end
105
+
106
+ pod_namespace = ENV.fetch(SW_K8S_NAMESPACE_ENV, nil)
107
+ if pod_namespace.nil?
108
+ begin
109
+ k8s_namspace_final_path = windows? ? K8S_NAMESPACE_PATH_WIN : K8S_NAMESPACE_PATH
110
+ pod_namespace = File.read(k8s_namspace_final_path).strip
111
+ SolarWindsAPM.logger.debug { 'read pod namespace from file' }
112
+ rescue StandardError => e
113
+ SolarWindsAPM.logger.debug { "can't read pod namespace #{e.message}" }
114
+ end
115
+ else
116
+ SolarWindsAPM.logger.debug { "read pod namespace from env #{pod_namespace}" }
117
+ end
118
+
119
+ pod_uid = ENV.fetch(SW_K8S_UID_ENV, nil)
120
+ if pod_uid.nil?
121
+ begin
122
+ File.open(K8S_MOUNTINFO_FILE) do |file|
123
+ file.each_line do |line|
124
+ fields = line.split
125
+ next if fields.size < 10
126
+
127
+ id, parent_id, _, root = fields
128
+ next unless safe_integer?(id) && safe_integer?(parent_id)
129
+ next unless root.include?('kube')
130
+
131
+ matches = UID_REGEX.match(root)
132
+ pod_uid = matches[0] if matches
133
+ break if pod_uid
134
+ end
135
+ end
136
+ rescue StandardError => e
137
+ SolarWindsAPM.logger.debug { "can't read pod uid #{e.message}" }
138
+ end
139
+ else
140
+ SolarWindsAPM.logger.debug { "read pod uid from env #{pod_uid}" }
141
+ end
142
+
143
+ resource_attributes = {
144
+ ::OpenTelemetry::SemanticConventions::Resource::K8S_NAMESPACE_NAME => pod_namespace,
145
+ ::OpenTelemetry::SemanticConventions::Resource::K8S_POD_NAME => pod_name,
146
+ ::OpenTelemetry::SemanticConventions::Resource::K8S_POD_UID => pod_uid
147
+ }
148
+
149
+ resource_attributes.compact!
150
+ SolarWindsAPM.logger.debug { "#{self.class}/#{__method__}] retrieved resource_attributes: #{resource_attributes.inspect}." }
151
+ ::OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
152
+ end
153
+
154
+ def self.detect_ec2
155
+ attribute = ::OpenTelemetry::Resource::Detector::AWS::EC2.detect
156
+ SolarWindsAPM.logger.debug { "#{self.class}/#{__method__}] retrieved resource_attributes: #{attribute.instance_variable_get(:@attributes)}" }
157
+ attribute
158
+ rescue StandardError
159
+ ::OpenTelemetry::SDK::Resources::Resource.create({})
160
+ end
161
+
162
+ def self.detect_azure
163
+ attribute = ::OpenTelemetry::Resource::Detector::Azure.detect
164
+ SolarWindsAPM.logger.debug { "#{self.class}/#{__method__}] retrieved resource_attributes: #{attribute.instance_variable_get(:@attributes)}" }
165
+ attribute
166
+ rescue StandardError
167
+ ::OpenTelemetry::SDK::Resources::Resource.create({})
168
+ end
169
+
170
+ def self.detect_container
171
+ attribute = ::OpenTelemetry::Resource::Detector::Container.detect
172
+ SolarWindsAPM.logger.debug { "#{self.class}/#{__method__}] retrieved resource_attributes: #{attribute.instance_variable_get(:@attributes)}" }
173
+ attribute
174
+ rescue StandardError
175
+ ::OpenTelemetry::SDK::Resources::Resource.create({})
176
+ end
177
+
178
+ def self.safe_integer?(number)
179
+ min_safe_integer = -((2**53) - 1)
180
+ max_safe_integer = (2**53) - 1
181
+ number.is_a?(Integer) && number >= min_safe_integer && number <= max_safe_integer
182
+ end
183
+
184
+ def self.windows?
185
+ %w[mingw32 cygwin].any? { |platform| RUBY_PLATFORM.include?(platform) }
186
+ end
187
+
188
+ def self.random_uuid
189
+ SecureRandom.uuid
190
+ end
191
+ end
192
+ end