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 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