scout_apm 1.2.1 → 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +6 -0
- data/lib/scout_apm.rb +1 -0
- data/lib/scout_apm/agent.rb +3 -0
- data/lib/scout_apm/agent/logging.rb +2 -2
- data/lib/scout_apm/agent/reporting.rb +1 -0
- data/lib/scout_apm/layer_converter.rb +38 -15
- data/lib/scout_apm/slow_request_policy.rb +83 -0
- data/lib/scout_apm/version.rb +1 -1
- data/test/test_helper.rb +19 -1
- data/test/unit/config_test.rb +2 -1
- data/test/unit/instruments/active_record_instruments_test.rb +0 -2
- data/test/unit/serializers/payload_serializer_test.rb +19 -16
- data/test/unit/sql_sanitizer_test.rb +5 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba5c475b02675dc137c1393e629891935ce8b340
|
4
|
+
data.tar.gz: a3b06a62571ee7b152351fee4893f2a9c395f067
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 046bf02d7bb03cec29d3922d5711353e61a873674ba82ee07bf9012ecd421fbfdcf872a8f88867c18604618b5173ca99352ec5ff66696a0c4a0c5b3bf77f0abc
|
7
|
+
data.tar.gz: dc675677e74bbb7138423b2268c8b3886338f95eed9db8d45f369f25c1149206f9cbf23d44f13e4bb2b5678b342757122c613c0db09710d6979bca1dc88f4226
|
data/CHANGELOG.markdown
CHANGED
data/lib/scout_apm.rb
CHANGED
@@ -97,6 +97,7 @@ require 'scout_apm/tracer'
|
|
97
97
|
require 'scout_apm/context'
|
98
98
|
require 'scout_apm/stackprof_tree_collapser'
|
99
99
|
require 'scout_apm/slow_transaction'
|
100
|
+
require 'scout_apm/slow_request_policy'
|
100
101
|
require 'scout_apm/capacity'
|
101
102
|
require 'scout_apm/attribute_arranger'
|
102
103
|
|
data/lib/scout_apm/agent.rb
CHANGED
@@ -17,6 +17,7 @@ module ScoutApm
|
|
17
17
|
attr_accessor :log_file # path to the log file
|
18
18
|
attr_accessor :options # options passed to the agent when +#start+ is called.
|
19
19
|
attr_accessor :metric_lookup # Hash used to lookup metric ids based on their name and scope
|
20
|
+
attr_reader :slow_request_policy
|
20
21
|
|
21
22
|
# All access to the agent is thru this class method to ensure multiple Agent instances are not initialized per-Ruby process.
|
22
23
|
def self.instance(options = {})
|
@@ -35,6 +36,8 @@ module ScoutApm
|
|
35
36
|
@layaway = ScoutApm::Layaway.new
|
36
37
|
@metric_lookup = Hash.new
|
37
38
|
|
39
|
+
@slow_request_policy = ScoutApm::SlowRequestPolicy.new
|
40
|
+
|
38
41
|
@capacity = ScoutApm::Capacity.new
|
39
42
|
@installed_instruments = []
|
40
43
|
end
|
@@ -8,12 +8,12 @@ module ScoutApm
|
|
8
8
|
|
9
9
|
def init_logger
|
10
10
|
begin
|
11
|
-
@log_file
|
11
|
+
@log_file ||= wants_stdout? ? STDOUT : "#{log_file_path}/scout_apm.log"
|
12
12
|
rescue => e
|
13
13
|
end
|
14
14
|
|
15
15
|
begin
|
16
|
-
@logger
|
16
|
+
@logger ||= Logger.new(@log_file)
|
17
17
|
@logger.level = log_level
|
18
18
|
apply_log_format
|
19
19
|
rescue Exception => e
|
@@ -88,22 +88,41 @@ module ScoutApm
|
|
88
88
|
|
89
89
|
# Take a TrackedRequest and turn it into either nil, or a SlowTransaction record
|
90
90
|
class LayerSlowTransactionConverter < LayerConverterBase
|
91
|
-
SLOW_REQUEST_TIME_THRESHOLD = 2 # seconds
|
92
|
-
|
93
91
|
def call
|
94
|
-
|
92
|
+
policy = ScoutApm::Agent.instance.slow_request_policy.capture_type(root_layer.total_call_time)
|
93
|
+
|
94
|
+
if policy == ScoutApm::SlowRequestPolicy::CAPTURE_NONE
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
|
95
98
|
|
96
99
|
scope = scope_layer
|
97
100
|
return nil unless scope
|
98
|
-
|
99
101
|
uri = request.annotations[:uri] || ""
|
102
|
+
|
103
|
+
metrics = case policy
|
104
|
+
when ScoutApm::SlowRequestPolicy::CAPTURE_SUMMARY
|
105
|
+
{}
|
106
|
+
when ScoutApm::SlowRequestPolicy::CAPTURE_DETAIL
|
107
|
+
create_metrics
|
108
|
+
end
|
109
|
+
|
110
|
+
stackprof = case policy
|
111
|
+
when ScoutApm::SlowRequestPolicy::CAPTURE_SUMMARY
|
112
|
+
[]
|
113
|
+
when ScoutApm::SlowRequestPolicy::CAPTURE_DETAIL
|
114
|
+
# Disable stackprof output for now
|
115
|
+
[] # request.stackprof
|
116
|
+
end
|
117
|
+
|
100
118
|
SlowTransaction.new(uri,
|
101
119
|
scope.legacy_metric_name,
|
102
120
|
root_layer.total_call_time,
|
103
|
-
|
121
|
+
metrics,
|
104
122
|
request.context,
|
105
123
|
root_layer.stop_time,
|
106
|
-
|
124
|
+
stackprof)
|
125
|
+
|
107
126
|
end
|
108
127
|
|
109
128
|
# Full metrics from this request. These get aggregated in Store for the
|
@@ -140,14 +159,15 @@ module ScoutApm
|
|
140
159
|
{:scope => scope_layer.legacy_metric_name}
|
141
160
|
end
|
142
161
|
|
143
|
-
|
144
162
|
# Specific Metric
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
163
|
+
if record_specific_metric?(layer.type)
|
164
|
+
meta_options.merge!(:desc => layer.desc) if layer.desc
|
165
|
+
meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
|
166
|
+
meta.extra.merge!(:backtrace => layer.backtrace) if layer.backtrace
|
167
|
+
metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
168
|
+
stat = metric_hash[meta]
|
169
|
+
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
170
|
+
end
|
151
171
|
|
152
172
|
# Merged Metric (no specifics, just sum up by type)
|
153
173
|
meta = MetricMeta.new("#{layer.type}/all")
|
@@ -159,8 +179,11 @@ module ScoutApm
|
|
159
179
|
metric_hash
|
160
180
|
end
|
161
181
|
|
162
|
-
|
163
|
-
|
182
|
+
SKIP_SPECIFICS = ["Middleware"]
|
183
|
+
# For metrics that are known to be of sort duration (Middleware right now), we don't record specifics on each call to eliminate a metric explosion.
|
184
|
+
# There can be many Middlewares in an app.
|
185
|
+
def record_specific_metric?(name)
|
186
|
+
!SKIP_SPECIFICS.include?(name)
|
164
187
|
end
|
165
188
|
end
|
166
189
|
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# Long running class that provides a yes/no answer to see if a transaction counts as a "slow transaction"
|
2
|
+
#
|
3
|
+
# Rules:
|
4
|
+
# - Runtime must be slower than a threshold
|
5
|
+
# - Log detailed metrics only for the first X
|
6
|
+
# - Stop logging anything after a maximum
|
7
|
+
|
8
|
+
module ScoutApm
|
9
|
+
class SlowRequestPolicy
|
10
|
+
CAPTURE_TYPES = [
|
11
|
+
CAPTURE_DETAIL = "capture_detail",
|
12
|
+
CAPTURE_SUMMARY = "capture_summary",
|
13
|
+
CAPTURE_NONE = "capture_none",
|
14
|
+
]
|
15
|
+
|
16
|
+
# It's not slow unless it's at least this slow
|
17
|
+
SLOW_REQUEST_TIME_THRESHOLD = 2.0 # seconds
|
18
|
+
|
19
|
+
# Stop recording detailed metrics after this count. Still record the fact
|
20
|
+
# a slow request happened though
|
21
|
+
MAX_DETAIL_PER_MINUTE = 10
|
22
|
+
|
23
|
+
# Stop recording anything after this number of slow transactions in a
|
24
|
+
# minute. Will also log a message once per minute that it is stopping
|
25
|
+
# recording.
|
26
|
+
MAX_PER_MINUTE = 500
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@minute_count = 0
|
30
|
+
@detailed_count = 0 # How many detailed slow transactions have we captured this minute?
|
31
|
+
@minute = Time.now.min
|
32
|
+
@clipped_recording = false
|
33
|
+
end
|
34
|
+
|
35
|
+
def capture_type(time)
|
36
|
+
reset_counters
|
37
|
+
|
38
|
+
return CAPTURE_NONE unless slow_enough?(time)
|
39
|
+
return CAPTURE_NONE if clip_recording?
|
40
|
+
|
41
|
+
@minute_count += 1
|
42
|
+
|
43
|
+
if @detailed_count < MAX_DETAIL_PER_MINUTE
|
44
|
+
@detailed_count += 1
|
45
|
+
return CAPTURE_DETAIL
|
46
|
+
else
|
47
|
+
return CAPTURE_SUMMARY
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def reset_counters
|
55
|
+
t = Time.now.min
|
56
|
+
return if t == @minute
|
57
|
+
|
58
|
+
@minute_count = 0
|
59
|
+
@detailed_count = 0
|
60
|
+
@minute = t
|
61
|
+
@clipped_recording = false
|
62
|
+
end
|
63
|
+
|
64
|
+
def slow_enough?(time)
|
65
|
+
time > SLOW_REQUEST_TIME_THRESHOLD
|
66
|
+
end
|
67
|
+
|
68
|
+
# Breaker for rapid-fire excessive slow requests.
|
69
|
+
# If we trip this breaker continually, it will log once per minute that it is broken
|
70
|
+
def clip_recording?
|
71
|
+
if @minute_count > MAX_PER_MINUTE
|
72
|
+
if !@clipped_recording
|
73
|
+
ScoutApm::Agent.instance.logger.info("Skipping future slow requests this minute, reached limit of #{MAX_PER_MINUTE}")
|
74
|
+
end
|
75
|
+
|
76
|
+
@clipped_recording = true
|
77
|
+
true
|
78
|
+
else
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/scout_apm/version.rb
CHANGED
data/test/test_helper.rb
CHANGED
@@ -5,6 +5,8 @@ require 'minitest/pride'
|
|
5
5
|
|
6
6
|
require 'pry'
|
7
7
|
|
8
|
+
require 'scout_apm'
|
9
|
+
|
8
10
|
Kernel.module_eval do
|
9
11
|
# Unset a constant without private access.
|
10
12
|
def self.const_unset(const)
|
@@ -12,5 +14,21 @@ Kernel.module_eval do
|
|
12
14
|
end
|
13
15
|
end
|
14
16
|
|
15
|
-
# require 'scout_apm'
|
16
17
|
|
18
|
+
# Helpers available to all tests
|
19
|
+
class Minitest::Test
|
20
|
+
def setup
|
21
|
+
reopen_logger
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_rack_env(env)
|
25
|
+
ENV['RACK_ENV'] = "production"
|
26
|
+
ScoutApm::Environment.instance.instance_variable_set("@env", nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
def reopen_logger
|
30
|
+
@log_contents = StringIO.new
|
31
|
+
@logger = Logger.new(@log_contents)
|
32
|
+
ScoutApm::Agent.instance.instance_variable_set("@logger", @logger)
|
33
|
+
end
|
34
|
+
end
|
data/test/unit/config_test.rb
CHANGED
@@ -13,17 +13,17 @@ require 'ostruct'
|
|
13
13
|
require 'json' # to deserialize what has been manually serialized by the production code
|
14
14
|
|
15
15
|
# stub the report_format value
|
16
|
-
class ScoutApm::Agent
|
17
|
-
module Config
|
18
|
-
def self.value(key)
|
19
|
-
'json'
|
20
|
-
end
|
21
|
-
end
|
16
|
+
# class ScoutApm::Agent
|
17
|
+
# module Config
|
18
|
+
# def self.value(key)
|
19
|
+
# 'json'
|
20
|
+
# end
|
21
|
+
# end
|
22
22
|
|
23
|
-
def self.instance
|
24
|
-
OpenStruct.new(:config => Config)
|
25
|
-
end
|
26
|
-
end
|
23
|
+
# def self.instance
|
24
|
+
# OpenStruct.new(:config => Config)
|
25
|
+
# end
|
26
|
+
# end
|
27
27
|
|
28
28
|
class PayloadSerializerTest < Minitest::Test
|
29
29
|
|
@@ -33,13 +33,14 @@ class PayloadSerializerTest < Minitest::Test
|
|
33
33
|
:unique_id => "unique_idz",
|
34
34
|
:agent_version => 123
|
35
35
|
}
|
36
|
-
payload = ScoutApm::Serializers::
|
36
|
+
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {})
|
37
37
|
|
38
38
|
# symbol keys turn to strings
|
39
39
|
formatted_metadata = {
|
40
40
|
"app_root" => "/srv/app/rootz",
|
41
41
|
"unique_id" => "unique_idz",
|
42
|
-
"agent_version" => 123
|
42
|
+
"agent_version" => 123,
|
43
|
+
"payload_version" => 2
|
43
44
|
}
|
44
45
|
assert_equal formatted_metadata, JSON.parse(payload)["metadata"]
|
45
46
|
end
|
@@ -73,7 +74,7 @@ class PayloadSerializerTest < Minitest::Test
|
|
73
74
|
stats.total_exclusive_time = 0.07813208899999999
|
74
75
|
}
|
75
76
|
}
|
76
|
-
payload = ScoutApm::Serializers::
|
77
|
+
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize({}, metrics, {})
|
77
78
|
formatted_metrics = [
|
78
79
|
{
|
79
80
|
"key" => {
|
@@ -145,7 +146,7 @@ class PayloadSerializerTest < Minitest::Test
|
|
145
146
|
context.add({"this" => "that"})
|
146
147
|
context.add_user({"hello" => "goodbye"})
|
147
148
|
slow_t = ScoutApm::SlowTransaction.new("http://example.com/blabla", "Buckethead/something/else", 1.23, slow_transaction_metrics, context, Time.at(1448198788), StackProf.new)
|
148
|
-
payload = ScoutApm::Serializers::
|
149
|
+
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize({}, {}, [slow_t])
|
149
150
|
formatted_slow_transactions = [
|
150
151
|
{
|
151
152
|
"key" => {
|
@@ -200,12 +201,14 @@ class PayloadSerializerTest < Minitest::Test
|
|
200
201
|
def test_escapes_json_quotes
|
201
202
|
metadata = {
|
202
203
|
:quotie => "here are some \"quotes\"",
|
204
|
+
:payload_version => 2,
|
203
205
|
}
|
204
|
-
payload = ScoutApm::Serializers::
|
206
|
+
payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {})
|
205
207
|
|
206
208
|
# symbol keys turn to strings
|
207
209
|
formatted_metadata = {
|
208
|
-
"quotie" => "here are some \"quotes\""
|
210
|
+
"quotie" => "here are some \"quotes\"",
|
211
|
+
"payload_version" => 2
|
209
212
|
}
|
210
213
|
assert_equal formatted_metadata, JSON.parse(payload)["metadata"]
|
211
214
|
end
|
@@ -5,6 +5,10 @@ require 'scout_apm/utils/sql_sanitizer'
|
|
5
5
|
module ScoutApm
|
6
6
|
module Utils
|
7
7
|
class SqlSanitizerTest < Minitest::Test
|
8
|
+
def setup
|
9
|
+
ScoutApm::Agent.instance.init_logger
|
10
|
+
end
|
11
|
+
|
8
12
|
# Too long, and we just bail out to prevent long running instrumentation
|
9
13
|
def test_long_sql
|
10
14
|
sql = " " * 1001
|
@@ -68,9 +72,7 @@ module ScoutApm
|
|
68
72
|
assert_equal false, sql.valid_encoding?
|
69
73
|
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :mysql }
|
70
74
|
assert_equal %q|SELECT `blogs`.* FROM `blogs` WHERE (title = 'a_c')|, ss.sql
|
71
|
-
|
72
|
-
assert_equal %q|SELECT `blogs`.* FROM `blogs` WHERE (title = ?)|, ss.to_s
|
73
|
-
end
|
75
|
+
assert_equal %q|SELECT `blogs`.* FROM `blogs` WHERE (title = ?)|, ss.to_s
|
74
76
|
end
|
75
77
|
end
|
76
78
|
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: 1.2.
|
4
|
+
version: 1.2.2
|
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:
|
12
|
+
date: 2016-01-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|
@@ -125,6 +125,7 @@ files:
|
|
125
125
|
- lib/scout_apm/server_integrations/thin.rb
|
126
126
|
- lib/scout_apm/server_integrations/unicorn.rb
|
127
127
|
- lib/scout_apm/server_integrations/webrick.rb
|
128
|
+
- lib/scout_apm/slow_request_policy.rb
|
128
129
|
- lib/scout_apm/slow_transaction.rb
|
129
130
|
- lib/scout_apm/stack_item.rb
|
130
131
|
- lib/scout_apm/stackprof_tree_collapser.rb
|