scout_apm 2.3.2 → 2.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|