scout_apm 4.1.2 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +9 -0
- data/LICENSE.md +21 -28
- data/lib/scout_apm/config.rb +6 -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/git_revision.rb +9 -0
- data/lib/scout_apm/instant/middleware.rb +2 -0
- data/lib/scout_apm/instruments/elasticsearch.rb +2 -0
- data/lib/scout_apm/instruments/sinatra.rb +2 -0
- data/lib/scout_apm/layer_converters/external_service_converter.rb +65 -0
- data/lib/scout_apm/logger.rb +1 -1
- 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 +21 -1
- data/lib/scout_apm/tracked_request.rb +7 -1
- data/lib/scout_apm/utils/sql_sanitizer.rb +2 -0
- data/lib/scout_apm/version.rb +1 -1
- data/lib/scout_apm.rb +4 -0
- data/scout_apm.gemspec +0 -2
- 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/serializers/payload_serializer_test.rb +3 -3
- data/test/unit/sql_sanitizer_test.rb +2 -0
- metadata +8 -3
- data/.travis.yml +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9a94a38d6b06a02e34ce7923e8084976739cc8d0fba1adfbc606f88b0bdf4d7
|
4
|
+
data.tar.gz: aebe73f5379911e8691be2771b3a3fc3f5fbdd4779f2038f6a6f6e0ed38e1e20
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8850b1365747879effcfe383d0b438070069d2b625365dc8ea61db7c72cf6840cdf73675c95aea7a7688750dbcb41efec1b7320232312172bc49be9b207d421
|
7
|
+
data.tar.gz: f3d389aa73f8b4090627843410c610edc1c5fa88740587136dbd8277d3ddfa5360e61d0d256ae23db7cf53f20b482d3f283541c077efcd2d12032e5f3f72f4e2
|
data/CHANGELOG.markdown
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
# 5.0.0
|
2
|
+
|
3
|
+
* Add External Service metrics reporting (#403)
|
4
|
+
* Relicense to MIT (#429)
|
5
|
+
* Opt out of frozen string literals in select files (#427)
|
6
|
+
* Fall back when logger can't write to destination (#423)
|
7
|
+
* Avoid exception on race condition (#407)
|
8
|
+
* Add Mina deploy tracking support (#327)
|
9
|
+
|
1
10
|
# 4.1.2
|
2
11
|
|
3
12
|
* Add record_queue_time configuration (PR #422)
|
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.
|
data/lib/scout_apm/config.rb
CHANGED
@@ -56,6 +56,8 @@ module ScoutApm
|
|
56
56
|
'direct_host',
|
57
57
|
'disabled_instruments',
|
58
58
|
'enable_background_jobs',
|
59
|
+
'external_service_metric_limit',
|
60
|
+
'external_service_metric_report_limit',
|
59
61
|
'host',
|
60
62
|
'hostname',
|
61
63
|
'ignore',
|
@@ -179,6 +181,8 @@ module ScoutApm
|
|
179
181
|
'compress_payload' => BooleanCoercion.new,
|
180
182
|
'database_metric_limit' => IntegerCoercion.new,
|
181
183
|
'database_metric_report_limit' => IntegerCoercion.new,
|
184
|
+
'external_service_metric_limit' => IntegerCoercion.new,
|
185
|
+
'external_service_metric_report_limit' => IntegerCoercion.new,
|
182
186
|
'instrument_http_url_length' => IntegerCoercion.new,
|
183
187
|
'record_queue_time' => BooleanCoercion.new,
|
184
188
|
'start_resque_server_instrument' => BooleanCoercion.new,
|
@@ -292,6 +296,8 @@ module ScoutApm
|
|
292
296
|
'remote_agent_port' => 7721, # picked at random
|
293
297
|
'database_metric_limit' => 5000, # The hard limit on db metrics
|
294
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,
|
295
301
|
'instrument_http_url_length' => 300,
|
296
302
|
'start_resque_server_instrument' => true, # still only starts if Resque is detected
|
297
303
|
'collect_remote_ip' => true,
|
@@ -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
|
@@ -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
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,11 @@ module ScoutApm
|
|
254
265
|
self
|
255
266
|
end
|
256
267
|
|
268
|
+
def merge_external_service_metrics!(other_metric_set)
|
269
|
+
external_service_metric_set.combine!(other_metric_set)
|
270
|
+
self
|
271
|
+
end
|
272
|
+
|
257
273
|
def merge_slow_transactions!(new_transactions)
|
258
274
|
Array(new_transactions).each do |one_transaction|
|
259
275
|
request_traces << one_transaction
|
@@ -316,6 +332,10 @@ module ScoutApm
|
|
316
332
|
db_query_metric_set.metrics_to_report
|
317
333
|
end
|
318
334
|
|
335
|
+
def external_service_metrics_payload
|
336
|
+
external_service_metric_set.metrics_to_report
|
337
|
+
end
|
338
|
+
|
319
339
|
#################################
|
320
340
|
# Debug Helpers
|
321
341
|
#################################
|
@@ -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,
|
data/lib/scout_apm/version.rb
CHANGED
data/lib/scout_apm.rb
CHANGED
@@ -42,6 +42,7 @@ require 'scout_apm/layer_converters/job_converter'
|
|
42
42
|
require 'scout_apm/layer_converters/slow_job_converter'
|
43
43
|
require 'scout_apm/layer_converters/metric_converter'
|
44
44
|
require 'scout_apm/layer_converters/database_converter'
|
45
|
+
require 'scout_apm/layer_converters/external_service_converter'
|
45
46
|
require 'scout_apm/layer_converters/slow_request_converter'
|
46
47
|
require 'scout_apm/layer_converters/request_queue_time_converter'
|
47
48
|
require 'scout_apm/layer_converters/allocation_metric_converter'
|
@@ -130,6 +131,7 @@ require 'scout_apm/bucket_name_splitter'
|
|
130
131
|
require 'scout_apm/stack_item'
|
131
132
|
require 'scout_apm/metric_set'
|
132
133
|
require 'scout_apm/db_query_metric_set'
|
134
|
+
require 'scout_apm/external_service_metric_set'
|
133
135
|
require 'scout_apm/store'
|
134
136
|
require 'scout_apm/fake_store'
|
135
137
|
require 'scout_apm/tracer'
|
@@ -142,6 +144,7 @@ require 'scout_apm/synchronous_recorder'
|
|
142
144
|
require 'scout_apm/metric_meta'
|
143
145
|
require 'scout_apm/metric_stats'
|
144
146
|
require 'scout_apm/db_query_metric_stats'
|
147
|
+
require 'scout_apm/external_service_metric_stats'
|
145
148
|
require 'scout_apm/slow_transaction'
|
146
149
|
require 'scout_apm/slow_job_record'
|
147
150
|
require 'scout_apm/detailed_trace'
|
@@ -167,6 +170,7 @@ require 'scout_apm/serializers/slow_jobs_serializer_to_json'
|
|
167
170
|
require 'scout_apm/serializers/metrics_to_json_serializer'
|
168
171
|
require 'scout_apm/serializers/histograms_serializer_to_json'
|
169
172
|
require 'scout_apm/serializers/db_query_serializer_to_json'
|
173
|
+
require 'scout_apm/serializers/external_service_serializer_to_json'
|
170
174
|
require 'scout_apm/serializers/directive_serializer'
|
171
175
|
require 'scout_apm/serializers/app_server_load_serializer'
|
172
176
|
|
data/scout_apm.gemspec
CHANGED
@@ -12,8 +12,6 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.description = "Monitors Ruby apps and reports detailed metrics on performance to Scout."
|
13
13
|
s.license = "Proprietary (See LICENSE.md)"
|
14
14
|
|
15
|
-
s.rubyforge_project = "scout_apm"
|
16
|
-
|
17
15
|
s.files = `git ls-files`.split("\n")
|
18
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
@@ -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
|
@@ -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 = {
|
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.0.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-02 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
|
@@ -283,6 +282,8 @@ files:
|
|
283
282
|
- lib/scout_apm/error_service/sidekiq.rb
|
284
283
|
- lib/scout_apm/extensions/config.rb
|
285
284
|
- lib/scout_apm/extensions/transaction_callback_payload.rb
|
285
|
+
- lib/scout_apm/external_service_metric_set.rb
|
286
|
+
- lib/scout_apm/external_service_metric_stats.rb
|
286
287
|
- lib/scout_apm/fake_store.rb
|
287
288
|
- lib/scout_apm/framework_integrations/rails_2.rb
|
288
289
|
- lib/scout_apm/framework_integrations/rails_3_or_4.rb
|
@@ -329,6 +330,7 @@ files:
|
|
329
330
|
- lib/scout_apm/layer_converters/database_converter.rb
|
330
331
|
- lib/scout_apm/layer_converters/depth_first_walker.rb
|
331
332
|
- lib/scout_apm/layer_converters/error_converter.rb
|
333
|
+
- lib/scout_apm/layer_converters/external_service_converter.rb
|
332
334
|
- lib/scout_apm/layer_converters/find_layer_by_type.rb
|
333
335
|
- lib/scout_apm/layer_converters/histograms.rb
|
334
336
|
- lib/scout_apm/layer_converters/job_converter.rb
|
@@ -360,6 +362,7 @@ files:
|
|
360
362
|
- lib/scout_apm/serializers/app_server_load_serializer.rb
|
361
363
|
- lib/scout_apm/serializers/db_query_serializer_to_json.rb
|
362
364
|
- lib/scout_apm/serializers/directive_serializer.rb
|
365
|
+
- lib/scout_apm/serializers/external_service_serializer_to_json.rb
|
363
366
|
- lib/scout_apm/serializers/histograms_serializer_to_json.rb
|
364
367
|
- lib/scout_apm/serializers/jobs_serializer_to_json.rb
|
365
368
|
- lib/scout_apm/serializers/metrics_to_json_serializer.rb
|
@@ -429,6 +432,8 @@ files:
|
|
429
432
|
- test/unit/error_service/ignored_exceptions_test.rb
|
430
433
|
- test/unit/extensions/periodic_callbacks_test.rb
|
431
434
|
- test/unit/extensions/transaction_callbacks_test.rb
|
435
|
+
- test/unit/external_service_metric_set_test.rb
|
436
|
+
- test/unit/external_service_metric_stats_test.rb
|
432
437
|
- test/unit/fake_store_test.rb
|
433
438
|
- test/unit/git_revision_test.rb
|
434
439
|
- test/unit/histogram_test.rb
|
data/.travis.yml
DELETED
@@ -1,25 +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/typhoeus.gemfile
|
18
|
-
env: "SCOUT_TEST_FEATURES=typhoeus"
|
19
|
-
- rvm: 2.6
|
20
|
-
gemfile: gems/octoshark.gemfile
|
21
|
-
- rvm: 2.6
|
22
|
-
name: rubocop yo
|
23
|
-
script: bundle exec rubocop
|
24
|
-
- rvm: 2.6
|
25
|
-
gemfile: gems/rails3.gemfile
|