scout_apm 6.2.0 → 6.3.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: da81dd5963b5868fe7ab83e62c6bf6609679bb48de5935c2fd87c5695f5b3469
4
- data.tar.gz: 8485eba7342b830f01b4e9345d116de1c83f373f7b228bbee0c2eac346931043
3
+ metadata.gz: fe18e4045997fdf6b731025ee98c13e2702096a0cfd13ac5f0f651d7a7f4a4db
4
+ data.tar.gz: d9b4593ef2b8d5df4983d7b93cb9eaf5cc3f7e6916d8288ae9814e59dd913007
5
5
  SHA512:
6
- metadata.gz: f44f9ac827d187f2c775c20ae04031024913ba27f1b621a4e2e882308779502e29e9648be8da2d283a75dfd30d27ec7299c1dee7d49d0e78d4150ad79ab40d44
7
- data.tar.gz: 1a2a82c2c17b935a048c6d3f200da1b161e8cdba3b37bc7860ecaf978cfe946f81df624ba0febfbd4e1d3a57b61a22c5a4385b4ee8178f422dcfa2857ace7ad9
6
+ metadata.gz: a5b838d8e12286496038899c7dd006b6c3d6507061bcba1a963fc10664fa96e36c603f71e315430eda9eeaa84dabadb21f91dee9e9aa9baf7009f6e03d8f3d9d
7
+ data.tar.gz: bee9ed547611139d56eb6f88cf6c1e730dc77dbdf575e89a8b1cd95f86756687174c78b64cf00acd389a74e90198f9750aec334e2dfba195340a0a9753c280bd
data/CHANGELOG.markdown CHANGED
@@ -1,5 +1,16 @@
1
1
  # Pending
2
2
 
3
+ # 6.3.0
4
+
5
+ - Fix span leak on `ActionController::Live` streaming requests (#631)
6
+
7
+ # 6.2.1
8
+
9
+ - Fix `SystemStackError` with `grape >= 3.3.0` by falling back to `prepend` instrumentation when Grape owns `Endpoint#run` itself (#624)
10
+ - Fix a GraphQL layer leak where an unhandled exception skipping `end_execute_field` could leave stale layers on the request (#626)
11
+ - Honor `SCOUT_LOG_LEVEL` during agent bootstrap instead of always logging at the default level (#628)
12
+ - Use `HEROKU_BUILD_COMMIT` for Heroku git revision detection, since `HEROKU_SLUG_COMMIT` is deprecated (#625)
13
+
3
14
  # 6.2.0
4
15
 
5
16
  - Fix compatibility with `http >= 6.0.0` (#613)
@@ -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
@@ -7,11 +7,23 @@ module ScoutApm
7
7
  find || create
8
8
  end
9
9
 
10
- # Get the current Thread local, and detecting, and not returning a stale request
10
+ # Get the current Thread local, detecting and not returning a stale request,
11
+ # nor a request that belongs to a different thread.
11
12
  def self.find
12
13
  req = Thread.current[:scout_request]
14
+ return nil unless req
13
15
 
14
- if req && (req.stopping? || req.recorded?)
16
+ # ActionController::Live runs the controller action in a child thread and
17
+ # copies the parent thread's raw Thread.current locals (including
18
+ # :scout_request) into it. Without this check the parent (Rack) thread and
19
+ # the child (streaming) thread would share and concurrently mutate a single
20
+ # TrackedRequest -- racing on @layers/@stopping -- which corrupts the layer
21
+ # stack, drops the transaction, and leaks layers across requests. If the
22
+ # resident request was created on a different thread, treat it as absent so
23
+ # lookup creates a fresh one owned by this thread.
24
+ return nil if req.creating_thread_id != Thread.current.object_id
25
+
26
+ if req.stopping? || req.recorded?
15
27
  nil
16
28
  else
17
29
  req
@@ -74,9 +74,24 @@ module ScoutApm
74
74
  @recorder = agent_context.recorder
75
75
  @real_request = false
76
76
  @transaction_id = ScoutApm::Utils::TransactionId.new.to_s
77
+ # The thread this request was created on. ActionController::Live copies the
78
+ # raw Thread.current locals (including :scout_request) into its streaming
79
+ # child thread, which would otherwise cause two threads to share and race
80
+ # on one TrackedRequest. RequestManager uses this to detect an inherited
81
+ # request and give the child its own instead. See RequestManager.find.
82
+ @creating_thread_id = Thread.current.object_id
77
83
  ignore_request! if @recorder.nil?
78
84
  end
79
85
 
86
+ # The object_id of the Thread that created this request. Used to detect a
87
+ # TrackedRequest that was inherited by a different thread (e.g. an
88
+ # ActionController::Live streaming thread) rather than created for it.
89
+ attr_reader :creating_thread_id
90
+
91
+ def layer_count
92
+ @layers.size
93
+ end
94
+
80
95
  def start_layer(layer)
81
96
  # If we're already stopping, don't do additional layers
82
97
  return if stopping?
@@ -364,14 +379,21 @@ module ScoutApm
364
379
  end
365
380
  end
366
381
 
367
- # This request is a job transaction iff it has a 'Job' layer
368
- # Use this only during recording
382
+ # True iff this request has a 'Job' layer. Only reliable during recording --
383
+ # see #web? for why.
369
384
  def job?
370
385
  layer_finder.job != nil
371
386
  end
372
387
 
373
- # This request is a web transaction iff it has a 'Controller' layer
374
- # Use this only during recording
388
+ # True iff this request has a 'Controller' layer.
389
+ #
390
+ # Only reliable during recording; returns false mid-request even for a real
391
+ # web request. #layer_finder walks root_layer's child *tree*, but layers are
392
+ # attached to that tree only in #stop_layer (via add_child), not in
393
+ # #start_layer. Controller/Job are top-level spans, so they stop last --
394
+ # meaning web?/job? only resolve as the request unwinds into recording.
395
+ # (Negative results aren't memoized, so an early false doesn't poison a
396
+ # later check.)
375
397
  def web?
376
398
  layer_finder.controller != nil
377
399
  end
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "6.2.0"
2
+ VERSION = "6.3.0"
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
@@ -0,0 +1,70 @@
1
+ require 'test_helper'
2
+
3
+ # Tests for RequestManager's thread-local handling, in particular that a
4
+ # TrackedRequest created on one thread is NOT shared with another thread.
5
+ #
6
+ # ActionController::Live runs the controller action in a child thread and copies
7
+ # the parent (Rack) thread's raw Thread.current locals -- including
8
+ # :scout_request -- into it. Without protection both threads would share and
9
+ # concurrently mutate a single TrackedRequest, racing on @layers/@stopping,
10
+ # which drops the transaction and leaks layers. RequestManager must give the
11
+ # inheriting thread its own request instead.
12
+ class RequestManagerTest < Minitest::Test
13
+ def teardown
14
+ # Never let a test leak the thread-local into sibling tests.
15
+ Thread.current[:scout_request] = nil
16
+ end
17
+
18
+ def test_lookup_reuses_the_request_on_the_same_thread
19
+ first = ScoutApm::RequestManager.lookup
20
+ second = ScoutApm::RequestManager.lookup
21
+
22
+ assert_same first, second,
23
+ "lookup should return the same TrackedRequest on repeated calls within one thread"
24
+ end
25
+
26
+ def test_lookup_in_a_child_thread_that_inherited_the_local_gets_its_own_request
27
+ parent_request = ScoutApm::RequestManager.lookup
28
+ inherited = Thread.current[:scout_request]
29
+
30
+ child_request = nil
31
+ child_local_before = nil
32
+
33
+ t = Thread.new do
34
+ # Simulate ActionController::Live copying the parent's raw thread local
35
+ # into the streaming child thread.
36
+ Thread.current[:scout_request] = inherited
37
+ child_local_before = Thread.current[:scout_request]
38
+
39
+ child_request = ScoutApm::RequestManager.lookup
40
+ end
41
+ t.join
42
+
43
+ # The child observed the inherited (parent's) request as its raw local...
44
+ assert_same parent_request, child_local_before
45
+
46
+ # ...but lookup must NOT hand it the parent's request to mutate.
47
+ refute_nil child_request
48
+ refute_same parent_request, child_request,
49
+ "a thread that inherited another thread's :scout_request must get its own TrackedRequest"
50
+
51
+ # And the parent's request must be left untouched (still the parent's).
52
+ assert_same parent_request, Thread.current[:scout_request]
53
+ end
54
+
55
+ def test_find_treats_a_foreign_thread_request_as_absent
56
+ # A request that reports it was created on a different thread should not be
57
+ # returned by find (so lookup falls through to create).
58
+ foreign = ScoutApm::TrackedRequest.new(ScoutApm::Agent.instance.context, ScoutApm::FakeStore.new)
59
+ foreign.stubs(:creating_thread_id).returns(Thread.current.object_id + 1)
60
+ Thread.current[:scout_request] = foreign
61
+
62
+ assert_nil ScoutApm::RequestManager.find,
63
+ "find should treat a request created on another thread as absent"
64
+ end
65
+
66
+ def test_find_returns_a_same_thread_request
67
+ own = ScoutApm::RequestManager.lookup
68
+ assert_same own, ScoutApm::RequestManager.find
69
+ end
70
+ 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.2.0
4
+ version: 6.3.0
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
@@ -484,6 +486,7 @@ files:
484
486
  - test/unit/remote/route_test.rb
485
487
  - test/unit/remote/server_test.rb
486
488
  - test/unit/request_histograms_test.rb
489
+ - test/unit/request_manager_test.rb
487
490
  - test/unit/sampling_test.rb
488
491
  - test/unit/scored_item_set_test.rb
489
492
  - test/unit/serializers/payload_serializer_test.rb
@@ -520,7 +523,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
520
523
  - !ruby/object:Gem::Version
521
524
  version: '0'
522
525
  requirements: []
523
- rubygems_version: 4.0.6
526
+ rubygems_version: 4.0.16
524
527
  specification_version: 4
525
528
  summary: Ruby application performance monitoring
526
529
  test_files: []