scout_apm 2.3.2 → 2.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +5 -0
- data/lib/scout_apm/instruments/active_record.rb +40 -3
- data/lib/scout_apm/layer_converters/converter_base.rb +1 -1
- data/lib/scout_apm/utils/active_record_metric_name.rb +66 -8
- data/lib/scout_apm/utils/sql_sanitizer.rb +3 -1
- data/lib/scout_apm/version.rb +1 -1
- data/scout_apm.gemspec +1 -0
- data/test/test_helper.rb +2 -0
- data/test/unit/utils/active_record_metric_name_test.rb +45 -7
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b48d3a3bd3b4f29128855e9cecbe5e98869704f2
|
4
|
+
data.tar.gz: 5a1e47f348ad25195f04d7250fc232fae4a082b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fdee3999614a9fbcc7c45087f55cd76176708ec3b162346143524cc5edd4feb44cfb692cd5c04e3f6d8d1f7b1b186083585d5dc833399a7812c4eeb4083b097b
|
7
|
+
data.tar.gz: b3cbeaf5c677468b2205dddc87b35c5e7a949a81c58874493033922624dc7a7d218ed8317c221b0b8dbd39258d03d21a2589252dd3737060d04998e32995d167
|
data/CHANGELOG.markdown
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
# 2.3.3
|
2
|
+
|
3
|
+
* Capture ActiveRecord calls that generate more complex queries
|
4
|
+
* More aggressively determine names of complex queries (to determine "User/find", "Account/create" and similar)
|
5
|
+
|
1
6
|
# 2.3.2
|
2
7
|
|
3
8
|
* More robust startup sequence when using `rails server` vs. directly launching an app server
|
@@ -1,6 +1,38 @@
|
|
1
1
|
require 'scout_apm/utils/sql_sanitizer'
|
2
2
|
|
3
3
|
module ScoutApm
|
4
|
+
class SqlList
|
5
|
+
attr_reader :sqls
|
6
|
+
|
7
|
+
def initialize(sql=nil)
|
8
|
+
@sqls = []
|
9
|
+
|
10
|
+
if !sql.nil?
|
11
|
+
push(sql)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(sql)
|
16
|
+
push(sql)
|
17
|
+
end
|
18
|
+
|
19
|
+
def push(sql)
|
20
|
+
if !(Utils::SqlSanitizer === sql)
|
21
|
+
sql = Utils::SqlSanitizer.new(sql)
|
22
|
+
end
|
23
|
+
@sqls << sql
|
24
|
+
end
|
25
|
+
|
26
|
+
# All of this one, then all of the other.
|
27
|
+
def merge(other)
|
28
|
+
@sqls += other.sqls
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
@sqls.map{|s| s.to_s }.join(";\n")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
4
36
|
module Instruments
|
5
37
|
class ActiveRecord
|
6
38
|
attr_reader :logger
|
@@ -115,13 +147,12 @@ module ScoutApm
|
|
115
147
|
# Extract data from the arguments
|
116
148
|
sql, name = args
|
117
149
|
metric_name = Utils::ActiveRecordMetricName.new(sql, name)
|
118
|
-
desc =
|
150
|
+
desc = SqlList.new(sql)
|
119
151
|
|
120
152
|
# Get current ScoutApm context
|
121
153
|
req = ScoutApm::RequestManager.lookup
|
122
154
|
current_layer = req.current_layer
|
123
155
|
|
124
|
-
|
125
156
|
# If we call #log, we have a real query to run, and we've already
|
126
157
|
# gotten through the cache gatekeeper. Since we want to only trace real
|
127
158
|
# queries, and not repeated identical queries that just hit cache, we
|
@@ -136,9 +167,13 @@ module ScoutApm
|
|
136
167
|
# TODO: Get rid of call .to_s, need to find this without forcing a previous run of the name logic
|
137
168
|
if current_layer.name.to_s == Utils::ActiveRecordMetricName::DEFAULT_METRIC
|
138
169
|
current_layer.name = metric_name
|
139
|
-
current_layer.desc = desc
|
140
170
|
end
|
141
171
|
|
172
|
+
if current_layer.desc.nil?
|
173
|
+
current_layer.desc = SqlList.new
|
174
|
+
end
|
175
|
+
current_layer.desc.merge(desc)
|
176
|
+
|
142
177
|
log_without_scout_instruments(*args, &block)
|
143
178
|
|
144
179
|
# OR: Start a new layer, we didn't pick up instrumentation earlier in the stack.
|
@@ -215,6 +250,7 @@ module ScoutApm
|
|
215
250
|
req = ScoutApm::RequestManager.lookup
|
216
251
|
layer = ScoutApm::Layer.new("ActiveRecord", Utils::ActiveRecordMetricName::DEFAULT_METRIC)
|
217
252
|
layer.annotate_layer(:ignorable => true)
|
253
|
+
layer.desc = SqlList.new
|
218
254
|
req.start_layer(layer)
|
219
255
|
req.ignore_children!
|
220
256
|
begin
|
@@ -233,6 +269,7 @@ module ScoutApm
|
|
233
269
|
|
234
270
|
req = ScoutApm::RequestManager.lookup
|
235
271
|
layer = ScoutApm::Layer.new("ActiveRecord", Utils::ActiveRecordMetricName.new("", "#{model} #{operation}"))
|
272
|
+
layer.desc = SqlList.new
|
236
273
|
req.start_layer(layer)
|
237
274
|
req.ignore_children!
|
238
275
|
begin
|
@@ -143,7 +143,7 @@ module ScoutApm
|
|
143
143
|
end
|
144
144
|
end
|
145
145
|
|
146
|
-
def make_meta_options_desc_hash(layer, max_desc_length=
|
146
|
+
def make_meta_options_desc_hash(layer, max_desc_length=32768)
|
147
147
|
if layer.desc
|
148
148
|
desc_s = layer.desc.to_s
|
149
149
|
trimmed_desc = desc_s[0 .. max_desc_length]
|
@@ -1,12 +1,11 @@
|
|
1
1
|
module ScoutApm
|
2
2
|
module Utils
|
3
3
|
class ActiveRecordMetricName
|
4
|
-
DEFAULT_METRIC = "SQL/Unknown"
|
5
|
-
|
6
4
|
attr_reader :sql, :name
|
5
|
+
DEFAULT_METRIC = 'SQL/other'.freeze
|
7
6
|
|
8
7
|
def initialize(sql, name)
|
9
|
-
@sql = sql
|
8
|
+
@sql = sql || ""
|
10
9
|
@name = name.to_s
|
11
10
|
end
|
12
11
|
|
@@ -17,13 +16,11 @@ module ScoutApm
|
|
17
16
|
# name: Place Load
|
18
17
|
# metric_name: Place/find
|
19
18
|
def to_s
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
if parsed = parse_operation
|
19
|
+
parsed = parse_operation
|
20
|
+
if parsed
|
24
21
|
"#{model}/#{parsed}"
|
25
22
|
else
|
26
|
-
|
23
|
+
regex_name(sql)
|
27
24
|
end
|
28
25
|
end
|
29
26
|
|
@@ -76,6 +73,67 @@ module ScoutApm
|
|
76
73
|
end
|
77
74
|
end
|
78
75
|
end
|
76
|
+
|
77
|
+
|
78
|
+
########################
|
79
|
+
# Regex based naming #
|
80
|
+
########################
|
81
|
+
#
|
82
|
+
WHITE_SPACE = '\s*'
|
83
|
+
REGEX_OPERATION = '(SELECT|UPDATE|INSERT|DELETE)'
|
84
|
+
FROM = 'FROM'
|
85
|
+
INTO = 'INTO'
|
86
|
+
NON_GREEDY_CONSUME = '.*?'
|
87
|
+
TABLE = '(?:"|`)?(.*?)(?:"|`)?\s'
|
88
|
+
COUNT = 'COUNT\(.*?\)'
|
89
|
+
|
90
|
+
SELECT_REGEX = /\A#{WHITE_SPACE}(SELECT)#{WHITE_SPACE}(#{COUNT})?#{NON_GREEDY_CONSUME}#{FROM}#{WHITE_SPACE}#{TABLE}/i.freeze
|
91
|
+
UPDATE_REGEX = /\A#{WHITE_SPACE}(UPDATE)#{WHITE_SPACE}#{TABLE}/i.freeze
|
92
|
+
INSERT_REGEX = /\A#{WHITE_SPACE}(INSERT)#{WHITE_SPACE}#{INTO}#{WHITE_SPACE}#{TABLE}/i.freeze
|
93
|
+
DELETE_REGEX = /\A#{WHITE_SPACE}(DELETE)#{WHITE_SPACE}#{FROM}#{TABLE}/i.freeze
|
94
|
+
|
95
|
+
COUNT_LABEL = 'count'.freeze
|
96
|
+
SELECT_LABEL = 'find'.freeze
|
97
|
+
UPDATE_LABEL = 'save'.freeze
|
98
|
+
INSERT_LABEL = 'create'.freeze
|
99
|
+
DELETE_LABEL = 'destroy'.freeze
|
100
|
+
UNKNOWN_LABEL = 'SQL/other'.freeze
|
101
|
+
|
102
|
+
# Attempt to do some basic parsing of SQL via regexes to extract the SQL
|
103
|
+
# verb (select, update, etc) and the table being operated on.
|
104
|
+
#
|
105
|
+
# This is a fallback from what ActiveRecord gives us, we prefer its
|
106
|
+
# names. But sometimes it is giving us a no-name query, and we have to
|
107
|
+
# attempt to figure it out ourselves.
|
108
|
+
#
|
109
|
+
# This relies on ActiveSupport's classify method. If it's not present,
|
110
|
+
# just skip the attempt to rename here. This could happen in a Grape or
|
111
|
+
# Sinatra application that doesn't import ActiveSupport. At this point,
|
112
|
+
# you're already using ActiveRecord, so it's likely loaded anyway.
|
113
|
+
def regex_name(sql)
|
114
|
+
# We rely on the ActiveSupport inflections code here. Bail early if we can't use it.
|
115
|
+
return UNKNOWN_LABEL unless UNKNOWN_LABEL.respond_to?(:classify)
|
116
|
+
|
117
|
+
if match = SELECT_REGEX.match(sql)
|
118
|
+
operation =
|
119
|
+
if match[2]
|
120
|
+
COUNT_LABEL
|
121
|
+
else
|
122
|
+
SELECT_LABEL
|
123
|
+
end
|
124
|
+
"#{match[3].classify}/#{operation}"
|
125
|
+
elsif match = UPDATE_REGEX.match(sql)
|
126
|
+
"#{match[2].classify}/#{UPDATE_LABEL}"
|
127
|
+
elsif match = INSERT_REGEX.match(sql)
|
128
|
+
"#{match[2].classify}/#{INSERT_LABEL}"
|
129
|
+
elsif match = DELETE_REGEX.match(sql)
|
130
|
+
"#{match[2].classify}/#{DELETE_LABEL}"
|
131
|
+
else
|
132
|
+
UNKNOWN_LABEL
|
133
|
+
end
|
134
|
+
rescue
|
135
|
+
UNKNOWN_LABEL
|
136
|
+
end
|
79
137
|
end
|
80
138
|
end
|
81
139
|
end
|
@@ -74,8 +74,10 @@ module ScoutApm
|
|
74
74
|
encodings.all?{|enc| Encoding.find(enc) rescue false}
|
75
75
|
end
|
76
76
|
|
77
|
+
MAX_SQL_LENGTH = 16384
|
78
|
+
|
77
79
|
def scrubbed(str)
|
78
|
-
return '' if !str.is_a?(String) || str.length >
|
80
|
+
return '' if !str.is_a?(String) || str.length > MAX_SQL_LENGTH # safeguard - don't sanitize or scrub large SQL statements
|
79
81
|
return str if !str.respond_to?(:encode) # Ruby <= 1.8 doesn't have string encoding
|
80
82
|
return str if str.valid_encoding? # Whatever encoding it is, it is valid and we can operate on it
|
81
83
|
ScoutApm::Agent.instance.logger.debug "Scrubbing invalid sql encoding."
|
data/lib/scout_apm/version.rb
CHANGED
data/scout_apm.gemspec
CHANGED
data/test/test_helper.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
require 'scout_apm/utils/active_record_metric_name'
|
3
3
|
|
4
|
+
|
4
5
|
class ActiveRecordMetricNameTest < Minitest::Test
|
5
6
|
# This is a bug report from Андрей Филиппов <tmn.sun@gmail.com>
|
6
7
|
# The code that triggered the bug was: ActiveRecord::Base.connection.execute("%some sql%", :skip_logging)
|
@@ -9,7 +10,7 @@ class ActiveRecordMetricNameTest < Minitest::Test
|
|
9
10
|
name = :skip_logging
|
10
11
|
|
11
12
|
mn = ScoutApm::Utils::ActiveRecordMetricName.new(sql, name)
|
12
|
-
assert_equal "
|
13
|
+
assert_equal "User/find", mn.to_s
|
13
14
|
end
|
14
15
|
|
15
16
|
def test_postgres_column_lookup
|
@@ -27,7 +28,7 @@ class ActiveRecordMetricNameTest < Minitest::Test
|
|
27
28
|
name = "SCHEMA"
|
28
29
|
|
29
30
|
mn = ScoutApm::Utils::ActiveRecordMetricName.new(sql, name)
|
30
|
-
assert_equal "SQL/
|
31
|
+
assert_equal "SQL/other", mn.to_s
|
31
32
|
end
|
32
33
|
|
33
34
|
|
@@ -44,23 +45,60 @@ class ActiveRecordMetricNameTest < Minitest::Test
|
|
44
45
|
name = nil
|
45
46
|
|
46
47
|
mn = ScoutApm::Utils::ActiveRecordMetricName.new(sql, name)
|
47
|
-
assert_equal "
|
48
|
+
assert_equal "User/find", mn.to_s
|
48
49
|
end
|
49
50
|
|
50
51
|
def test_with_sql_name
|
51
|
-
sql = %q|INSERT INTO "users"
|
52
|
+
sql = %q|INSERT INTO "users" VALUES (1,2,3)|
|
52
53
|
name = "SQL"
|
53
54
|
|
54
55
|
mn = ScoutApm::Utils::ActiveRecordMetricName.new(sql, name)
|
55
|
-
assert_equal "
|
56
|
+
assert_equal "User/create", mn.to_s
|
56
57
|
end
|
57
58
|
|
58
|
-
# TODO: Determine if there should be a distinction between Unknown and Other.
|
59
59
|
def test_with_custom_name
|
60
60
|
sql = %q|SELECT "users".* FROM "users" /*application:Testapp,controller:public,action:index*/|
|
61
61
|
name = "A whole sentance describing what's what"
|
62
62
|
|
63
63
|
mn = ScoutApm::Utils::ActiveRecordMetricName.new(sql, name)
|
64
|
-
assert_equal "
|
64
|
+
assert_equal "User/find", mn.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# Regex test cases, pass these in w/ "SQL" as the AR provided name field
|
69
|
+
[
|
70
|
+
["LoginRecord/save", 'UPDATE "login_record" SET "updated_at" = ? WHERE "login_record"."login_record_id" = ?'],
|
71
|
+
["UserAccount/save", 'UPDATE "user_account" SET "last_activity" = ? WHERE "user_account"."user_account_id" = ?'],
|
72
|
+
["Membership/save", 'UPDATE "membership" SET "updated_at" = ? WHERE "membership"."membership_id" = ?'],
|
73
|
+
["Membership/save", 'UPDATE "membership" SET "updated_by" = ? WHERE "membership"."membership_id" = ?'],
|
74
|
+
["Project/find", 'SELECT "project".project_id FROM "project" INNER JOIN "membership" ON "project"."project_id" = "membership"."project_id" WHERE "membership"."user_account_id" = ?'],
|
75
|
+
["Entity/count", 'SELECT COUNT(*) FROM "entity" WHERE "entity"."entity_id" IN (?)'],
|
76
|
+
["EntityEventStep/find", 'SELECT entity_event_step.entity_id, entity_event_step.event_step_id, event_step.event_id FROM entity_event_step INNER JOIN event_step ON event_step.event_step_id = entity_event_step.event_step_id WHERE event_step.event_id in (?) AND entity_event_step.entity_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'],
|
77
|
+
["CustomField/count", 'SELECT COUNT(*) FROM "custom_field" WHERE "custom_field"."project_id" = ? AND "custom_field"."enabled" = ?'],
|
78
|
+
["Event/find", 'SELECT MAX("events"."updated_at") AS max_id FROM "events" LEFT OUTER JOIN "configurations" ON "configurations"."event_id" = "events"."id" WHERE (configurations.date >= ?)'],
|
79
|
+
["Sponsorship/find", ' SELECT "sponsorships"."id" AS t0_r0, "sponsorships"."event_id" AS t0_r1, "sponsorships"."name" AS t0_r2, "sponsorships"."position" AS t0_r3, "sponsorships"."created_at" AS t0_r4, "sponsorships"."updated_at" AS t0_r5, "sponsorships"."display_name" AS t0_r6, "sponsorships"."home_page_id" AS t0_r7, "sponsors"."id" AS t1_r0, "sponsors"."name" AS t1_r1, "sponsors"."url" AS t1_r2, "sponsors"."created_at" AS t1_r3, "sponsors"."updated_at" AS t1_r4, "assets"."id" AS t2_r0, "assets"."type" AS t2_r1, "assets"."assetable_id" AS t2_r2, "assets"."assetable_type" AS t2_r3, "assets"."attachment_file_name" AS t2_r4, "assets"."attachment_content_type" AS t2_r5, "assets"."attachment_file_size" AS t2_r6, "assets"."attachment_updated_at" AS t2_r7, "assets"."created_at" AS t2_r8, "assets"."updated_at" AS t2_r9 FROM "sponsorships" LEFT OUTER JOIN "ranks" ON "ranks"."sponsorship_id" = "sponsorships"."id" LEFT OUTER JOIN "sponsors" ON "sponsors"."id" = "ranks"."sponsor_id" LEFT OUTER JOIN "assets" ON "assets".'],
|
80
|
+
["MaxmindGeoliteCountry/find", 'SELECT country, country_code FROM maxmind_geolite_country WHERE start_ip_num <= ? AND ? <= end_ip_num'],
|
81
|
+
["Activity/find", 'SELECT `activities`.`id` FROM `activities` INNER JOIN `activity_attendees` ON `activity_attendees`.`activity_id` = `activities`.`id` AND `activity_attendees`.`deleted` = ? LEFT JOIN activity_recurrence_periods arp ON arp.activity_id = activities.id WHERE (activities.updated_at >= ?) AND ((activities.start_date between ? and ? and activities.all_day = ?) or (CAST(activities.start_date as Date) between ? and ? and activities.all_day = ?)) AND (activity_attendees.user_id IN (?)) AND (arp.id IS NULL)'],
|
82
|
+
|
83
|
+
# Lower Case.
|
84
|
+
# ["MaxmindGeoliteCountry#Select", 'select country, country_code from maxmind_geolite_country where start_ip_num <= ? and ? <= end_ip_num'],
|
85
|
+
|
86
|
+
# No FROM clause (was clipped off)
|
87
|
+
["SQL/other", 'SELECT "events"."id" AS t0_r0, "events"."name" AS t0_r1, "events"."subdomain" AS t0_r2, "events"."created_at" AS t0_r3, "events"."updated_at" AS t0_r4, "events"."slug" AS t0_r5, "events"."domain" AS t0_r6, "events"."published" AS t0_r7, "events"."category" AS t0_r8, "events"."venue_id" AS t0_r9, "events"."bitly_url" AS t0_r10, "events"."shown_in_events_list" AS t0_r11, "venues"."id" AS t1_r0, "venues"."name" AS t1_r1, "venues"."capacity" AS t1_r2, "venues"."address" AS t1_r3, "venues"."parking_info" AS t1_r4, "venues"."manual_coordinates" AS t1_r5, "venues"."latitude" AS t1_r6, "venues"."longitude" AS t1_r7, "venues"."description" AS t1_r8, "venues"."created_at" AS t1_r9, "venues"."updated_at" AS t1_r10, "venue_translations"."id" AS t2_r0, "venue_translations"."venue_id" AS t2_r1, "venue_translations"."locale" AS t2_r2, "venue_translations"."created_at" AS t2_r3, "venue_translations"."updated_at" AS t2_r4, "venue_translations"."name" AS t2_r5, "venue_translations"."description" AS t2_r6'],
|
88
|
+
|
89
|
+
# Stuff we don't care about in SQL
|
90
|
+
["SQL/other", 'SET SESSION statement_timeout = ?'],
|
91
|
+
["SQL/other", 'SHOW TIME ZONE'],
|
92
|
+
["SQL/other", 'BEGIN'],
|
93
|
+
["SQL/other", 'COMMIT'],
|
94
|
+
|
95
|
+
# Empty strings, or invalid SQL
|
96
|
+
["SQL/other", ''],
|
97
|
+
["SQL/other", 'not sql at all!'],
|
98
|
+
].each_with_index do |(expected, sql), i|
|
99
|
+
define_method :"test_regex_naming_#{i}" do
|
100
|
+
actual = ScoutApm::Utils::ActiveRecordMetricName.new(sql, "SQL").to_s
|
101
|
+
assert_equal expected, actual
|
102
|
+
end
|
65
103
|
end
|
66
104
|
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: 2.3.
|
4
|
+
version: 2.3.3
|
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: 2017-12-
|
12
|
+
date: 2017-12-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|
@@ -137,6 +137,20 @@ dependencies:
|
|
137
137
|
- - ">="
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: '0'
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: activesupport
|
142
|
+
requirement: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
type: :development
|
148
|
+
prerelease: false
|
149
|
+
version_requirements: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
140
154
|
description: Monitors Ruby apps and reports detailed metrics on performance to Scout.
|
141
155
|
email:
|
142
156
|
- support@scoutapp.com
|