scout_apm 6.3.0 → 6.3.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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.markdown +4 -0
- data/gems/instruments-ruby4.gemfile +1 -0
- data/gems/instruments.gemfile +5 -0
- data/gems/typhoeus.gemfile +1 -0
- data/lib/scout_apm/instruments/grape.rb +32 -22
- data/lib/scout_apm/version.rb +1 -1
- data/test/unit/instruments/grape_test.rb +25 -0
- data/test/unit/instruments/httpx_test.rb +36 -5
- data/test/unit/instruments/typhoeus_test.rb +17 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97692f03252aa5c2af4a4f87092742319b9088dace2f3dbcb4c48af766dfb848
|
|
4
|
+
data.tar.gz: 30bc39d151abec5d26723b2de0626319cf80685548a0f68da48074e283c72515
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c6a06a5e08a3a753a7110568f44d9c9c6b0163aeeabf3be281a2de9df6a2357f14515de7419c9dedd79e8f7e8266dca67cbf58b3dbac2e6efb54f283ba4d433
|
|
7
|
+
data.tar.gz: 13bacea25583bf396a3e4d0edee2e957a756296a3c4fcf6083a5a42b5e0bb2e57c90a9751dd6b04f7d86f0315d8c30222ba4d3d0c9c679aa2d73eb1eb38b2eb3
|
data/.gitignore
CHANGED
data/CHANGELOG.markdown
CHANGED
data/gems/instruments.gemfile
CHANGED
|
@@ -7,4 +7,9 @@ gem 'moped'
|
|
|
7
7
|
gem 'actionpack'
|
|
8
8
|
gem 'actionview'
|
|
9
9
|
gem 'httpx'
|
|
10
|
+
gem 'webmock'
|
|
10
11
|
gem 'grape'
|
|
12
|
+
# json 3.0 (released 2026-09) dropped the `quirks_mode` keyword that Grape 3's
|
|
13
|
+
# ActiveSupport JSON formatter still passes, so any Grape 3 JSON request raises
|
|
14
|
+
# ArgumentError. Pin to the 2.x line until this matrix moves off Grape 3.
|
|
15
|
+
gem 'json', '< 3'
|
data/gems/typhoeus.gemfile
CHANGED
|
@@ -51,6 +51,36 @@ module ScoutApm
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
module GrapeEndpointNaming
|
|
55
|
+
# Grape >= 4 moved endpoint route metadata off the public `options`
|
|
56
|
+
# Hash and onto `Endpoint#config` (a Grape::Endpoint::Options Data
|
|
57
|
+
# object). Support both so the instrument works across Grape 3 and 4.
|
|
58
|
+
def self.name_for(endpoint)
|
|
59
|
+
# `config` is a protected reader on Grape::Endpoint, so a public
|
|
60
|
+
# respond_to? misses it: check with `true` and read via `send`.
|
|
61
|
+
if endpoint.respond_to?(:config, true) && endpoint.send(:config).respond_to?(:http_methods)
|
|
62
|
+
config = endpoint.send(:config)
|
|
63
|
+
method = config.http_methods && config.http_methods.first
|
|
64
|
+
api = config.api || (endpoint.respond_to?(:api) ? endpoint.api : nil)
|
|
65
|
+
path = config.path && config.path.first
|
|
66
|
+
else
|
|
67
|
+
method = endpoint.options[:method] && endpoint.options[:method].first
|
|
68
|
+
api = endpoint.options[:for]
|
|
69
|
+
path = endpoint.options[:path] && endpoint.options[:path].first
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
["Grape",
|
|
73
|
+
method,
|
|
74
|
+
api.to_s,
|
|
75
|
+
endpoint.namespace.sub(%r{\A/}, ''), # removing leading slashes
|
|
76
|
+
path,
|
|
77
|
+
].compact.map { |n| n.to_s }.join("/")
|
|
78
|
+
rescue => e
|
|
79
|
+
ScoutApm::Agent.instance.context.logger.info("Error getting Grape Endpoint Name. Error: #{e.message}. Options: #{endpoint.options.inspect}")
|
|
80
|
+
"Grape/Unknown"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
54
84
|
module GrapeEndpointInstruments
|
|
55
85
|
def run_with_scout_instruments(*args)
|
|
56
86
|
request = ::Grape::Request.new(env || args.first)
|
|
@@ -64,17 +94,7 @@ module ScoutApm
|
|
|
64
94
|
|
|
65
95
|
req.set_headers(request.headers)
|
|
66
96
|
|
|
67
|
-
|
|
68
|
-
name = ["Grape",
|
|
69
|
-
self.options[:method].first,
|
|
70
|
-
self.options[:for].to_s,
|
|
71
|
-
self.namespace.sub(%r{\A/}, ''), # removing leading slashes
|
|
72
|
-
self.options[:path].first,
|
|
73
|
-
].compact.map{ |n| n.to_s }.join("/")
|
|
74
|
-
rescue => e
|
|
75
|
-
ScoutApm::Agent.instance.context.logger.info("Error getting Grape Endpoint Name. Error: #{e.message}. Options: #{self.options.inspect}")
|
|
76
|
-
name = "Grape/Unknown"
|
|
77
|
-
end
|
|
97
|
+
name = GrapeEndpointNaming.name_for(self)
|
|
78
98
|
|
|
79
99
|
req.start_layer( ScoutApm::Layer.new("Controller", name) )
|
|
80
100
|
begin
|
|
@@ -101,17 +121,7 @@ module ScoutApm
|
|
|
101
121
|
|
|
102
122
|
req.set_headers(request.headers)
|
|
103
123
|
|
|
104
|
-
|
|
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
|
|
124
|
+
name = GrapeEndpointNaming.name_for(self)
|
|
115
125
|
|
|
116
126
|
req.start_layer( ScoutApm::Layer.new("Controller", name) )
|
|
117
127
|
begin
|
data/lib/scout_apm/version.rb
CHANGED
|
@@ -53,6 +53,31 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
|
|
|
53
53
|
assert_includes body_content, 'hello'
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Grape >= 4 moved endpoint route metadata (method/path/api) off the
|
|
57
|
+
# public `options` Hash and onto the protected `Endpoint#config` object.
|
|
58
|
+
# Reading the old `options[...]` keys under Grape 4 silently returns nil,
|
|
59
|
+
# so every request gets named "Grape/Unknown" instead of the real
|
|
60
|
+
# endpoint name.
|
|
61
|
+
def test_endpoint_name_reflects_the_route
|
|
62
|
+
captured_names = []
|
|
63
|
+
original_new = ScoutApm::Layer.method(:new)
|
|
64
|
+
ScoutApm::Layer.define_singleton_method(:new) do |type, name, *rest|
|
|
65
|
+
captured_names << name if type == "Controller"
|
|
66
|
+
original_new.call(type, name, *rest)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
begin
|
|
70
|
+
status, _headers, _body = HelloAPI.call(Rack::MockRequest.env_for('/hello'))
|
|
71
|
+
ensure
|
|
72
|
+
ScoutApm::Layer.define_singleton_method(:new, original_new)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
assert_equal 200, status
|
|
76
|
+
refute_empty captured_names
|
|
77
|
+
refute_equal "Grape/Unknown", captured_names.first
|
|
78
|
+
assert_equal "Grape/GET/#{HelloAPI}//hello", captured_names.first
|
|
79
|
+
end
|
|
80
|
+
|
|
56
81
|
# Either instrumentation method is valid: alias_method is the default,
|
|
57
82
|
# prepend is used when configured (`use_prepend`/`prepend_instruments`)
|
|
58
83
|
# or when the instrument falls back to it for safety.
|
|
@@ -4,17 +4,34 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
|
|
|
4
4
|
require 'scout_apm/instruments/httpx'
|
|
5
5
|
|
|
6
6
|
require 'httpx'
|
|
7
|
+
require 'webmock'
|
|
7
8
|
|
|
8
9
|
class HTTPXTest < Minitest::Test
|
|
10
|
+
include WebMock::API
|
|
11
|
+
|
|
9
12
|
def setup
|
|
13
|
+
super # clears Thread.current[:scout_request] so a stale request from a
|
|
14
|
+
# prior test can't swallow this test's layers
|
|
15
|
+
WebMock.enable!
|
|
16
|
+
WebMock.disable_net_connect!
|
|
17
|
+
|
|
10
18
|
@context = ScoutApm::AgentContext.new
|
|
11
19
|
@recorder = FakeRecorder.new
|
|
12
20
|
ScoutApm::Agent.instance.context.recorder = @recorder
|
|
13
21
|
ScoutApm::Instruments::HTTPX.new(@context).install(prepend: false)
|
|
14
22
|
end
|
|
15
23
|
|
|
24
|
+
def teardown
|
|
25
|
+
WebMock.reset!
|
|
26
|
+
WebMock.allow_net_connect!
|
|
27
|
+
WebMock.disable!
|
|
28
|
+
super
|
|
29
|
+
end
|
|
16
30
|
|
|
17
31
|
def test_httpx
|
|
32
|
+
stub_request(:get, /news\.ycombinator\.com/).to_return(status: 200, body: "")
|
|
33
|
+
stub_request(:get, /google\.com/).to_return(status: 200, body: "")
|
|
34
|
+
|
|
18
35
|
responses = HTTPX.get(
|
|
19
36
|
"https://news.ycombinator.com/news",
|
|
20
37
|
"https://news.ycombinator.com/news?p=2",
|
|
@@ -27,11 +44,18 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
|
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
def test_httpx_post_request
|
|
47
|
+
stub_request(:post, /httpbin\.org\/post/).to_return(status: 200, body: "")
|
|
48
|
+
|
|
30
49
|
HTTPX.post("https://httpbin.org/post", json: { test: "data" })
|
|
31
50
|
assert_recorded(@recorder, "HTTP", "POST", "httpbin.org/post")
|
|
32
51
|
end
|
|
33
52
|
|
|
34
53
|
def test_instruments_httpx_error_handling
|
|
54
|
+
# An error response still flows through Session#request, so the layer is
|
|
55
|
+
# started and stopped, and the request is recorded exactly once.
|
|
56
|
+
stub_request(:get, /thisshouldnotexistatall12345\.com/)
|
|
57
|
+
.to_return(status: 500, body: "")
|
|
58
|
+
|
|
35
59
|
begin
|
|
36
60
|
HTTPX.get("https://thisshouldnotexistatall12345.com")
|
|
37
61
|
rescue
|
|
@@ -41,6 +65,10 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
|
|
|
41
65
|
end
|
|
42
66
|
|
|
43
67
|
def test_httpx_request_retry
|
|
68
|
+
# A timed-out request is still dispatched through Session#request, so the
|
|
69
|
+
# layer is recorded even though the response errors.
|
|
70
|
+
stub_request(:get, /httpbin\.org\/delay/).to_timeout
|
|
71
|
+
|
|
44
72
|
begin
|
|
45
73
|
HTTPX.with(timeout: { connect_timeout: 0.25, request_timeout: 0.25 })
|
|
46
74
|
.get("https://httpbin.org/delay/5")
|
|
@@ -50,14 +78,17 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
|
|
|
50
78
|
end
|
|
51
79
|
|
|
52
80
|
def test_multiple_plugins
|
|
81
|
+
stub_request(:get, /news\.ycombinator\.com/).to_return(status: 200, body: "")
|
|
82
|
+
stub_request(:get, "http://httpbin.org/redirect/2")
|
|
83
|
+
.to_return(status: 302, headers: { "Location" => "http://httpbin.org/redirect/1" })
|
|
84
|
+
stub_request(:get, "http://httpbin.org/redirect/1")
|
|
85
|
+
.to_return(status: 302, headers: { "Location" => "http://httpbin.org/get" })
|
|
86
|
+
stub_request(:get, "http://httpbin.org/get").to_return(status: 200, body: "")
|
|
87
|
+
|
|
53
88
|
session = HTTPX.plugin(:persistent).plugin(:follow_redirects)
|
|
54
89
|
|
|
55
90
|
session.get("https://news.ycombinator.com/news")
|
|
56
|
-
|
|
57
|
-
session.get("http://httpbin.org/redirect/2")
|
|
58
|
-
rescue
|
|
59
|
-
skip "httpbin.org not available"
|
|
60
|
-
end
|
|
91
|
+
session.get("http://httpbin.org/redirect/2")
|
|
61
92
|
|
|
62
93
|
assert_equal 2, @recorder.requests.length, "Expected 2 requests to be recorded"
|
|
63
94
|
end
|
|
@@ -4,15 +4,31 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("typhoeus")
|
|
|
4
4
|
require 'scout_apm/instruments/typhoeus'
|
|
5
5
|
|
|
6
6
|
require 'typhoeus'
|
|
7
|
+
require 'webmock'
|
|
7
8
|
|
|
8
9
|
class TyphoeusTest < Minitest::Test
|
|
10
|
+
include WebMock::API
|
|
11
|
+
|
|
9
12
|
def setup
|
|
13
|
+
super # clears Thread.current[:scout_request] so a stale request from a
|
|
14
|
+
# prior test can't swallow this test's layers
|
|
15
|
+
WebMock.enable!
|
|
16
|
+
WebMock.disable_net_connect!
|
|
17
|
+
stub_request(:any, /example\.com/).to_return(status: 200, body: "")
|
|
18
|
+
|
|
10
19
|
@context = ScoutApm::AgentContext.new
|
|
11
20
|
@recorder = FakeRecorder.new
|
|
12
21
|
ScoutApm::Agent.instance.context.recorder = @recorder
|
|
13
22
|
ScoutApm::Instruments::Typhoeus.new(@context).install(prepend: false)
|
|
14
23
|
end
|
|
15
24
|
|
|
25
|
+
def teardown
|
|
26
|
+
WebMock.reset!
|
|
27
|
+
WebMock.allow_net_connect!
|
|
28
|
+
WebMock.disable!
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
16
32
|
def test_instruments_typhoeus_hydra
|
|
17
33
|
hydra = Typhoeus::Hydra.new
|
|
18
34
|
2.times.map{ hydra.queue(Typhoeus::Request.new("example.com", followlocation: true)) }
|
|
@@ -39,4 +55,4 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("typhoeus")
|
|
|
39
55
|
assert_equal desc, req.root_layer.desc if !desc.nil?
|
|
40
56
|
end
|
|
41
57
|
end
|
|
42
|
-
end
|
|
58
|
+
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.3.
|
|
4
|
+
version: 6.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Derek Haynes
|
|
@@ -523,7 +523,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
523
523
|
- !ruby/object:Gem::Version
|
|
524
524
|
version: '0'
|
|
525
525
|
requirements: []
|
|
526
|
-
rubygems_version: 4.0.
|
|
526
|
+
rubygems_version: 4.0.20
|
|
527
527
|
specification_version: 4
|
|
528
528
|
summary: Ruby application performance monitoring
|
|
529
529
|
test_files: []
|