scout_apm 5.8.0 → 6.0.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -0
  3. data/CHANGELOG.markdown +14 -2
  4. data/README.markdown +20 -8
  5. data/gems/instruments.gemfile +1 -0
  6. data/lib/scout_apm/auto_instrument/instruction_sequence.rb +2 -1
  7. data/lib/scout_apm/auto_instrument/parser.rb +150 -2
  8. data/lib/scout_apm/auto_instrument/prism.rb +357 -0
  9. data/lib/scout_apm/auto_instrument/rails.rb +9 -155
  10. data/lib/scout_apm/auto_instrument/requirements.rb +11 -0
  11. data/lib/scout_apm/background_job_integrations/delayed_job.rb +15 -1
  12. data/lib/scout_apm/background_job_integrations/sidekiq.rb +89 -1
  13. data/lib/scout_apm/config.rb +32 -7
  14. data/lib/scout_apm/context.rb +3 -1
  15. data/lib/scout_apm/error_service/error_record.rb +1 -1
  16. data/lib/scout_apm/instrument_manager.rb +2 -0
  17. data/lib/scout_apm/instruments/http_client.rb +10 -0
  18. data/lib/scout_apm/instruments/httpx.rb +119 -0
  19. data/lib/scout_apm/instruments/opensearch.rb +131 -0
  20. data/lib/scout_apm/sampling.rb +25 -13
  21. data/lib/scout_apm/server_integrations/puma.rb +21 -4
  22. data/lib/scout_apm/version.rb +1 -1
  23. data/lib/scout_apm.rb +9 -3
  24. data/test/unit/auto_instrument/controller-ast.prism.txt +1015 -0
  25. data/test/unit/auto_instrument/controller-instrumented.rb +36 -11
  26. data/test/unit/auto_instrument/controller.rb +25 -0
  27. data/test/unit/auto_instrument/hash_shorthand_controller-instrumented.rb +28 -10
  28. data/test/unit/auto_instrument/hash_shorthand_controller.rb +19 -1
  29. data/test/unit/auto_instrument_test.rb +7 -1
  30. data/test/unit/background_job_integrations/sidekiq_test.rb +38 -0
  31. data/test/unit/config_test.rb +14 -0
  32. data/test/unit/error_service/error_buffer_test.rb +31 -0
  33. data/test/unit/error_test.rb +1 -1
  34. data/test/unit/ignored_uris_test.rb +7 -0
  35. data/test/unit/instruments/http_client_test.rb +0 -2
  36. data/test/unit/instruments/httpx_test.rb +78 -0
  37. data/test/unit/sampling_test.rb +10 -10
  38. metadata +8 -2
  39. /data/test/unit/auto_instrument/{controller-ast.txt → controller-ast.parser.txt} +0 -0
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: false
2
+
3
+ module ScoutApm
4
+ module Instruments
5
+ class OpenSearch
6
+ attr_reader :context
7
+
8
+ def initialize(context)
9
+ @context = context
10
+ @installed = false
11
+ end
12
+
13
+ def logger
14
+ context.logger
15
+ end
16
+
17
+ def installed?
18
+ @installed
19
+ end
20
+
21
+ def install(prepend:)
22
+ if defined?(::OpenSearch) &&
23
+ defined?(::OpenSearch::Transport) &&
24
+ defined?(::OpenSearch::Transport::Client)
25
+
26
+ @installed = true
27
+
28
+ logger.info "Instrumenting OpenSearch. Prepend: #{prepend}"
29
+
30
+ if prepend
31
+ ::OpenSearch::Transport::Client.send(:include, ScoutApm::Tracer)
32
+ ::OpenSearch::Transport::Client.send(:prepend, OpenSearchTransportClientInstrumentationPrepend)
33
+ else
34
+ ::OpenSearch::Transport::Client.class_eval do
35
+ include ScoutApm::Tracer
36
+
37
+ def perform_request_with_scout_instruments(*args, &block)
38
+ name = _sanitize_name(args[1])
39
+
40
+ self.class.instrument("OpenSearch", name, :ignore_children => true) do
41
+ perform_request_without_scout_instruments(*args, &block)
42
+ end
43
+ end
44
+
45
+ alias_method :perform_request_without_scout_instruments, :perform_request
46
+ alias_method :perform_request, :perform_request_with_scout_instruments
47
+
48
+ def _sanitize_name(path)
49
+ name = path.split("/").last.gsub(/^_/, '')
50
+ allowed_names = ["bench",
51
+ "bulk",
52
+ "count",
53
+ "exists",
54
+ "explain",
55
+ "field_stats",
56
+ "health",
57
+ "mget",
58
+ "mlt",
59
+ "mpercolate",
60
+ "msearch",
61
+ "mtermvectors",
62
+ "percolate",
63
+ "query",
64
+ "scroll",
65
+ "search_shards",
66
+ "source",
67
+ "suggest",
68
+ "template",
69
+ "termvectors",
70
+ "update",
71
+ "search", ]
72
+
73
+ if allowed_names.include?(name)
74
+ name
75
+ else
76
+ "Unknown"
77
+ end
78
+ rescue
79
+ "Unknown"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ module OpenSearchTransportClientInstrumentationPrepend
88
+ def perform_request(*args, &block)
89
+ name = _sanitize_name(args[1])
90
+
91
+ self.class.instrument("OpenSearch", name, :ignore_children => true) do
92
+ super(*args, &block)
93
+ end
94
+ end
95
+
96
+ def _sanitize_name(path)
97
+ name = path.split("/").last.gsub(/^_/, '')
98
+ allowed_names = ["bench",
99
+ "bulk",
100
+ "count",
101
+ "exists",
102
+ "explain",
103
+ "field_stats",
104
+ "health",
105
+ "mget",
106
+ "mlt",
107
+ "mpercolate",
108
+ "msearch",
109
+ "mtermvectors",
110
+ "percolate",
111
+ "query",
112
+ "scroll",
113
+ "search_shards",
114
+ "source",
115
+ "suggest",
116
+ "template",
117
+ "termvectors",
118
+ "update",
119
+ "search", ]
120
+
121
+ if allowed_names.include?(name)
122
+ name
123
+ else
124
+ "Unknown"
125
+ end
126
+ rescue
127
+ "Unknown"
128
+ end
129
+ end
130
+ end
131
+ end
@@ -8,7 +8,8 @@ module ScoutApm
8
8
  # jobs matched explicitly by name
9
9
 
10
10
  # for now still support old config key ('ignore') for backwards compatibility
11
- @ignore_endpoints = config.value_present?('ignore') ? config.value('ignore') : config.value('ignore_endpoints')
11
+ raw_ignore = config.value_present?('ignore') ? config.value('ignore') : config.value('ignore_endpoints')
12
+ @ignore_endpoints = IgnoredUris.new(raw_ignore) unless raw_ignore.blank?
12
13
  @sample_endpoints = individual_sample_to_hash(config.value('sample_endpoints'))
13
14
  @endpoint_sample_rate = config.value('endpoint_sample_rate')
14
15
 
@@ -34,20 +35,20 @@ module ScoutApm
34
35
  if transaction.job?
35
36
  job_name = transaction.layer_finder.job.name
36
37
  rate = job_sample_rate(job_name)
37
- return sample?(rate) unless rate.nil?
38
+ return downsample?(rate) unless rate.nil?
38
39
  return true if ignore_job?(job_name)
39
- return sample?(@job_sample_rate) unless @job_sample_rate.nil?
40
+ return downsample?(@job_sample_rate) unless @job_sample_rate.nil?
40
41
  elsif transaction.web?
41
42
  uri = transaction.annotations[:uri]
42
43
  rate = web_sample_rate(uri)
43
- return sample?(rate) unless rate.nil?
44
+ return downsample?(rate) unless rate.nil?
44
45
  return true if ignore_uri?(uri)
45
- return sample?(@endpoint_sample_rate) unless @endpoint_sample_rate.nil?
46
+ return downsample?(@endpoint_sample_rate) unless @endpoint_sample_rate.nil?
46
47
  end
47
48
 
48
49
  # global sample check
49
50
  if @global_sample_rate
50
- return sample?(@global_sample_rate)
51
+ return downsample?(@global_sample_rate)
51
52
  end
52
53
 
53
54
  false # don't drop the request
@@ -59,17 +60,14 @@ module ScoutApm
59
60
  sample_hash = {}
60
61
  sampling_config.each do |sample|
61
62
  path, _, rate = sample.rpartition(':')
62
- sample_hash[path] = rate.to_i
63
+ sample_hash[path] = coerce_to_rate(rate)
63
64
  end
64
65
  sample_hash
65
66
  end
66
67
 
67
68
  def ignore_uri?(uri)
68
69
  return false if @ignore_endpoints.blank?
69
- @ignore_endpoints.each do |prefix|
70
- return true if uri.start_with?(prefix)
71
- end
72
- false
70
+ @ignore_endpoints.ignore?(uri)
73
71
  end
74
72
 
75
73
  def web_sample_rate(uri)
@@ -90,8 +88,9 @@ module ScoutApm
90
88
  @sample_jobs.fetch(job_name, nil)
91
89
  end
92
90
 
93
- def sample?(rate)
94
- rand * 100 > rate
91
+ def downsample?(rate)
92
+ # Should we drop this request based on the sample rate?
93
+ rand > rate
95
94
  end
96
95
 
97
96
  private
@@ -100,5 +99,18 @@ module ScoutApm
100
99
  ScoutApm::Agent.instance.logger
101
100
  end
102
101
 
102
+ def coerce_to_rate(val)
103
+ # Analogous to Config::SampleRateCoercion
104
+ v = val.to_f
105
+ # Anything above 1 is assumed a percentage for backwards compat, so convert to a decimal
106
+ if v > 1
107
+ v = v / 100
108
+ end
109
+ if v < 0 || v > 1
110
+ logger.warn("Sample rates must be between 0 and 1. You passed in #{val.inspect}, which we interpreted as #{v}. Clamping.")
111
+ v = v.clamp(0, 1)
112
+ end
113
+ v
114
+ end
103
115
  end
104
116
  end
@@ -53,11 +53,28 @@ module ScoutApm
53
53
  #
54
54
  def install
55
55
  old = ::Puma.cli_config.options.user_options[:before_worker_boot] || []
56
- new = Array(old) + [Proc.new do
57
- logger.info "Installing Puma worker loop."
58
- ScoutApm::Agent.instance.start_background_worker
59
- end]
60
56
 
57
+ hook =
58
+ if Gem::Version.new(::Puma::Const::PUMA_VERSION) < Gem::Version.new("7.0.0")
59
+ # Puma < 7 uses a raw block
60
+ Proc.new do
61
+ logger.info "Installing Puma worker loop."
62
+ ScoutApm::Agent.instance.start_background_worker
63
+ end
64
+ else
65
+ # Puma >= 7 uses the structured hook format:
66
+ # https://github.com/puma/puma/commit/b16790f7a3c1bfc1847225a58897c9fbd19981f8
67
+ {
68
+ block: Proc.new {
69
+ logger.info "Installing Puma worker loop."
70
+ ScoutApm::Agent.instance.start_background_worker
71
+ },
72
+ id: nil,
73
+ cluster_only: false
74
+ }
75
+ end
76
+
77
+ new = Array.new(old) + [hook]
61
78
  ::Puma.cli_config.options[:before_worker_boot] = new
62
79
  rescue
63
80
  logger.warn "Unable to install Puma worker loop: #{$!.message}"
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "5.8.0"
2
+ VERSION = "6.0.0"
3
3
  end
data/lib/scout_apm.rb CHANGED
@@ -84,6 +84,7 @@ require 'scout_apm/histogram'
84
84
  require 'scout_apm/instruments/net_http'
85
85
  require 'scout_apm/instruments/http_client'
86
86
  require 'scout_apm/instruments/http'
87
+ require 'scout_apm/instruments/httpx'
87
88
  require 'scout_apm/instruments/typhoeus'
88
89
  require 'scout_apm/instruments/moped'
89
90
  require 'scout_apm/instruments/mongoid'
@@ -92,6 +93,7 @@ require 'scout_apm/instruments/redis'
92
93
  require 'scout_apm/instruments/redis5'
93
94
  require 'scout_apm/instruments/influxdb'
94
95
  require 'scout_apm/instruments/elasticsearch'
96
+ require 'scout_apm/instruments/opensearch'
95
97
  require 'scout_apm/instruments/active_record'
96
98
  require 'scout_apm/instruments/action_controller_rails_2'
97
99
  require 'scout_apm/instruments/action_controller_rails_3_rails4'
@@ -225,11 +227,15 @@ if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR
225
227
  # Attempt to start right away, this will work best for preloading apps, Unicorn & Puma & similar
226
228
  ScoutApm::Agent.instance.install
227
229
 
228
- if ScoutApm::Agent.instance.context.config.value("auto_instruments")
229
- if defined?(Parser::TreeRewriter)
230
+ if ScoutApm::Agent.instance.context.config.value("auto_instruments") &&
231
+ ScoutApm::Agent.instance.should_load_instruments?
232
+ require 'scout_apm/auto_instrument/requirements'
233
+ if defined?(Prism) || defined?(Parser::TreeRewriter)
230
234
  ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled.")
231
235
  require 'scout_apm/auto_instrument'
232
- else # AutoInstruments is turned on, but we don't he the prerequisites to use it
236
+ else
237
+ # AutoInstruments is turned on, but we don't have the prerequisites to use it
238
+ # Prism should be available for Ruby >= 3.3.0
233
239
  ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled, but Parser::TreeRewriter is missing. Update 'parser' gem to >= 2.5.0.")
234
240
  end
235
241
  else