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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 499e2fb1ca29ef0ca1a596acf1ef561efc9b8f05
4
- data.tar.gz: 4ac01fb2789e05be922d6121eabae98b72af4198
3
+ metadata.gz: b48d3a3bd3b4f29128855e9cecbe5e98869704f2
4
+ data.tar.gz: 5a1e47f348ad25195f04d7250fc232fae4a082b6
5
5
  SHA512:
6
- metadata.gz: 553499a35ca3b92571b649277feda1d2e195ccf091eefb85f2e0ca5279a36c3e8ea90a0a0073104ee9db398a2c1984d87ace59e706e76b8dda8b02e39ed67f1c
7
- data.tar.gz: db4cc309f2c46c2f4a77f84c7f1c1ab66b7488f08d87310f1c18daa3ba62b0554a170c501cd407050cb89a52417b255c0269b70274c26e64d6ddc9def35c7a70
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 = Utils::SqlSanitizer.new(sql)
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=1000)
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
- return DEFAULT_METRIC unless name
21
- return DEFAULT_METRIC unless model && operation
22
-
23
- if parsed = parse_operation
19
+ parsed = parse_operation
20
+ if parsed
24
21
  "#{model}/#{parsed}"
25
22
  else
26
- "SQL/other"
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 > 4000 # safeguard - don't sanitize or scrub large SQL statements
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."
@@ -1,4 +1,4 @@
1
1
  module ScoutApm
2
- VERSION = "2.3.2"
2
+ VERSION = "2.3.3"
3
3
  end
4
4
 
data/scout_apm.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |s|
30
30
  s.add_development_dependency "addressable"
31
31
  s.add_development_dependency "guard"
32
32
  s.add_development_dependency "guard-minitest"
33
+ s.add_development_dependency "activesupport"
33
34
  end
data/test/test_helper.rb CHANGED
@@ -9,6 +9,8 @@ require 'mocha/mini_test'
9
9
  require 'pry'
10
10
 
11
11
 
12
+ require 'active_support/core_ext/string/inflections'
13
+
12
14
  require 'scout_apm'
13
15
 
14
16
  Kernel.module_eval do
@@ -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 "SQL/Unknown", mn.to_s
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/Unknown", mn.to_s
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 "SQL/Unknown", mn.to_s
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".* VALUES (1,2,3)|
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 "SQL/Unknown", mn.to_s
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 "SQL/other", mn.to_s
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.2
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-13 00:00:00.000000000 Z
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