scout_apm 4.1.0 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/CHANGELOG.markdown +33 -0
- data/LICENSE.md +21 -28
- data/gems/typhoeus.gemfile +3 -0
- data/lib/scout_apm/auto_instrument/instruction_sequence.rb +2 -2
- data/lib/scout_apm/config.rb +13 -3
- data/lib/scout_apm/exceptions.rb +12 -0
- data/lib/scout_apm/external_service_metric_set.rb +97 -0
- data/lib/scout_apm/external_service_metric_stats.rb +85 -0
- data/lib/scout_apm/fake_store.rb +3 -0
- data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +1 -1
- data/lib/scout_apm/git_revision.rb +9 -0
- data/lib/scout_apm/instant/middleware.rb +2 -0
- data/lib/scout_apm/instrument_manager.rb +1 -0
- data/lib/scout_apm/instruments/elasticsearch.rb +2 -0
- data/lib/scout_apm/instruments/sinatra.rb +2 -0
- data/lib/scout_apm/instruments/typhoeus.rb +1 -1
- data/lib/scout_apm/layer_converters/external_service_converter.rb +65 -0
- data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +2 -0
- data/lib/scout_apm/logger.rb +4 -0
- data/lib/scout_apm/reporting.rb +2 -1
- data/lib/scout_apm/serializers/external_service_serializer_to_json.rb +15 -0
- data/lib/scout_apm/serializers/payload_serializer.rb +4 -3
- data/lib/scout_apm/serializers/payload_serializer_to_json.rb +4 -1
- data/lib/scout_apm/store.rb +25 -1
- data/lib/scout_apm/tracked_request.rb +7 -1
- data/lib/scout_apm/utils/sql_sanitizer.rb +5 -0
- data/lib/scout_apm/version.rb +1 -1
- data/lib/scout_apm.rb +12 -2
- data/scout_apm.gemspec +1 -3
- data/test/unit/external_service_metric_set_test.rb +67 -0
- data/test/unit/external_service_metric_stats_test.rb +106 -0
- data/test/unit/instruments/typhoeus_test.rb +42 -0
- data/test/unit/serializers/payload_serializer_test.rb +3 -3
- data/test/unit/sql_sanitizer_test.rb +13 -0
- metadata +13 -5
- data/.travis.yml +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a569967a27143fe36a506e8757eeda10f2de6cf85567cb2af8c08993b558abb
|
4
|
+
data.tar.gz: 49a5277948f6dab053268af7f912f8905b78d2173c79cfb1c3e9a87288e33113
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 523767ea43b634748ff16ee974fd6bb659ddced673fbd9106e6f3a07ed8e9d78ec30b88094dac2aff4a374af719691249534aa471a7fc993813018d92bf71333
|
7
|
+
data.tar.gz: f67a78084a1280b497422f4dc35ea3ed5afb1cfbde55c79c4020b11e407674f803ea8d1dec8803bb6bf1fcc8a4e11ac728621cf2f45b8d2d35956497d55a78d7
|
data/.github/workflows/test.yml
CHANGED
@@ -26,6 +26,9 @@ jobs:
|
|
26
26
|
- ruby: 2.4
|
27
27
|
- ruby: 2.5
|
28
28
|
- ruby: 2.6
|
29
|
+
- ruby: 2.6
|
30
|
+
gemfile: gems/typhoeus.gemfile
|
31
|
+
test_features: "typhoeus"
|
29
32
|
- ruby: 2.6
|
30
33
|
gemfile: gems/octoshark.gemfile
|
31
34
|
- ruby: 2.6
|
@@ -36,6 +39,7 @@ jobs:
|
|
36
39
|
|
37
40
|
env:
|
38
41
|
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
42
|
+
SCOUT_TEST_FEATURES: ${{ matrix.test_features }}
|
39
43
|
|
40
44
|
runs-on: ubuntu-latest
|
41
45
|
|
data/CHANGELOG.markdown
CHANGED
@@ -1,3 +1,36 @@
|
|
1
|
+
# Unreleased
|
2
|
+
|
3
|
+
# 5.1.0
|
4
|
+
|
5
|
+
* Specify correct (MIT) license in Gemspec (#430)
|
6
|
+
* Install HTTP::Client instruments (#420)
|
7
|
+
* Sanitize FROM jsonb_as_recordset AS correctly in Postgres (#332)
|
8
|
+
* Call to_h on `ActiveRecord::Base.configurations` (#434)
|
9
|
+
* Allow loading of trusted `config/scout_apm.yml` via `YAML.unsafe_load` if available (#435)
|
10
|
+
* Better exception handling when loading config (#436)
|
11
|
+
* Check for nil other_metric_set in merge_external_service_metrics (#437)
|
12
|
+
* Log `warn` in InstructionSequence only if SCOUT_LOG_LEVEL is debug (#438)
|
13
|
+
* Check for Parser::TreeRewriter before loading AutoInstruments to avoid LoadError (#440)
|
14
|
+
* Fall back to STDERR upon exception in build_logger (#441)
|
15
|
+
|
16
|
+
|
17
|
+
# 5.0.0
|
18
|
+
|
19
|
+
* Add External Service metrics reporting (#403)
|
20
|
+
* Relicense to MIT (#429)
|
21
|
+
* Opt out of frozen string literals in select files (#427)
|
22
|
+
* Fall back when logger can't write to destination (#423)
|
23
|
+
* Avoid exception on race condition (#407)
|
24
|
+
* Add Mina deploy tracking support (#327)
|
25
|
+
|
26
|
+
# 4.1.2
|
27
|
+
|
28
|
+
* Add record_queue_time configuration (PR #422)
|
29
|
+
|
30
|
+
# 4.1.1
|
31
|
+
|
32
|
+
* Fix issue with Typheous Hydra instrument (#418)
|
33
|
+
|
1
34
|
# 4.1.0
|
2
35
|
|
3
36
|
* Preload Celluloid in Shoryuken instrumentation (#331)
|
data/LICENSE.md
CHANGED
@@ -1,31 +1,24 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
This license shall be automatically terminated and revoked if you exceed the scope or violate any terms and conditions of this license.
|
23
|
-
|
24
|
-
ALL LICENSED SOFTWARE, DOCUMENTATION AND OTHER PRODUCTS, INFORMATION, MATERIALS AND SERVICES PROVIDED BY ZIMUTH ARE PROVIDED HERE “AS IS.” ZIMUTH DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHER (INCLUDING ALL WARRANTIES ARISING FROM COURSE OF DEALING, USAGE OR TRADE PRACTICE), AND SPECIFICALLY DISCLAIMS ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. WITHOUT LIMITING THE FOREGOING, ZIMUTH MAKES NO WARRANTY OF ANY KIND THAT THE LICENSED SOFTWARE OR DOCUMENTATION, OR ANY OTHER LICENSOR OR THIRD-PARTY GOODS, SERVICES, TECHNOLOGIES OR MATERIALS, WILL MEET YOUR REQUIREMENTS, OPERATE WITHOUT INTERRUPTION, ACHIEVE ANY INTENDED RESULT, BE COMPATIBLE OR WORK WITH ANY OTHER GOODS, SERVICES, TECHNOLOGIES OR MATERIALS (INCLUDING ANY SOFTWARE, HARDWARE, SYSTEM OR NETWORK) EXCEPT IF AND TO THE EXTENT EXPRESSLY SET FORTH IN THE DOCUMENTATION, OR BE SECURE, ACCURATE, COMPLETE, FREE OF HARMFUL CODE OR ERROR FREE. ALL OPEN-SOURCE COMPONENTS AND OTHER THIRD-PARTY MATERIALS ARE PROVIDED “AS IS” AND ANY REPRESENTATION OR WARRANTY OF OR CONCERNING ANY OF THEM IS STRICTLY BETWEEN LICENSEE AND THE THIRD-PARTY OWNER OR DISTRIBUTOR OF SUCH OPEN-SOURCE COMPONENTS AND THIRD-PARTY MATERIALS.
|
25
|
-
|
26
|
-
IN NO EVENT WILL ZIMUTH BE LIABLE UNDER OR IN CONNECTION WITH THIS LICENSE OR ITS SUBJECT MATTER UNDER ANY LEGAL OR EQUITABLE THEORY, INCLUDING BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY AND OTHERWISE, FOR ANY CONSEQUENTIAL, INCIDENTAL, INDIRECT, EXEMPLARY, SPECIAL, ENHANCED OR PUNITIVE DAMAGES, IN EACH CASE REGARDLESS OF WHETHER SUCH PERSONS WERE ADVISED OF THE POSSIBILITY OF SUCH LOSSES OR DAMAGES OR SUCH LOSSES OR DAMAGES WERE OTHERWISE FORESEEABLE, AND NOTWITHSTANDING THE FAILURE OF ANY AGREED OR OTHER REMEDY OF ITS ESSENTIAL PURPOSE.
|
27
|
-
|
28
|
-
## OPEN SOURCE COMPONENTS
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015-2021 Zimuth, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
29
22
|
|
30
23
|
This product includes `rusage`, which inherits the Artistic License 2.0 from proc/wait3.
|
31
24
|
See http://www.rubydoc.info/gems/rusage/0.2.0.
|
@@ -10,10 +10,10 @@ module ScoutApm
|
|
10
10
|
new_code = Rails.rewrite(path)
|
11
11
|
return self.compile(new_code, path, path)
|
12
12
|
rescue
|
13
|
-
warn "Failed to apply auto-instrumentation to #{path}: #{$!}"
|
13
|
+
warn "Failed to apply auto-instrumentation to #{path}: #{$!}" if ENV['SCOUT_LOG_LEVEL'].to_s.downcase == "debug"
|
14
14
|
end
|
15
15
|
elsif Rails.ignore?(path)
|
16
|
-
warn "AutoInstruments are ignored for path=#{path}."
|
16
|
+
warn "AutoInstruments are ignored for path=#{path}." if ENV['SCOUT_LOG_LEVEL'].to_s.downcase == "debug"
|
17
17
|
end
|
18
18
|
|
19
19
|
return self.compile_file(path)
|
data/lib/scout_apm/config.rb
CHANGED
@@ -29,6 +29,7 @@ require 'scout_apm/environment'
|
|
29
29
|
# report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
|
30
30
|
# scm_subdirectory - if the app root lives in source management in a subdirectory. E.g. #{SCM_ROOT}/src
|
31
31
|
# uri_reporting - 'path' or 'full_path' default is 'full_path', which reports URL params as well as the path.
|
32
|
+
# record_queue_time - true/false to enable recording of queuetime.
|
32
33
|
# remote_agent_host - Internal: What host to bind to, and also send messages to for remote. Default: 127.0.0.1.
|
33
34
|
# remote_agent_port - What port to bind the remote webserver to
|
34
35
|
# start_resque_server_instrument - Used in special situations with certain Resque installs
|
@@ -55,6 +56,8 @@ module ScoutApm
|
|
55
56
|
'direct_host',
|
56
57
|
'disabled_instruments',
|
57
58
|
'enable_background_jobs',
|
59
|
+
'external_service_metric_limit',
|
60
|
+
'external_service_metric_report_limit',
|
58
61
|
'host',
|
59
62
|
'hostname',
|
60
63
|
'ignore',
|
@@ -69,6 +72,7 @@ module ScoutApm
|
|
69
72
|
'name',
|
70
73
|
'profile',
|
71
74
|
'proxy',
|
75
|
+
'record_queue_time',
|
72
76
|
'remote_agent_host',
|
73
77
|
'remote_agent_port',
|
74
78
|
'report_format',
|
@@ -177,7 +181,10 @@ module ScoutApm
|
|
177
181
|
'compress_payload' => BooleanCoercion.new,
|
178
182
|
'database_metric_limit' => IntegerCoercion.new,
|
179
183
|
'database_metric_report_limit' => IntegerCoercion.new,
|
184
|
+
'external_service_metric_limit' => IntegerCoercion.new,
|
185
|
+
'external_service_metric_report_limit' => IntegerCoercion.new,
|
180
186
|
'instrument_http_url_length' => IntegerCoercion.new,
|
187
|
+
'record_queue_time' => BooleanCoercion.new,
|
181
188
|
'start_resque_server_instrument' => BooleanCoercion.new,
|
182
189
|
'timeline_traces' => BooleanCoercion.new,
|
183
190
|
'auto_instruments' => BooleanCoercion.new,
|
@@ -289,9 +296,12 @@ module ScoutApm
|
|
289
296
|
'remote_agent_port' => 7721, # picked at random
|
290
297
|
'database_metric_limit' => 5000, # The hard limit on db metrics
|
291
298
|
'database_metric_report_limit' => 1000,
|
299
|
+
'external_service_metric_limit' => 5000, # The hard limit on external service metrics
|
300
|
+
'external_service_metric_report_limit' => 1000,
|
292
301
|
'instrument_http_url_length' => 300,
|
293
302
|
'start_resque_server_instrument' => true, # still only starts if Resque is detected
|
294
303
|
'collect_remote_ip' => true,
|
304
|
+
'record_queue_time' => true,
|
295
305
|
'timeline_traces' => true,
|
296
306
|
'auto_instruments' => false,
|
297
307
|
'auto_instruments_ignore' => [],
|
@@ -422,7 +432,7 @@ module ScoutApm
|
|
422
432
|
begin
|
423
433
|
raw_file = File.read(@resolved_file_path)
|
424
434
|
erb_file = ERB.new(raw_file).result(binding)
|
425
|
-
parsed_yaml = YAML.load(erb_file)
|
435
|
+
parsed_yaml = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(erb_file) : YAML.load(erb_file)
|
426
436
|
file_settings = parsed_yaml[app_environment]
|
427
437
|
|
428
438
|
if file_settings.is_a? Hash
|
@@ -433,8 +443,8 @@ module ScoutApm
|
|
433
443
|
logger.info("Couldn't find configuration in #{@resolved_file_path} for environment: #{app_environment}. Configuration in ENV will still be applied.")
|
434
444
|
@file_loaded = false
|
435
445
|
end
|
436
|
-
rescue
|
437
|
-
logger.info("Failed loading configuration file (#{@resolved_file_path}):
|
446
|
+
rescue ScoutApm::AllExceptionsExceptOnesWeMustNotRescue => e # Everything except the most important exceptions we should never interfere with
|
447
|
+
logger.info("Failed loading configuration file (#{@resolved_file_path}): ScoutAPM will continue starting with configuration from ENV and defaults. Exception was #{e.class}: #{e.message}#{e.backtrace.map { |bt| "\n #{bt}" }.join('')}")
|
438
448
|
@file_loaded = false
|
439
449
|
end
|
440
450
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module AllExceptionsExceptOnesWeMustNotRescue
|
3
|
+
# Borrowed from https://github.com/rspec/rspec-support/blob/v3.8.0/lib/rspec/support.rb#L132-L140
|
4
|
+
# These exceptions are dangerous to rescue as rescuing them
|
5
|
+
# would interfere with things we should not interfere with.
|
6
|
+
AVOID_RESCUING = [NoMemoryError, SignalException, Interrupt, SystemExit]
|
7
|
+
|
8
|
+
def self.===(exception)
|
9
|
+
AVOID_RESCUING.none? { |ar| ar === exception }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Note, this class must be Marshal Dumpable
|
2
|
+
module ScoutApm
|
3
|
+
class ExternalServiceMetricSet
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :metrics # the raw metrics. You probably want #metrics_to_report
|
7
|
+
|
8
|
+
def marshal_dump
|
9
|
+
[ @metrics ]
|
10
|
+
end
|
11
|
+
|
12
|
+
def marshal_load(array)
|
13
|
+
@metrics = array.first
|
14
|
+
@context = ScoutApm::Agent.instance.context
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(context)
|
18
|
+
@context = context
|
19
|
+
|
20
|
+
# A hash of ExternalServiceMetricStats values, keyed by ExternalServiceMetricStats.key
|
21
|
+
@metrics = Hash.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Need to look this up again if we end up as nil. Which I guess can happen
|
25
|
+
# after a Marshal load?
|
26
|
+
def context
|
27
|
+
@context ||= ScoutApm::Agent.instance.context
|
28
|
+
end
|
29
|
+
|
30
|
+
def each
|
31
|
+
metrics.each do |_key, external_service_metric_stat|
|
32
|
+
yield external_service_metric_stat
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Looks up a ExternalServiceMetricStats instance in the +@metrics+ hash. Sets the value to +other+ if no key
|
37
|
+
# Returns a ExternalServiceMetricStats instance
|
38
|
+
def lookup(other)
|
39
|
+
metrics[other.key] ||= other
|
40
|
+
end
|
41
|
+
|
42
|
+
# Take another set, and merge it with this one
|
43
|
+
def combine!(other)
|
44
|
+
other.each do |metric|
|
45
|
+
self << metric
|
46
|
+
end
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add a single ExternalServiceMetricStats object to this set.
|
51
|
+
#
|
52
|
+
# Looks up an existing one under this key and merges, or just saves a new
|
53
|
+
# one under the key
|
54
|
+
def <<(stat)
|
55
|
+
existing_stat = metrics[stat.key]
|
56
|
+
if existing_stat
|
57
|
+
existing_stat.combine!(stat)
|
58
|
+
elsif at_limit?
|
59
|
+
# We're full up, can't add any more.
|
60
|
+
# Should I log this? It may get super noisy?
|
61
|
+
else
|
62
|
+
metrics[stat.key] = stat
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def increment_transaction_count!
|
67
|
+
metrics.each do |_key, external_service_metric_stat|
|
68
|
+
external_service_metric_stat.increment_transaction_count!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def metrics_to_report
|
73
|
+
report_limit = context.config.value('external_service_metric_report_limit')
|
74
|
+
if metrics.size > report_limit
|
75
|
+
metrics.
|
76
|
+
values.
|
77
|
+
sort_by {|stat| stat.call_time }.
|
78
|
+
reverse.
|
79
|
+
take(report_limit)
|
80
|
+
else
|
81
|
+
metrics.values
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def inspect
|
86
|
+
metrics.map {|key, metric|
|
87
|
+
"#{key.inspect} - Count: #{metric.call_count}, Total Time: #{"%.2f" % metric.call_time}"
|
88
|
+
}.join("\n")
|
89
|
+
end
|
90
|
+
|
91
|
+
def at_limit?
|
92
|
+
@limit ||= context.config.value('external_service_metric_limit')
|
93
|
+
metrics.size >= @limit
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
class ExternalServiceMetricStats
|
3
|
+
|
4
|
+
DEFAULT_HISTOGRAM_SIZE = 50
|
5
|
+
|
6
|
+
attr_reader :domain_name
|
7
|
+
attr_reader :operation
|
8
|
+
attr_reader :scope
|
9
|
+
|
10
|
+
attr_reader :transaction_count
|
11
|
+
|
12
|
+
attr_reader :call_count
|
13
|
+
attr_reader :call_time
|
14
|
+
|
15
|
+
attr_reader :min_call_time
|
16
|
+
attr_reader :max_call_time
|
17
|
+
|
18
|
+
attr_reader :histogram
|
19
|
+
|
20
|
+
def initialize(domain_name, operation, scope, call_count, call_time)
|
21
|
+
@domain_name = domain_name
|
22
|
+
@operation = operation
|
23
|
+
|
24
|
+
@call_count = call_count
|
25
|
+
|
26
|
+
@call_time = call_time
|
27
|
+
@min_call_time = call_time
|
28
|
+
@max_call_time = call_time
|
29
|
+
|
30
|
+
# This histogram is for call_time
|
31
|
+
@histogram = NumericHistogram.new(DEFAULT_HISTOGRAM_SIZE)
|
32
|
+
@histogram.add(call_time)
|
33
|
+
|
34
|
+
@transaction_count = 0
|
35
|
+
|
36
|
+
@scope = scope
|
37
|
+
end
|
38
|
+
|
39
|
+
# Merge data in this scope. Used in ExternalServiceMetricSet
|
40
|
+
def key
|
41
|
+
@key ||= [domain_name, operation, scope]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Combine data from another ExternalServiceMetricStats into +self+. Modifies and returns +self+
|
45
|
+
def combine!(other)
|
46
|
+
return self if other == self
|
47
|
+
|
48
|
+
@transaction_count += other.transaction_count
|
49
|
+
@call_count += other.call_count
|
50
|
+
@call_time += other.call_time
|
51
|
+
|
52
|
+
@min_call_time = other.min_call_time if @min_call_time.zero? or other.min_call_time < @min_call_time
|
53
|
+
@max_call_time = other.max_call_time if other.max_call_time > @max_call_time
|
54
|
+
|
55
|
+
@histogram.combine!(other.histogram)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def as_json
|
60
|
+
json_attributes = [
|
61
|
+
:domain_name,
|
62
|
+
:operation,
|
63
|
+
:scope,
|
64
|
+
|
65
|
+
:transaction_count,
|
66
|
+
:call_count,
|
67
|
+
|
68
|
+
:histogram,
|
69
|
+
:call_time,
|
70
|
+
:max_call_time,
|
71
|
+
:min_call_time,
|
72
|
+
]
|
73
|
+
|
74
|
+
ScoutApm::AttributeArranger.call(self, json_attributes)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Called by the Set on each ExternalServiceMetricStats object that it holds, only
|
78
|
+
# once during the recording of a transaction.
|
79
|
+
#
|
80
|
+
# Don't call elsewhere, and don't set to 1 in the initializer.
|
81
|
+
def increment_transaction_count!
|
82
|
+
@transaction_count += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/scout_apm/fake_store.rb
CHANGED
@@ -20,6 +20,7 @@ module ScoutApm
|
|
20
20
|
detect_from_config ||
|
21
21
|
detect_from_heroku ||
|
22
22
|
detect_from_capistrano ||
|
23
|
+
detect_from_mina ||
|
23
24
|
detect_from_git
|
24
25
|
end
|
25
26
|
|
@@ -43,6 +44,14 @@ module ScoutApm
|
|
43
44
|
nil
|
44
45
|
end
|
45
46
|
|
47
|
+
# https://github.com/mina-deploy/mina
|
48
|
+
def detect_from_mina
|
49
|
+
File.read(File.join(app_root, '.mina_git_revision')).strip
|
50
|
+
rescue
|
51
|
+
logger.debug "Unable to detect Git Revision from Mina: #{$!.message}"
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
46
55
|
def detect_from_git
|
47
56
|
if File.directory?(".git")
|
48
57
|
`git rev-parse --short HEAD`.strip
|
@@ -32,6 +32,7 @@ module ScoutApm
|
|
32
32
|
install_instrument(ScoutApm::Instruments::NetHttp)
|
33
33
|
install_instrument(ScoutApm::Instruments::Typhoeus)
|
34
34
|
install_instrument(ScoutApm::Instruments::HttpClient)
|
35
|
+
install_instrument(ScoutApm::Instruments::HTTP)
|
35
36
|
install_instrument(ScoutApm::Instruments::Memcached)
|
36
37
|
install_instrument(ScoutApm::Instruments::Redis)
|
37
38
|
install_instrument(ScoutApm::Instruments::InfluxDB)
|
@@ -30,7 +30,7 @@ module ScoutApm
|
|
30
30
|
module TyphoeusHydraInstrumentation
|
31
31
|
def run(*args, &block)
|
32
32
|
layer = ScoutApm::Layer.new("HTTP", "Hydra")
|
33
|
-
layer.desc = scout_desc
|
33
|
+
layer.desc = scout_desc
|
34
34
|
|
35
35
|
req = ScoutApm::RequestManager.lookup
|
36
36
|
req.start_layer(layer)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module LayerConverters
|
3
|
+
class ExternalServiceConverter < ConverterBase
|
4
|
+
def initialize(*)
|
5
|
+
super
|
6
|
+
@external_service_metric_set = ExternalServiceMetricSet.new(context)
|
7
|
+
end
|
8
|
+
|
9
|
+
def register_hooks(walker)
|
10
|
+
super
|
11
|
+
|
12
|
+
return unless scope_layer
|
13
|
+
|
14
|
+
walker.on do |layer|
|
15
|
+
next if skip_layer?(layer)
|
16
|
+
stat = ExternalServiceMetricStats.new(
|
17
|
+
domain_name(layer),
|
18
|
+
operation_name(layer), # operation name/verb. GET/POST/PUT etc.
|
19
|
+
scope_layer.legacy_metric_name, # controller_scope
|
20
|
+
1, # count, this is a single call, so 1
|
21
|
+
layer.total_call_time
|
22
|
+
)
|
23
|
+
@external_service_metric_set << stat
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def record!
|
28
|
+
# Everything in the metric set here is from a single transaction, which
|
29
|
+
# we want to keep track of. (One web call did a User#find 10 times, but
|
30
|
+
# only due to 1 http request)
|
31
|
+
@external_service_metric_set.increment_transaction_count!
|
32
|
+
@store.track_external_service_metrics!(@external_service_metric_set)
|
33
|
+
|
34
|
+
nil # not returning anything in the layer results ... not used
|
35
|
+
end
|
36
|
+
|
37
|
+
def skip_layer?(layer)
|
38
|
+
layer.type != 'HTTP' ||
|
39
|
+
layer.limited? ||
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# If we can't name the domain name, default to:
|
46
|
+
DEFAULT_DOMAIN = "Unknown"
|
47
|
+
|
48
|
+
def domain_name(layer)
|
49
|
+
domain = ""
|
50
|
+
desc_str = layer.desc.to_s
|
51
|
+
desc_str = 'http://' + desc_str unless desc_str =~ /^http/i
|
52
|
+
domain = URI.parse(desc_str).host
|
53
|
+
rescue
|
54
|
+
# Do nothing
|
55
|
+
ensure
|
56
|
+
domain = DEFAULT_DOMAIN if domain.to_s.blank?
|
57
|
+
domain
|
58
|
+
end
|
59
|
+
|
60
|
+
def operation_name(layer)
|
61
|
+
"all" # Hardcode to "all" until we support breakout by verb
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/scout_apm/logger.rb
CHANGED
@@ -70,6 +70,10 @@ module ScoutApm
|
|
70
70
|
|
71
71
|
def build_logger
|
72
72
|
logger_class.new(@log_destination)
|
73
|
+
rescue => e
|
74
|
+
logger = ::Logger.new(STDERR)
|
75
|
+
logger.error("Error while building ScoutApm logger: #{e.message}. Falling back to STDERR")
|
76
|
+
logger
|
73
77
|
end
|
74
78
|
|
75
79
|
def logger_class
|
data/lib/scout_apm/reporting.rb
CHANGED
@@ -83,11 +83,12 @@ module ScoutApm
|
|
83
83
|
slow_jobs = reporting_period.slow_jobs_payload
|
84
84
|
histograms = reporting_period.histograms
|
85
85
|
db_query_metrics = reporting_period.db_query_metrics_payload
|
86
|
+
external_service_metrics = reporting_period.external_service_metrics_payload
|
86
87
|
traces = (slow_transactions.map(&:span_trace) + slow_jobs.map(&:span_trace)).compact
|
87
88
|
|
88
89
|
log_deliver(metrics, slow_transactions, metadata, slow_jobs, histograms)
|
89
90
|
|
90
|
-
payload = ScoutApm::Serializers::PayloadSerializer.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, traces)
|
91
|
+
payload = ScoutApm::Serializers::PayloadSerializer.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, external_service_metrics, traces)
|
91
92
|
logger.debug("Sending payload w/ Headers: #{headers.inspect}")
|
92
93
|
|
93
94
|
reporter.report(payload, headers)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module Serializers
|
3
|
+
class ExternalServiceSerializerToJson
|
4
|
+
attr_reader :external_service_metrics
|
5
|
+
|
6
|
+
def initialize(external_service_metrics)
|
7
|
+
@external_service_metrics = external_service_metrics
|
8
|
+
end
|
9
|
+
|
10
|
+
def as_json
|
11
|
+
external_service_metrics.map{|metric| metric.as_json }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -2,9 +2,9 @@
|
|
2
2
|
module ScoutApm
|
3
3
|
module Serializers
|
4
4
|
class PayloadSerializer
|
5
|
-
def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, traces)
|
5
|
+
def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, external_service_metrics, traces)
|
6
6
|
if ScoutApm::Agent.instance.context.config.value("report_format") == 'json'
|
7
|
-
ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, traces)
|
7
|
+
ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, external_service_metrics, traces)
|
8
8
|
else
|
9
9
|
metadata = metadata.dup
|
10
10
|
metadata.default = nil
|
@@ -22,7 +22,8 @@ module ScoutApm
|
|
22
22
|
# payloads. At this point, the marshal code branch is
|
23
23
|
# very rarely used anyway.
|
24
24
|
:histograms => HistogramsSerializerToJson.new(histograms).as_json,
|
25
|
-
:db_query_metrics => db_query_metrics
|
25
|
+
:db_query_metrics => db_query_metrics,
|
26
|
+
:external_service_metrics => external_service_metrics)
|
26
27
|
end
|
27
28
|
end
|
28
29
|
|
@@ -2,7 +2,7 @@ module ScoutApm
|
|
2
2
|
module Serializers
|
3
3
|
module PayloadSerializerToJson
|
4
4
|
class << self
|
5
|
-
def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, traces)
|
5
|
+
def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, external_service_metrics, traces)
|
6
6
|
metadata.merge!({:payload_version => 2})
|
7
7
|
|
8
8
|
jsonify_hash({:metadata => metadata,
|
@@ -14,6 +14,9 @@ module ScoutApm
|
|
14
14
|
:db_metrics => {
|
15
15
|
:query => DbQuerySerializerToJson.new(db_query_metrics).as_json,
|
16
16
|
},
|
17
|
+
:es_metrics => {
|
18
|
+
:http => ExternalServiceSerializerToJson.new(external_service_metrics).as_json,
|
19
|
+
},
|
17
20
|
:span_traces => traces.map{ |t| t.as_json },
|
18
21
|
})
|
19
22
|
end
|
data/lib/scout_apm/store.rb
CHANGED
@@ -52,6 +52,13 @@ module ScoutApm
|
|
52
52
|
}
|
53
53
|
end
|
54
54
|
|
55
|
+
def track_external_service_metrics!(external_service_metric_set, options={})
|
56
|
+
@mutex.synchronize {
|
57
|
+
period = find_period(options[:timestamp])
|
58
|
+
period.merge_external_service_metrics!(external_service_metric_set)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
55
62
|
def track_one!(type, name, value, options={})
|
56
63
|
meta = MetricMeta.new("#{type}/#{name}")
|
57
64
|
stat = MetricStats.new(false)
|
@@ -206,6 +213,8 @@ module ScoutApm
|
|
206
213
|
|
207
214
|
attr_reader :db_query_metric_set
|
208
215
|
|
216
|
+
attr_reader :external_service_metric_set
|
217
|
+
|
209
218
|
def initialize(timestamp, context)
|
210
219
|
@timestamp = timestamp
|
211
220
|
|
@@ -216,6 +225,7 @@ module ScoutApm
|
|
216
225
|
|
217
226
|
@metric_set = MetricSet.new
|
218
227
|
@db_query_metric_set = DbQueryMetricSet.new(context)
|
228
|
+
@external_service_metric_set = ExternalServiceMetricSet.new(context)
|
219
229
|
|
220
230
|
@jobs = Hash.new
|
221
231
|
end
|
@@ -228,7 +238,8 @@ module ScoutApm
|
|
228
238
|
merge_jobs!(other.jobs).
|
229
239
|
merge_slow_jobs!(other.slow_jobs_payload).
|
230
240
|
merge_histograms!(other.histograms).
|
231
|
-
merge_db_query_metrics!(other.db_query_metric_set)
|
241
|
+
merge_db_query_metrics!(other.db_query_metric_set).
|
242
|
+
merge_external_service_metrics!(other.external_service_metric_set)
|
232
243
|
self
|
233
244
|
end
|
234
245
|
|
@@ -254,6 +265,15 @@ module ScoutApm
|
|
254
265
|
self
|
255
266
|
end
|
256
267
|
|
268
|
+
def merge_external_service_metrics!(other_metric_set)
|
269
|
+
if other_metric_set.nil?
|
270
|
+
logger.debug("Missing other_metric_set for merge_external_service_metrics - skipping.")
|
271
|
+
else
|
272
|
+
external_service_metric_set.combine!(other_metric_set)
|
273
|
+
end
|
274
|
+
self
|
275
|
+
end
|
276
|
+
|
257
277
|
def merge_slow_transactions!(new_transactions)
|
258
278
|
Array(new_transactions).each do |one_transaction|
|
259
279
|
request_traces << one_transaction
|
@@ -316,6 +336,10 @@ module ScoutApm
|
|
316
336
|
db_query_metric_set.metrics_to_report
|
317
337
|
end
|
318
338
|
|
339
|
+
def external_service_metrics_payload
|
340
|
+
external_service_metric_set.metrics_to_report
|
341
|
+
end
|
342
|
+
|
319
343
|
#################################
|
320
344
|
# Debug Helpers
|
321
345
|
#################################
|
@@ -117,7 +117,12 @@ module ScoutApm
|
|
117
117
|
# Must follow layer.record_stop_time! as the total_call_time is used to determine if the layer is significant.
|
118
118
|
return if layer_insignificant?(layer)
|
119
119
|
|
120
|
-
|
120
|
+
# Check that the parent exists before calling a method on it, since some threading can get us into a weird state.
|
121
|
+
# this doesn't fix that state, but prevents exceptions from leaking out.
|
122
|
+
parent = @layers[-1]
|
123
|
+
if parent
|
124
|
+
parent.add_child(layer)
|
125
|
+
end
|
121
126
|
|
122
127
|
# This must be called before checking if a backtrace should be collected as the call count influences our capture logic.
|
123
128
|
# We call `#update_call_counts in stop layer to ensure the layer has a final desc. Layer#desc is updated during the AR instrumentation flow.
|
@@ -315,6 +320,7 @@ module ScoutApm
|
|
315
320
|
:queue_time => LayerConverters::RequestQueueTimeConverter,
|
316
321
|
:job => LayerConverters::JobConverter,
|
317
322
|
:db => LayerConverters::DatabaseConverter,
|
323
|
+
:external_service => LayerConverters::ExternalServiceConverter,
|
318
324
|
|
319
325
|
:slow_job => LayerConverters::SlowJobConverter,
|
320
326
|
:slow_req => LayerConverters::SlowRequestConverter,
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
require 'scout_apm/environment'
|
2
4
|
|
3
5
|
# Removes actual values from SQL. Used to both obfuscate the SQL and group
|
@@ -10,11 +12,13 @@ module ScoutApm
|
|
10
12
|
|
11
13
|
PSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*\z|.freeze
|
12
14
|
PSQL_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
|
15
|
+
PSQL_REMOVE_JSON_STRINGS = /:"(?:[^"]|"")*"/.freeze
|
13
16
|
PSQL_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
|
14
17
|
PSQL_AFTER_SELECT = /(?:SELECT\s+).*?(?:WHERE|FROM\z)/im.freeze # Should be everything between a FROM and a WHERE
|
15
18
|
PSQL_PLACEHOLDER = /\$\d+/.freeze
|
16
19
|
PSQL_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
|
17
20
|
PSQL_AFTER_FROM = /(?:FROM\s+).*?(?:WHERE|\z)/im.freeze # Should be everything between a FROM and a WHERE
|
21
|
+
PSQL_AFTER_FROM_AS = /(?:FROM\s+).*?(?:AS|\z)/im.freeze # Should be everything between a FROM and AS without WHERE
|
18
22
|
PSQL_AFTER_JOIN = /(?:JOIN\s+).*?\z/im.freeze
|
19
23
|
PSQL_AFTER_WHERE = /(?:WHERE\s+).*?(?:SELECT|\z)/im.freeze
|
20
24
|
PSQL_AFTER_SET = /(?:SET\s+).*?(?:WHERE|\z)/im.freeze
|
@@ -74,6 +78,7 @@ module ScoutApm
|
|
74
78
|
sql.gsub!(PSQL_VAR_INTERPOLATION, '')
|
75
79
|
# sql.gsub!(PSQL_REMOVE_STRINGS, '?')
|
76
80
|
sql.gsub!(PSQL_AFTER_WHERE) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
|
81
|
+
sql.gsub!(PSQL_AFTER_FROM_AS) {|c| c.gsub(PSQL_REMOVE_JSON_STRINGS, ':"?"')}
|
77
82
|
sql.gsub!(PSQL_AFTER_JOIN) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
|
78
83
|
sql.gsub!(PSQL_AFTER_SET) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
|
79
84
|
sql.gsub!(PSQL_REMOVE_INTEGERS, '?')
|
data/lib/scout_apm/version.rb
CHANGED
data/lib/scout_apm.rb
CHANGED
@@ -27,6 +27,7 @@ require 'rusage'
|
|
27
27
|
#####################################
|
28
28
|
require 'scout_apm/version'
|
29
29
|
|
30
|
+
require 'scout_apm/exceptions'
|
30
31
|
require 'scout_apm/debug'
|
31
32
|
require 'scout_apm/tracked_request'
|
32
33
|
require 'scout_apm/layer'
|
@@ -42,6 +43,7 @@ require 'scout_apm/layer_converters/job_converter'
|
|
42
43
|
require 'scout_apm/layer_converters/slow_job_converter'
|
43
44
|
require 'scout_apm/layer_converters/metric_converter'
|
44
45
|
require 'scout_apm/layer_converters/database_converter'
|
46
|
+
require 'scout_apm/layer_converters/external_service_converter'
|
45
47
|
require 'scout_apm/layer_converters/slow_request_converter'
|
46
48
|
require 'scout_apm/layer_converters/request_queue_time_converter'
|
47
49
|
require 'scout_apm/layer_converters/allocation_metric_converter'
|
@@ -79,6 +81,7 @@ require 'scout_apm/histogram'
|
|
79
81
|
|
80
82
|
require 'scout_apm/instruments/net_http'
|
81
83
|
require 'scout_apm/instruments/http_client'
|
84
|
+
require 'scout_apm/instruments/http'
|
82
85
|
require 'scout_apm/instruments/typhoeus'
|
83
86
|
require 'scout_apm/instruments/moped'
|
84
87
|
require 'scout_apm/instruments/mongoid'
|
@@ -130,6 +133,7 @@ require 'scout_apm/bucket_name_splitter'
|
|
130
133
|
require 'scout_apm/stack_item'
|
131
134
|
require 'scout_apm/metric_set'
|
132
135
|
require 'scout_apm/db_query_metric_set'
|
136
|
+
require 'scout_apm/external_service_metric_set'
|
133
137
|
require 'scout_apm/store'
|
134
138
|
require 'scout_apm/fake_store'
|
135
139
|
require 'scout_apm/tracer'
|
@@ -142,6 +146,7 @@ require 'scout_apm/synchronous_recorder'
|
|
142
146
|
require 'scout_apm/metric_meta'
|
143
147
|
require 'scout_apm/metric_stats'
|
144
148
|
require 'scout_apm/db_query_metric_stats'
|
149
|
+
require 'scout_apm/external_service_metric_stats'
|
145
150
|
require 'scout_apm/slow_transaction'
|
146
151
|
require 'scout_apm/slow_job_record'
|
147
152
|
require 'scout_apm/detailed_trace'
|
@@ -167,6 +172,7 @@ require 'scout_apm/serializers/slow_jobs_serializer_to_json'
|
|
167
172
|
require 'scout_apm/serializers/metrics_to_json_serializer'
|
168
173
|
require 'scout_apm/serializers/histograms_serializer_to_json'
|
169
174
|
require 'scout_apm/serializers/db_query_serializer_to_json'
|
175
|
+
require 'scout_apm/serializers/external_service_serializer_to_json'
|
170
176
|
require 'scout_apm/serializers/directive_serializer'
|
171
177
|
require 'scout_apm/serializers/app_server_load_serializer'
|
172
178
|
|
@@ -217,8 +223,12 @@ if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR
|
|
217
223
|
ScoutApm::Agent.instance.install
|
218
224
|
|
219
225
|
if ScoutApm::Agent.instance.context.config.value("auto_instruments")
|
220
|
-
|
221
|
-
|
226
|
+
if defined?(Parser::TreeRewriter)
|
227
|
+
ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled.")
|
228
|
+
require 'scout_apm/auto_instrument'
|
229
|
+
else # AutoInstruments is turned on, but we don't he the prerequisites to use it
|
230
|
+
ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled, but Parser::TreeRewriter is missing. Update 'parser' gem to >= 2.5.0.")
|
231
|
+
end
|
222
232
|
else
|
223
233
|
ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is disabled.")
|
224
234
|
end
|
data/scout_apm.gemspec
CHANGED
@@ -10,9 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.homepage = "https://github.com/scoutapp/scout_apm_ruby"
|
11
11
|
s.summary = "Ruby application performance monitoring"
|
12
12
|
s.description = "Monitors Ruby apps and reports detailed metrics on performance to Scout."
|
13
|
-
s.license = "
|
14
|
-
|
15
|
-
s.rubyforge_project = "scout_apm"
|
13
|
+
s.license = "MIT"
|
16
14
|
|
17
15
|
s.files = `git ls-files`.split("\n")
|
18
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'scout_apm/external_service_metric_set'
|
4
|
+
|
5
|
+
module ScoutApm
|
6
|
+
class ExternalServiceMetricSetTest < Minitest::Test
|
7
|
+
def test_hard_limit
|
8
|
+
config = make_fake_config(
|
9
|
+
'external_service_metric_limit' => 5, # The hard limit on db metrics
|
10
|
+
'external_service_metric_report_limit' => 2
|
11
|
+
)
|
12
|
+
context = ScoutApm::AgentContext.new().tap{|c| c.config = config }
|
13
|
+
set = ExternalServiceMetricSet.new(context)
|
14
|
+
|
15
|
+
set << fake_stat("a", 10)
|
16
|
+
set << fake_stat("b", 20)
|
17
|
+
set << fake_stat("c", 30)
|
18
|
+
set << fake_stat("d", 40)
|
19
|
+
set << fake_stat("e", 50)
|
20
|
+
set << fake_stat("f", 60)
|
21
|
+
|
22
|
+
assert_equal 5, set.metrics.size
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_report_limit
|
26
|
+
config = make_fake_config(
|
27
|
+
'external_service_metric_limit' => 50, # much larger max, uninterested in hitting it.
|
28
|
+
'external_service_metric_report_limit' => 2
|
29
|
+
)
|
30
|
+
context = ScoutApm::AgentContext.new().tap{|c| c.config = config }
|
31
|
+
set = ExternalServiceMetricSet.new(context)
|
32
|
+
set << fake_stat("a", 10)
|
33
|
+
set << fake_stat("b", 20)
|
34
|
+
set << fake_stat("c", 30)
|
35
|
+
set << fake_stat("d", 40)
|
36
|
+
set << fake_stat("e", 50)
|
37
|
+
set << fake_stat("f", 60)
|
38
|
+
|
39
|
+
assert_equal 2, set.metrics_to_report.size
|
40
|
+
assert_equal ["f","e"], set.metrics_to_report.map{|m| m.key}
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_combine
|
44
|
+
config = make_fake_config(
|
45
|
+
'external_service_metric_limit' => 5, # The hard limit on db metrics
|
46
|
+
'external_service_metric_report_limit' => 2
|
47
|
+
)
|
48
|
+
context = ScoutApm::AgentContext.new().tap{|c| c.config = config }
|
49
|
+
set1 = ExternalServiceMetricSet.new(context)
|
50
|
+
set1 << fake_stat("a", 10)
|
51
|
+
set1 << fake_stat("b", 20)
|
52
|
+
set2 = ExternalServiceMetricSet.new(context)
|
53
|
+
set2 << fake_stat("c", 10)
|
54
|
+
set2 << fake_stat("d", 20)
|
55
|
+
|
56
|
+
combined = set1.combine!(set2)
|
57
|
+
assert_equal ["a", "b", "c", "d"], combined.metrics.map{|_k, m| m.key}.sort
|
58
|
+
end
|
59
|
+
|
60
|
+
def fake_stat(key, call_time)
|
61
|
+
OpenStruct.new(
|
62
|
+
:key => key,
|
63
|
+
:call_time => call_time
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'scout_apm/external_service_metric_stats'
|
4
|
+
|
5
|
+
module ScoutApm
|
6
|
+
class ExternalServiceMetricStatsTest < Minitest::Test
|
7
|
+
def test_as_json_empty_stats
|
8
|
+
stat = build("example.com", "GET", "Controller/public/index", 1, 10)
|
9
|
+
|
10
|
+
assert_equal({
|
11
|
+
:domain_name => "example.com",
|
12
|
+
:operation => "GET",
|
13
|
+
:call_count => 1,
|
14
|
+
:transaction_count => 0,
|
15
|
+
:scope => "Controller/public/index",
|
16
|
+
:histogram => [[10.0, 1]],
|
17
|
+
|
18
|
+
:max_call_time => 10.0,
|
19
|
+
:min_call_time => 10.0,
|
20
|
+
:call_time => 10.0
|
21
|
+
}, stat.as_json)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_increment_transaction_count
|
25
|
+
stat = build()
|
26
|
+
assert_equal 0, stat.transaction_count
|
27
|
+
|
28
|
+
stat.increment_transaction_count!
|
29
|
+
|
30
|
+
assert_equal 1, stat.transaction_count
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_key_name
|
34
|
+
stat = build("example.com", "GET", "Controller/public/index")
|
35
|
+
assert_equal ["example.com", "GET", "Controller/public/index"], stat.key
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_combine_min_call_time_picks_smallest
|
39
|
+
stat1, stat2 = build_pair
|
40
|
+
assert_equal 5.1, stat1.combine!(stat2).min_call_time
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_combine_max_call_time_picks_largest
|
44
|
+
stat1, stat2 = build_pair
|
45
|
+
assert_equal 8.2, stat1.combine!(stat2).max_call_time
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_combine_call_counts_adds
|
49
|
+
stat1, stat2 = build_pair
|
50
|
+
assert_equal 5, stat1.combine!(stat2).call_count
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_combine_transaction_count_adds
|
54
|
+
stat1, stat2 = build_pair
|
55
|
+
2.times { stat1.increment_transaction_count! }
|
56
|
+
3.times { stat2.increment_transaction_count! }
|
57
|
+
|
58
|
+
assert_equal 5, stat1.combine!(stat2).call_count
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_combine_doesnt_merge_with_self
|
62
|
+
stat = build
|
63
|
+
merged = stat.combine!(stat)
|
64
|
+
|
65
|
+
assert_equal DEFAULTS[:call_count], merged.call_count
|
66
|
+
assert_equal DEFAULTS[:call_time], merged.call_time
|
67
|
+
end
|
68
|
+
|
69
|
+
# A.combine!(B) should be the the same as B.combine!(A)
|
70
|
+
# Have to be a bit careful, since combine! is destructive, so make two pairs
|
71
|
+
# with same data to do both sides, then check that they result in the same
|
72
|
+
# answer
|
73
|
+
[:transaction_count, :call_count, :max_call_time, :min_call_time].each do |attr|
|
74
|
+
define_method :"test_combine_#{attr}_is_symmetric" do
|
75
|
+
stat1_a, stat2_a = build_pair
|
76
|
+
stat1_b, stat2_b = build_pair
|
77
|
+
merged_a = stat1_a.combine!(stat2_a)
|
78
|
+
merged_b = stat2_b.combine!(stat1_b)
|
79
|
+
|
80
|
+
assert_equal merged_a.send(attr), merged_b.send(attr)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
#############
|
85
|
+
# Helpers #
|
86
|
+
#############
|
87
|
+
DEFAULTS = {
|
88
|
+
:call_count => 1,
|
89
|
+
:call_time => 10.0,
|
90
|
+
}
|
91
|
+
|
92
|
+
def build(domain_name="example.com",
|
93
|
+
operation="GET",
|
94
|
+
scope="Controller/public/index",
|
95
|
+
call_count=DEFAULTS[:call_count],
|
96
|
+
call_time=DEFAULTS[:call_time])
|
97
|
+
ExternalServiceMetricStats.new(domain_name, operation, scope, call_count, call_time)
|
98
|
+
end
|
99
|
+
|
100
|
+
def build_pair
|
101
|
+
stat1 = build("example.com", "GET", "Controller/public/index", 2, 5.1)
|
102
|
+
stat2 = build("example.com", "GET", "Controller/public/index", 3, 8.2)
|
103
|
+
[stat1, stat2]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
if (ENV["SCOUT_TEST_FEATURES"] || "").include?("typhoeus")
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
require 'scout_apm/instruments/typhoeus'
|
5
|
+
|
6
|
+
require 'typhoeus'
|
7
|
+
|
8
|
+
class TyphoeusTest < Minitest::Test
|
9
|
+
def setup
|
10
|
+
@context = ScoutApm::AgentContext.new
|
11
|
+
@recorder = FakeRecorder.new
|
12
|
+
ScoutApm::Agent.instance.context.recorder = @recorder
|
13
|
+
ScoutApm::Instruments::Typhoeus.new(@context).install
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_instruments_typhoeus_hydra
|
17
|
+
hydra = Typhoeus::Hydra.new
|
18
|
+
2.times.map{ hydra.queue(Typhoeus::Request.new("example.com", followlocation: true)) }
|
19
|
+
|
20
|
+
assert_equal "2 requests", hydra.scout_desc
|
21
|
+
|
22
|
+
hydra.run
|
23
|
+
assert_equal "0 requests", hydra.scout_desc
|
24
|
+
assert_recorded(@recorder, "HTTP", "Hydra", "2 requests")
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_instruments_typhoeus
|
28
|
+
Typhoeus.get("example.com", followlocation: true)
|
29
|
+
assert_recorded(@recorder, "HTTP", "get", "example.com")
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def assert_recorded(recorder, type, name, desc = nil)
|
35
|
+
req = recorder.requests.first
|
36
|
+
assert req, "recorder recorded no layers"
|
37
|
+
assert_equal type, req.root_layer.type
|
38
|
+
assert_equal name, req.root_layer.name
|
39
|
+
assert_equal desc, req.root_layer.desc if !desc.nil?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -8,7 +8,7 @@ class PayloadSerializerTest < Minitest::Test
|
|
8
8
|
:unique_id => "unique_idz",
|
9
9
|
:agent_version => 123
|
10
10
|
}
|
11
|
-
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {}, [], [], [], {}, [])
|
11
|
+
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {}, [], [], [], {}, {}, [])
|
12
12
|
|
13
13
|
# symbol keys turn to strings
|
14
14
|
formatted_metadata = {
|
@@ -49,7 +49,7 @@ class PayloadSerializerTest < Minitest::Test
|
|
49
49
|
stats.total_exclusive_time = 0.078132088
|
50
50
|
}
|
51
51
|
}
|
52
|
-
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize({}, metrics, {}, [], [], [], {}, [])
|
52
|
+
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize({}, metrics, {}, [], [], [], {}, {}, [])
|
53
53
|
formatted_metrics = [
|
54
54
|
{
|
55
55
|
"key" => {
|
@@ -94,7 +94,7 @@ class PayloadSerializerTest < Minitest::Test
|
|
94
94
|
:quotie => "here are some \"quotes\"",
|
95
95
|
:payload_version => 2,
|
96
96
|
}
|
97
|
-
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {}, [], [], [], {}, [])
|
97
|
+
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {}, [], [], [], {}, {}, [])
|
98
98
|
|
99
99
|
# symbol keys turn to strings
|
100
100
|
formatted_metadata = {
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
require 'test_helper'
|
2
4
|
|
3
5
|
module ScoutApm
|
@@ -192,6 +194,17 @@ module ScoutApm
|
|
192
194
|
WHERE (title = ?)|, ss.to_s
|
193
195
|
end
|
194
196
|
|
197
|
+
def test_postgres_insert_select_from_jsonb_to_recordset_with_as
|
198
|
+
sql = %q|
|
199
|
+
INSERT INTO foos(foo_id, bar_id, external_id, email_address, created_at, updated_at)
|
200
|
+
SELECT 123, 456, external_id, email_address, NOW(), NOW()
|
201
|
+
FROM jsonb_to_recordset($${"items":[{"external_id":1234,"email_address":"test@domain.com"}]}$$::jsonb->'items')
|
202
|
+
AS t(external_id integer, email_address varchar)
|
203
|
+
|
|
204
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
|
205
|
+
assert_equal %q|INSERT INTO foos(foo_id, bar_id, external_id, email_address, created_at, updated_at) SELECT ?, ?, external_id, email_address, NOW(), NOW() FROM jsonb_to_recordset($${"items":[{"external_id":?,"email_address":"?"}]}$$::jsonb->'items') AS t(external_id integer, email_address varchar)|, ss.to_s
|
206
|
+
end
|
207
|
+
|
195
208
|
def assert_faster_than(target_seconds)
|
196
209
|
t1 = ::Time.now
|
197
210
|
yield
|
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:
|
4
|
+
version: 5.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Derek Haynes
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-11-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|
@@ -219,7 +219,6 @@ files:
|
|
219
219
|
- ".github/workflows/test.yml"
|
220
220
|
- ".gitignore"
|
221
221
|
- ".rubocop.yml"
|
222
|
-
- ".travis.yml"
|
223
222
|
- CHANGELOG.markdown
|
224
223
|
- Gemfile
|
225
224
|
- Guardfile
|
@@ -238,6 +237,7 @@ files:
|
|
238
237
|
- gems/rails4.gemfile
|
239
238
|
- gems/rails5.gemfile
|
240
239
|
- gems/rails6.gemfile
|
240
|
+
- gems/typhoeus.gemfile
|
241
241
|
- lib/scout_apm.rb
|
242
242
|
- lib/scout_apm/agent.rb
|
243
243
|
- lib/scout_apm/agent/exit_handler.rb
|
@@ -280,8 +280,11 @@ files:
|
|
280
280
|
- lib/scout_apm/error_service/periodic_work.rb
|
281
281
|
- lib/scout_apm/error_service/railtie.rb
|
282
282
|
- lib/scout_apm/error_service/sidekiq.rb
|
283
|
+
- lib/scout_apm/exceptions.rb
|
283
284
|
- lib/scout_apm/extensions/config.rb
|
284
285
|
- lib/scout_apm/extensions/transaction_callback_payload.rb
|
286
|
+
- lib/scout_apm/external_service_metric_set.rb
|
287
|
+
- lib/scout_apm/external_service_metric_stats.rb
|
285
288
|
- lib/scout_apm/fake_store.rb
|
286
289
|
- lib/scout_apm/framework_integrations/rails_2.rb
|
287
290
|
- lib/scout_apm/framework_integrations/rails_3_or_4.rb
|
@@ -328,6 +331,7 @@ files:
|
|
328
331
|
- lib/scout_apm/layer_converters/database_converter.rb
|
329
332
|
- lib/scout_apm/layer_converters/depth_first_walker.rb
|
330
333
|
- lib/scout_apm/layer_converters/error_converter.rb
|
334
|
+
- lib/scout_apm/layer_converters/external_service_converter.rb
|
331
335
|
- lib/scout_apm/layer_converters/find_layer_by_type.rb
|
332
336
|
- lib/scout_apm/layer_converters/histograms.rb
|
333
337
|
- lib/scout_apm/layer_converters/job_converter.rb
|
@@ -359,6 +363,7 @@ files:
|
|
359
363
|
- lib/scout_apm/serializers/app_server_load_serializer.rb
|
360
364
|
- lib/scout_apm/serializers/db_query_serializer_to_json.rb
|
361
365
|
- lib/scout_apm/serializers/directive_serializer.rb
|
366
|
+
- lib/scout_apm/serializers/external_service_serializer_to_json.rb
|
362
367
|
- lib/scout_apm/serializers/histograms_serializer_to_json.rb
|
363
368
|
- lib/scout_apm/serializers/jobs_serializer_to_json.rb
|
364
369
|
- lib/scout_apm/serializers/metrics_to_json_serializer.rb
|
@@ -428,6 +433,8 @@ files:
|
|
428
433
|
- test/unit/error_service/ignored_exceptions_test.rb
|
429
434
|
- test/unit/extensions/periodic_callbacks_test.rb
|
430
435
|
- test/unit/extensions/transaction_callbacks_test.rb
|
436
|
+
- test/unit/external_service_metric_set_test.rb
|
437
|
+
- test/unit/external_service_metric_stats_test.rb
|
431
438
|
- test/unit/fake_store_test.rb
|
432
439
|
- test/unit/git_revision_test.rb
|
433
440
|
- test/unit/histogram_test.rb
|
@@ -435,6 +442,7 @@ files:
|
|
435
442
|
- test/unit/instruments/active_record_test.rb
|
436
443
|
- test/unit/instruments/net_http_test.rb
|
437
444
|
- test/unit/instruments/percentile_sampler_test.rb
|
445
|
+
- test/unit/instruments/typhoeus_test.rb
|
438
446
|
- test/unit/layaway_test.rb
|
439
447
|
- test/unit/layer_children_set_test.rb
|
440
448
|
- test/unit/layer_converters/depth_first_walker_test.rb
|
@@ -462,7 +470,7 @@ files:
|
|
462
470
|
- test/unit/utils/scm.rb
|
463
471
|
homepage: https://github.com/scoutapp/scout_apm_ruby
|
464
472
|
licenses:
|
465
|
-
-
|
473
|
+
- MIT
|
466
474
|
metadata: {}
|
467
475
|
post_install_message:
|
468
476
|
rdoc_options: []
|
@@ -480,7 +488,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
480
488
|
- !ruby/object:Gem::Version
|
481
489
|
version: '0'
|
482
490
|
requirements: []
|
483
|
-
rubygems_version: 3.
|
491
|
+
rubygems_version: 3.0.3
|
484
492
|
signing_key:
|
485
493
|
specification_version: 4
|
486
494
|
summary: Ruby application performance monitoring
|
data/.travis.yml
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
dist: xenial
|
3
|
-
cache: bundler
|
4
|
-
|
5
|
-
matrix:
|
6
|
-
include:
|
7
|
-
- rvm: 2.1
|
8
|
-
gemfile: gems/rails3.gemfile
|
9
|
-
- rvm: 2.2
|
10
|
-
- rvm: 2.3
|
11
|
-
- rvm: 2.4
|
12
|
-
- rvm: 2.5
|
13
|
-
- rvm: 2.6
|
14
|
-
- rvm: 2.7
|
15
|
-
- rvm: 3.0
|
16
|
-
- rvm: 2.6
|
17
|
-
gemfile: gems/octoshark.gemfile
|
18
|
-
- rvm: 2.6
|
19
|
-
name: rubocop yo
|
20
|
-
script: bundle exec rubocop
|
21
|
-
- rvm: 2.6
|
22
|
-
gemfile: gems/rails3.gemfile
|