scout_apm 6.1.1 → 6.2.1

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: '08eb9e88e12ae5cee4b5ee2a2356f595f190591692d3a174cf91279f58fcc1a8'
4
- data.tar.gz: 9c6cccd60e11ec456d315b9cff7e1f45f5779f626a53f175592f5c9f9ed30299
3
+ metadata.gz: c667c4b7279ecdc5107a30bb50ac2a42eda6403a257f6bf2a05a6452c1143f18
4
+ data.tar.gz: d5183ae2f3fb7fbdfabae56616543f99503f59bb65b154e6709ea278f42166d3
5
5
  SHA512:
6
- metadata.gz: d74d8fbdc79fc43ae00d08dd11060959792cb64dac50d82ef92e5e9f0760a69fcedfe44a45f5b35eabbbf01f8a86c0730e5b3296152a9f6869cbb1051a20e615
7
- data.tar.gz: 0ef1636e6523df96f822d9874f3b4b8597e14405722d8851ff1682e34653edeb5aa5fe993ea2d3502d30d912a132c6124790eace4e6226dd34f724cf4a8f22ba
6
+ metadata.gz: bdc045802aa3d9cc37c147f6fd5a866e4bc3a3a49de480711595b3ca5b6dd04c314c2085bd229ccaebc0dca3125a7463f2bf0a53552adbb2caef8b0a09102de2
7
+ data.tar.gz: 07a522e00f9d86d4d03a57c5e8965a9fd724104881429c8acddf603c0a0ad8829d098f998b3e551a6a0751ab1ba60af0f2c077bab1c3528d988138d2f4c2268e
data/CHANGELOG.markdown CHANGED
@@ -1,5 +1,16 @@
1
1
  # Pending
2
2
 
3
+ # 6.2.1
4
+
5
+ - Fix `SystemStackError` with `grape >= 3.3.0` by falling back to `prepend` instrumentation when Grape owns `Endpoint#run` itself (#624)
6
+ - Fix a GraphQL layer leak where an unhandled exception skipping `end_execute_field` could leave stale layers on the request (#626)
7
+ - Honor `SCOUT_LOG_LEVEL` during agent bootstrap instead of always logging at the default level (#628)
8
+ - Use `HEROKU_BUILD_COMMIT` for Heroku git revision detection, since `HEROKU_SLUG_COMMIT` is deprecated (#625)
9
+
10
+ # 6.2.0
11
+
12
+ - Fix compatibility with `http >= 6.0.0` (#613)
13
+
3
14
  # 6.1.1
4
15
 
5
16
  - Fix Redis 5 prepend interaction (#611)
@@ -10,3 +10,4 @@ gem 'moped'
10
10
  gem 'actionpack'
11
11
  gem 'actionview'
12
12
  gem 'httpx'
13
+ gem 'grape'
@@ -7,3 +7,4 @@ gem 'moped'
7
7
  gem 'actionpack'
8
8
  gem 'actionview'
9
9
  gem 'httpx'
10
+ gem 'grape'
@@ -259,7 +259,7 @@ module ScoutApm
259
259
  end
260
260
 
261
261
  def self.build_minimal_logger
262
- ScoutApm::Logger.new(nil, :stdout => true)
262
+ ScoutApm::Logger.new(nil, :stdout => true, :log_level => ENV["SCOUT_LOG_LEVEL"])
263
263
  end
264
264
  end
265
265
  end
@@ -26,7 +26,9 @@ module ScoutApm
26
26
  end
27
27
 
28
28
  def detect_from_heroku
29
- ENV['HEROKU_SLUG_COMMIT']
29
+ # HEROKU_SLUG_COMMIT is deprecated in favor of HEROKU_BUILD_COMMIT.
30
+ # https://devcenter.heroku.com/articles/dyno-metadata#usage
31
+ ENV['HEROKU_BUILD_COMMIT'] || ENV['HEROKU_SLUG_COMMIT']
30
32
  end
31
33
 
32
34
  # Config will locate the value from:
@@ -41,6 +41,7 @@ module ScoutApm
41
41
  install_instrument(ScoutApm::Instruments::Elasticsearch)
42
42
  install_instrument(ScoutApm::Instruments::OpenSearch)
43
43
  install_instrument(ScoutApm::Instruments::Grape)
44
+ install_instrument(ScoutApm::Instruments::GraphQL)
44
45
  rescue
45
46
  logger.warn "Exception loading instruments:"
46
47
  logger.warn $!.message
@@ -20,16 +20,35 @@ module ScoutApm
20
20
  if defined?(::Grape) && defined?(::Grape::Endpoint)
21
21
  @installed = true
22
22
 
23
- logger.info "Instrumenting Grape::Endpoint"
23
+ # Grape >= 3.3.0 prepends a module of its own (Grape::Testing::RunBeforeEach)
24
+ # in front of Grape::Endpoint#run. An alias-method chain built on a method
25
+ # owned by a prepended module recurses infinitely (SystemStackError), because
26
+ # the aliased copy's `super` resolves back into our wrapper. Whenever #run is
27
+ # not owned by Grape::Endpoint itself, prepend is the only safe option.
28
+ prepend = true unless endpoint_owns_run?
24
29
 
25
- ::Grape::Endpoint.class_eval do
26
- include ScoutApm::Instruments::GrapeEndpointInstruments
30
+ logger.info "Instrumenting Grape::Endpoint. Prepend: #{prepend}"
27
31
 
28
- alias run_without_scout_instruments run
29
- alias run run_with_scout_instruments
32
+ if prepend
33
+ ::Grape::Endpoint.send(:prepend, GrapeEndpointInstrumentsPrepend)
34
+ else
35
+ ::Grape::Endpoint.class_eval do
36
+ include ScoutApm::Instruments::GrapeEndpointInstruments
37
+
38
+ alias run_without_scout_instruments run
39
+ alias run run_with_scout_instruments
40
+ end
30
41
  end
31
42
  end
32
43
  end
44
+
45
+ private
46
+
47
+ def endpoint_owns_run?
48
+ ::Grape::Endpoint.instance_method(:run).owner == ::Grape::Endpoint
49
+ rescue NameError
50
+ true
51
+ end
33
52
  end
34
53
 
35
54
  module GrapeEndpointInstruments
@@ -68,6 +87,42 @@ module ScoutApm
68
87
  end
69
88
  end
70
89
  end
90
+
91
+ module GrapeEndpointInstrumentsPrepend
92
+ def run(*args)
93
+ request = ::Grape::Request.new(env || args.first)
94
+ req = ScoutApm::RequestManager.lookup
95
+
96
+ path = ScoutApm::Agent.instance.context.config.value("uri_reporting") == 'path' ? request.path : request.fullpath
97
+ req.annotate_request(:uri => path)
98
+
99
+ # IP Spoofing Protection can throw an exception, just move on w/o remote ip
100
+ req.context.add_user(:ip => request.ip) rescue nil
101
+
102
+ req.set_headers(request.headers)
103
+
104
+ begin
105
+ name = ["Grape",
106
+ self.options[:method].first,
107
+ self.options[:for].to_s,
108
+ self.namespace.sub(%r{\A/}, ''), # removing leading slashes
109
+ self.options[:path].first,
110
+ ].compact.map{ |n| n.to_s }.join("/")
111
+ rescue => e
112
+ ScoutApm::Agent.instance.context.logger.info("Error getting Grape Endpoint Name. Error: #{e.message}. Options: #{self.options.inspect}")
113
+ name = "Grape/Unknown"
114
+ end
115
+
116
+ req.start_layer( ScoutApm::Layer.new("Controller", name) )
117
+ begin
118
+ super(*args)
119
+ rescue
120
+ req.error!
121
+ raise
122
+ ensure
123
+ req.stop_layer
124
+ end
125
+ end
126
+ end
71
127
  end
72
128
  end
73
-
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScoutApm
4
+ module Instruments
5
+ class GraphQL
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: false)
22
+ return unless defined?(::GraphQL::Tracing::ScoutTrace)
23
+
24
+ @installed = true
25
+ logger.info "Instrumenting GraphQL::Tracing::ScoutTrace"
26
+ ::GraphQL::Tracing::ScoutTrace.prepend(ScoutApm::Instruments::GraphQLExecuteFieldGuard)
27
+ end
28
+ end
29
+
30
+ # Prepended onto GraphQL::Tracing::ScoutTrace to guarantee that every
31
+ # Scout layer opened during a GraphQL multiplex is closed before control
32
+ # returns to the Scout Rack middleware.
33
+ #
34
+ # graphql-ruby's interpreter calls begin_execute_field / end_execute_field
35
+ # around each field resolver. When an unhandled exception causes
36
+ # end_execute_field to be skipped (runtime.rb lines 465-479), the
37
+ # corresponding ScoutApm::Layer is never popped from TrackedRequest#@layers.
38
+ # finalized? never returns true, record! is never called, and
39
+ # Thread.current[:scout_request] accumulates every subsequent request's
40
+ # layers without bound.
41
+ #
42
+ # This guard snapshots the open layer count before executing the multiplex
43
+ # and pops any excess layers in an ensure block, independent of whichever
44
+ # internal trace hooks graphql-ruby happens to use. It has no dependency on
45
+ # MonitorTrace's field-tracing condition or event slot architecture, so it
46
+ # remains correct even if those internals change.
47
+ module GraphQLExecuteFieldGuard
48
+ def execute_multiplex(multiplex:)
49
+ req = ScoutApm::RequestManager.lookup
50
+ layers_before = req.layer_count
51
+ super
52
+ ensure
53
+ extra = req.layer_count - layers_before
54
+ extra.times { req.stop_layer } if extra > 0
55
+ end
56
+ end
57
+ end
58
+ end
@@ -24,30 +24,63 @@ module ScoutApm
24
24
 
25
25
  if prepend
26
26
  ::HTTP::Client.send(:include, ScoutApm::Tracer)
27
- ::HTTP::Client.send(:prepend, HTTPInstrumentationPrepend)
27
+ if http_version_at_least?("6.0.0")
28
+ ::HTTP::Client.send(:prepend, HTTPInstrumentationPrependV6)
29
+ else
30
+ ::HTTP::Client.send(:prepend, HTTPInstrumentationPrepend)
31
+ end
28
32
  else
29
- ::HTTP::Client.class_eval do
30
- include ScoutApm::Tracer
33
+ if http_version_at_least?("6.0.0")
34
+ ::HTTP::Client.class_eval do
35
+ include ScoutApm::Tracer
31
36
 
32
- def request_with_scout_instruments(verb, uri, opts = {})
33
- self.class.instrument("HTTP", verb, :ignore_children => true, :desc => request_scout_description(verb, uri)) do
34
- request_without_scout_instruments(verb, uri, opts)
37
+ def request_with_scout_instruments(verb, uri, **opts)
38
+ self.class.instrument("HTTP", verb, :ignore_children => true, :desc => request_scout_description(verb, uri)) do
39
+ request_without_scout_instruments(verb, uri, **opts)
40
+ end
41
+ end
42
+
43
+ def request_scout_description(verb, uri)
44
+ max_length = ScoutApm::Agent.instance.context.config.value('instrument_http_url_length')
45
+ (String(uri).split('?').first)[0..(max_length - 1)]
46
+ rescue
47
+ ""
35
48
  end
36
- end
37
49
 
38
- def request_scout_description(verb, uri)
39
- max_length = ScoutApm::Agent.instance.context.config.value('instrument_http_url_length')
40
- (String(uri).split('?').first)[0..(max_length - 1)]
41
- rescue
42
- ""
50
+ alias request_without_scout_instruments request
51
+ alias request request_with_scout_instruments
43
52
  end
53
+ else
54
+ ::HTTP::Client.class_eval do
55
+ include ScoutApm::Tracer
56
+
57
+ def request_with_scout_instruments(verb, uri, opts = {})
58
+ self.class.instrument("HTTP", verb, :ignore_children => true, :desc => request_scout_description(verb, uri)) do
59
+ request_without_scout_instruments(verb, uri, opts)
60
+ end
61
+ end
44
62
 
45
- alias request_without_scout_instruments request
46
- alias request request_with_scout_instruments
63
+ def request_scout_description(verb, uri)
64
+ max_length = ScoutApm::Agent.instance.context.config.value('instrument_http_url_length')
65
+ (String(uri).split('?').first)[0..(max_length - 1)]
66
+ rescue
67
+ ""
68
+ end
69
+
70
+ alias request_without_scout_instruments request
71
+ alias request request_with_scout_instruments
72
+ end
47
73
  end
48
74
  end
49
75
  end
50
76
  end
77
+
78
+ private
79
+
80
+ def http_version_at_least?(version)
81
+ defined?(::HTTP::VERSION) &&
82
+ Gem::Version.new(::HTTP::VERSION) >= Gem::Version.new(version)
83
+ end
51
84
  end
52
85
 
53
86
  module HTTPInstrumentationPrepend
@@ -64,5 +97,20 @@ module ScoutApm
64
97
  ""
65
98
  end
66
99
  end
100
+
101
+ module HTTPInstrumentationPrependV6
102
+ def request(verb, uri, **opts)
103
+ self.class.instrument("HTTP", verb, :ignore_children => true, :desc => request_scout_description(verb, uri)) do
104
+ super(verb, uri, **opts)
105
+ end
106
+ end
107
+
108
+ def request_scout_description(verb, uri)
109
+ max_length = ScoutApm::Agent.instance.context.config.value('instrument_http_url_length')
110
+ (String(uri).split('?').first)[0..(max_length - 1)]
111
+ rescue
112
+ ""
113
+ end
114
+ end
67
115
  end
68
116
  end
@@ -77,6 +77,10 @@ module ScoutApm
77
77
  ignore_request! if @recorder.nil?
78
78
  end
79
79
 
80
+ def layer_count
81
+ @layers.size
82
+ end
83
+
80
84
  def start_layer(layer)
81
85
  # If we're already stopping, don't do additional layers
82
86
  return if stopping?
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "6.1.1"
2
+ VERSION = "6.2.1"
3
3
  end
data/lib/scout_apm.rb CHANGED
@@ -102,6 +102,7 @@ require 'scout_apm/instruments/middleware_summary'
102
102
  require 'scout_apm/instruments/middleware_detailed' # Disabled by default, see the file for more details
103
103
  require 'scout_apm/instruments/rails_router'
104
104
  require 'scout_apm/instruments/grape'
105
+ require 'scout_apm/instruments/graphql'
105
106
  require 'scout_apm/instruments/sinatra'
106
107
  require 'allocations'
107
108
 
@@ -3,6 +3,15 @@ require "test_helper"
3
3
  require "scout_apm/agent_context"
4
4
 
5
5
  class AgentContextTest < Minitest::Test
6
+ def test_bootstrap_logger_uses_scout_log_level
7
+ original_log_level = ENV["SCOUT_LOG_LEVEL"]
8
+ ENV["SCOUT_LOG_LEVEL"] = "error"
9
+
10
+ assert_equal ::Logger::ERROR, ScoutApm::AgentContext.new.logger.log_level
11
+ ensure
12
+ ENV["SCOUT_LOG_LEVEL"] = original_log_level
13
+ end
14
+
6
15
  def test_has_error_service_ignored_exceptions
7
16
  context = ScoutApm::AgentContext.new
8
17
  assert ScoutApm::ErrorService::IgnoredExceptions, context.ignored_exceptions.class
@@ -26,4 +35,4 @@ class AgentContextTest < Minitest::Test
26
35
  context.slow_request_policy.add(TestPolicy.new)
27
36
  assert 5, context.slow_request_policy.policies
28
37
  end
29
- end
38
+ end
@@ -34,6 +34,19 @@ class GitRevisionTest < Minitest::Test
34
34
  assert_equal 'heroku_slug', revision.sha
35
35
  end
36
36
 
37
+ def test_sha_from_heroku_build_commit
38
+ ENV['HEROKU_BUILD_COMMIT'] = 'heroku_build'
39
+ revision = ScoutApm::GitRevision.new(ScoutApm::AgentContext.new)
40
+ assert_equal 'heroku_build', revision.sha
41
+ end
42
+
43
+ def test_sha_from_heroku_prefers_build_commit_over_deprecated_slug_commit
44
+ ENV['HEROKU_BUILD_COMMIT'] = 'heroku_build'
45
+ ENV['HEROKU_SLUG_COMMIT'] = 'heroku_slug'
46
+ revision = ScoutApm::GitRevision.new(ScoutApm::AgentContext.new)
47
+ assert_equal 'heroku_build', revision.sha
48
+ end
49
+
37
50
  def test_sha_from_capistrano
38
51
  Dir.mktmpdir do |dir|
39
52
  context = context_with_file_in_root(File.join(dir, 'REVISION'), 'capistrano_sha')
@@ -0,0 +1,83 @@
1
+ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
2
+
3
+ require 'test_helper'
4
+
5
+ require 'grape'
6
+ require 'scout_apm/instruments/grape'
7
+
8
+ class GrapeTest < Minitest::Test
9
+ # Captured before the instrument is installed. On grape >= 3.3.0 #run is
10
+ # owned by the prepended Grape::Testing::RunBeforeEach module, not the
11
+ # Grape::Endpoint class itself.
12
+ GRAPE_ENDPOINT_OWNS_RUN = ::Grape::Endpoint.instance_method(:run).owner == ::Grape::Endpoint
13
+
14
+ class HelloAPI < ::Grape::API
15
+ format :json
16
+
17
+ get :hello do
18
+ { message: 'hello' }
19
+ end
20
+ end
21
+
22
+ # Install exactly once for the entire test run. Re-running an
23
+ # alias-method install would alias the wrapper onto itself.
24
+ def self.install_instruments
25
+ @install_instruments ||= begin
26
+ context = ScoutApm::AgentContext.new
27
+ instrument_manager = ScoutApm::InstrumentManager.new(context)
28
+ instance = ScoutApm::Instruments::Grape.new(context)
29
+ instance.install(prepend: instrument_manager.prepend_for_instrument?(instance.class))
30
+ instance
31
+ end
32
+ end
33
+
34
+ def setup
35
+ @instance = self.class.install_instruments
36
+ end
37
+
38
+ def test_installed
39
+ assert @instance.installed?
40
+ end
41
+
42
+ # Grape >= 3.3.0 prepends Grape::Testing::RunBeforeEach in front of
43
+ # Grape::Endpoint#run. Building an alias-method chain on top of the
44
+ # prepended method caused infinite recursion (SystemStackError) as soon
45
+ # as any endpoint was called.
46
+ def test_endpoint_runs_without_infinite_recursion
47
+ status, _headers, body = HelloAPI.call(Rack::MockRequest.env_for('/hello'))
48
+
49
+ assert_equal 200, status
50
+
51
+ body_content = "".dup
52
+ body.each { |chunk| body_content << chunk }
53
+ assert_includes body_content, 'hello'
54
+ end
55
+
56
+ # Either instrumentation method is valid: alias_method is the default,
57
+ # prepend is used when configured (`use_prepend`/`prepend_instruments`)
58
+ # or when the instrument falls back to it for safety.
59
+ def test_installs_using_prepend_or_alias_method
60
+ assert prepended? || aliased?, "expected Grape::Endpoint to be instrumented via prepend or alias_method"
61
+
62
+ unless GRAPE_ENDPOINT_OWNS_RUN
63
+ # On grape >= 3.3.0 #run is owned by a module prepended onto
64
+ # Grape::Endpoint, where an alias-method chain would recurse, so the
65
+ # instrument must have used prepend even though alias_method is the
66
+ # default.
67
+ assert prepended?, "grape >= 3.3.0 requires the prepend instrumentation method"
68
+ refute aliased?, "the alias-method chain must not be installed over a prepended #run"
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def prepended?
75
+ ::Grape::Endpoint.ancestors.include?(ScoutApm::Instruments::GrapeEndpointInstrumentsPrepend)
76
+ end
77
+
78
+ def aliased?
79
+ ::Grape::Endpoint.method_defined?(:run_without_scout_instruments) ||
80
+ ::Grape::Endpoint.private_method_defined?(:run_without_scout_instruments)
81
+ end
82
+ end
83
+ end
@@ -15,9 +15,14 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
15
15
 
16
16
  def test_installs_using_proper_method
17
17
  if @instrument_manager.prepend_for_instrument?(@instance.class) == true
18
- assert ::HTTP::Client.ancestors.include?(ScoutApm::Instruments::HTTPInstrumentationPrepend)
18
+ if Gem::Version.new(::HTTP::VERSION) >= Gem::Version.new("6.0.0")
19
+ assert ::HTTP::Client.ancestors.include?(ScoutApm::Instruments::HTTPInstrumentationPrependV6)
20
+ else
21
+ assert ::HTTP::Client.ancestors.include?(ScoutApm::Instruments::HTTPInstrumentationPrepend)
22
+ end
19
23
  else
20
24
  assert_equal false, ::HTTP::Client.ancestors.include?(ScoutApm::Instruments::HTTPInstrumentationPrepend)
25
+ assert_equal false, ::HTTP::Client.ancestors.include?(ScoutApm::Instruments::HTTPInstrumentationPrependV6)
21
26
  end
22
27
  end
23
28
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.1
4
+ version: 6.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -314,6 +314,7 @@ files:
314
314
  - lib/scout_apm/instruments/active_record.rb
315
315
  - lib/scout_apm/instruments/elasticsearch.rb
316
316
  - lib/scout_apm/instruments/grape.rb
317
+ - lib/scout_apm/instruments/graphql.rb
317
318
  - lib/scout_apm/instruments/http.rb
318
319
  - lib/scout_apm/instruments/http_client.rb
319
320
  - lib/scout_apm/instruments/httpx.rb
@@ -464,6 +465,7 @@ files:
464
465
  - test/unit/instruments/fixtures/test/_test_partial.html.erb
465
466
  - test/unit/instruments/fixtures/test/_test_partial_collection.html.erb
466
467
  - test/unit/instruments/fixtures/test_view.html.erb
468
+ - test/unit/instruments/grape_test.rb
467
469
  - test/unit/instruments/http_client_test.rb
468
470
  - test/unit/instruments/http_test.rb
469
471
  - test/unit/instruments/httpx_test.rb
@@ -520,7 +522,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
520
522
  - !ruby/object:Gem::Version
521
523
  version: '0'
522
524
  requirements: []
523
- rubygems_version: 4.0.3
525
+ rubygems_version: 4.0.16
524
526
  specification_version: 4
525
527
  summary: Ruby application performance monitoring
526
528
  test_files: []