scout_apm 4.1.1 → 5.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +33 -0
  3. data/LICENSE.md +21 -28
  4. data/lib/scout_apm/auto_instrument/instruction_sequence.rb +2 -2
  5. data/lib/scout_apm/config.rb +13 -3
  6. data/lib/scout_apm/exceptions.rb +12 -0
  7. data/lib/scout_apm/external_service_metric_set.rb +97 -0
  8. data/lib/scout_apm/external_service_metric_stats.rb +85 -0
  9. data/lib/scout_apm/fake_store.rb +3 -0
  10. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +1 -1
  11. data/lib/scout_apm/git_revision.rb +9 -0
  12. data/lib/scout_apm/instant/middleware.rb +2 -0
  13. data/lib/scout_apm/instrument_manager.rb +1 -0
  14. data/lib/scout_apm/instruments/elasticsearch.rb +2 -0
  15. data/lib/scout_apm/instruments/sinatra.rb +2 -0
  16. data/lib/scout_apm/layer_converters/external_service_converter.rb +65 -0
  17. data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +2 -0
  18. data/lib/scout_apm/logger.rb +4 -0
  19. data/lib/scout_apm/reporting.rb +2 -1
  20. data/lib/scout_apm/serializers/external_service_serializer_to_json.rb +15 -0
  21. data/lib/scout_apm/serializers/payload_serializer.rb +4 -3
  22. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +4 -1
  23. data/lib/scout_apm/store.rb +25 -1
  24. data/lib/scout_apm/tracked_request.rb +7 -1
  25. data/lib/scout_apm/utils/sql_sanitizer.rb +9 -2
  26. data/lib/scout_apm/version.rb +1 -1
  27. data/lib/scout_apm.rb +12 -2
  28. data/scout_apm.gemspec +1 -3
  29. data/test/unit/external_service_metric_set_test.rb +67 -0
  30. data/test/unit/external_service_metric_stats_test.rb +106 -0
  31. data/test/unit/serializers/payload_serializer_test.rb +3 -3
  32. data/test/unit/sql_sanitizer_test.rb +22 -9
  33. metadata +11 -5
  34. data/.travis.yml +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b7247aa1c6545f1bad63c0f911359ba038686c9cf15b30d560ca887467956ef
4
- data.tar.gz: 1fd60f4f23521c1274d27cbf710886bbb5af1baa922ddb3642254917c4db9061
3
+ metadata.gz: 5a1da3776ca231a4d81c9ab5935e6b644ab46ae5609cef60402ccd5258e07795
4
+ data.tar.gz: f583776f39fa426aa43d1942bd83e4bcd23ca3a6f71a2e883df22a74cb2db868
5
5
  SHA512:
6
- metadata.gz: 1581588299fe697107ce3fd8be5575bd63dbc811cf1b6827e08e666ff7e24a9f857366592e632a441e53daea21914c9a9ec7dfdc8acaa1cfaabccde9636b3b56
7
- data.tar.gz: 41c2b41c8e7875188f671b59ad23aa62974b2e0c2ae3d08e93366e81158809b14b8b4c8f217c198e39d311ec8ef1921ec672da726a095702c1b7d7521ab0f7f3
6
+ metadata.gz: bc2f10c76ea8abbb6f74cbe86cbd0e0d1265143a19008307609be35adb57737f474c9a2f59545e2f36b00b5006c5f1b9726f59e70c3e327e6487eb2445e07df6
7
+ data.tar.gz: 8863c891bbd5f1e87d56a06f8275cbcf711b15964116cbddbb648398825eed65fa6ce73cd3848815e8ab8a1eec1f8a2e4a878682e2d47d457778786532ee3db4
data/CHANGELOG.markdown CHANGED
@@ -1,3 +1,36 @@
1
+ # Unreleased
2
+
3
+ # 5.1.1
4
+
5
+ * Improvements to SqlServer scrubbing in SqlSanitizer (#422)
6
+
7
+ # 5.1.0
8
+
9
+ * Specify correct (MIT) license in Gemspec (#430)
10
+ * Install HTTP::Client instruments (#420)
11
+ * Sanitize FROM jsonb_as_recordset AS correctly in Postgres (#332)
12
+ * Call to_h on `ActiveRecord::Base.configurations` (#434)
13
+ * Allow loading of trusted `config/scout_apm.yml` via `YAML.unsafe_load` if available (#435)
14
+ * Better exception handling when loading config (#436)
15
+ * Check for nil other_metric_set in merge_external_service_metrics (#437)
16
+ * Log `warn` in InstructionSequence only if SCOUT_LOG_LEVEL is debug (#438)
17
+ * Check for Parser::TreeRewriter before loading AutoInstruments to avoid LoadError (#440)
18
+ * Fall back to STDERR upon exception in build_logger (#441)
19
+
20
+
21
+ # 5.0.0
22
+
23
+ * Add External Service metrics reporting (#403)
24
+ * Relicense to MIT (#429)
25
+ * Opt out of frozen string literals in select files (#427)
26
+ * Fall back when logger can't write to destination (#423)
27
+ * Avoid exception on race condition (#407)
28
+ * Add Mina deploy tracking support (#327)
29
+
30
+ # 4.1.2
31
+
32
+ * Add record_queue_time configuration (PR #422)
33
+
1
34
  # 4.1.1
2
35
 
3
36
  * Fix issue with Typheous Hydra instrument (#418)
data/LICENSE.md CHANGED
@@ -1,31 +1,24 @@
1
- # Scout Software Agent License
2
-
3
- Subject to and conditioned upon your continued compliance with the terms and conditions of this license, Zimuth, Inc. grants you a non-exclusive, non-sublicensable and non-transferable, limited license to install, use and run one copy of this software on each of your and your affiliate’s computers. This license also grants you the limited right to distribute verbatim copies of this software and documentation to third parties provided the software and documentation will (a) remain the exclusive property of Zimuth; (b) be subject to the terms and conditions of this license; and (c) include a complete and unaltered copy of this license and all other copyright or other intellectual property rights notices contained in the original.
4
-
5
- The software includes the open-source components listed below. Any use of the open-source components by you shall be governed by, and subject to, the terms and conditions of the applicable open-source licenses.
6
-
7
- Except as this license expressly permits, you may not:
8
-
9
- * copy the software, in whole or in part;
10
- * modify, correct, adapt, translate, enhance or otherwise prepare derivative works or improvements of the software;
11
- * sell, sublicense, assign, distribute, publish, transfer or otherwise make available the software to any person or entity;
12
- * remove, delete, efface, alter, obscure, translate, combine, supplement or otherwise change any trademarks, terms of the documentation, warranties, disclaimers, or intellectual property rights, or other symbols, notices, marks or serial numbers on or relating to any copy of the software or documentation; or
13
- use the software in any manner or for any purpose that infringes, misappropriates or otherwise violates any intellectual property right or other right of any person or entity, or that violates any applicable law;
14
-
15
- By using the software, you acknowledge and agree that:
16
-
17
- * the software and documentation are licensed, not sold, to you by Zimuth and you do not and will not have or acquire under or in connection with this license any ownership interest in the software or documentation, or in any related intellectual property rights; and
18
- * Zimuth will remain the sole and exclusive owner of all right, title and interest in and to the software and documentation, including all related intellectual property rights, subject only to the rights of third parties in open-source components and the limited license granted to you under this license; and
19
-
20
- Except for the limited rights and licenses expressly granted to you under this agreement, nothing in this license grants, by implication, waiver, estoppel or otherwise, to you or any third party any intellectual property rights or other right, title, or interest in or to any of the software or documentation.
21
-
22
- This license shall be automatically terminated and revoked if you exceed the scope or violate any terms and conditions of this license.
23
-
24
- ALL LICENSED SOFTWARE, DOCUMENTATION AND OTHER PRODUCTS, INFORMATION, MATERIALS AND SERVICES PROVIDED BY ZIMUTH ARE PROVIDED HERE “AS IS.” ZIMUTH DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHER (INCLUDING ALL WARRANTIES ARISING FROM COURSE OF DEALING, USAGE OR TRADE PRACTICE), AND SPECIFICALLY DISCLAIMS ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. WITHOUT LIMITING THE FOREGOING, ZIMUTH MAKES NO WARRANTY OF ANY KIND THAT THE LICENSED SOFTWARE OR DOCUMENTATION, OR ANY OTHER LICENSOR OR THIRD-PARTY GOODS, SERVICES, TECHNOLOGIES OR MATERIALS, WILL MEET YOUR REQUIREMENTS, OPERATE WITHOUT INTERRUPTION, ACHIEVE ANY INTENDED RESULT, BE COMPATIBLE OR WORK WITH ANY OTHER GOODS, SERVICES, TECHNOLOGIES OR MATERIALS (INCLUDING ANY SOFTWARE, HARDWARE, SYSTEM OR NETWORK) EXCEPT IF AND TO THE EXTENT EXPRESSLY SET FORTH IN THE DOCUMENTATION, OR BE SECURE, ACCURATE, COMPLETE, FREE OF HARMFUL CODE OR ERROR FREE. ALL OPEN-SOURCE COMPONENTS AND OTHER THIRD-PARTY MATERIALS ARE PROVIDED “AS IS” AND ANY REPRESENTATION OR WARRANTY OF OR CONCERNING ANY OF THEM IS STRICTLY BETWEEN LICENSEE AND THE THIRD-PARTY OWNER OR DISTRIBUTOR OF SUCH OPEN-SOURCE COMPONENTS AND THIRD-PARTY MATERIALS.
25
-
26
- IN NO EVENT WILL ZIMUTH BE LIABLE UNDER OR IN CONNECTION WITH THIS LICENSE OR ITS SUBJECT MATTER UNDER ANY LEGAL OR EQUITABLE THEORY, INCLUDING BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY AND OTHERWISE, FOR ANY CONSEQUENTIAL, INCIDENTAL, INDIRECT, EXEMPLARY, SPECIAL, ENHANCED OR PUNITIVE DAMAGES, IN EACH CASE REGARDLESS OF WHETHER SUCH PERSONS WERE ADVISED OF THE POSSIBILITY OF SUCH LOSSES OR DAMAGES OR SUCH LOSSES OR DAMAGES WERE OTHERWISE FORESEEABLE, AND NOTWITHSTANDING THE FAILURE OF ANY AGREED OR OTHER REMEDY OF ITS ESSENTIAL PURPOSE.
27
-
28
- ## OPEN SOURCE COMPONENTS
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2021 Zimuth, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
29
22
 
30
23
  This product includes `rusage`, which inherits the Artistic License 2.0 from proc/wait3.
31
24
  See http://www.rubydoc.info/gems/rusage/0.2.0.
@@ -10,10 +10,10 @@ module ScoutApm
10
10
  new_code = Rails.rewrite(path)
11
11
  return self.compile(new_code, path, path)
12
12
  rescue
13
- warn "Failed to apply auto-instrumentation to #{path}: #{$!}"
13
+ warn "Failed to apply auto-instrumentation to #{path}: #{$!}" if ENV['SCOUT_LOG_LEVEL'].to_s.downcase == "debug"
14
14
  end
15
15
  elsif Rails.ignore?(path)
16
- warn "AutoInstruments are ignored for path=#{path}."
16
+ warn "AutoInstruments are ignored for path=#{path}." if ENV['SCOUT_LOG_LEVEL'].to_s.downcase == "debug"
17
17
  end
18
18
 
19
19
  return self.compile_file(path)
@@ -29,6 +29,7 @@ require 'scout_apm/environment'
29
29
  # report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
30
30
  # scm_subdirectory - if the app root lives in source management in a subdirectory. E.g. #{SCM_ROOT}/src
31
31
  # uri_reporting - 'path' or 'full_path' default is 'full_path', which reports URL params as well as the path.
32
+ # record_queue_time - true/false to enable recording of queuetime.
32
33
  # remote_agent_host - Internal: What host to bind to, and also send messages to for remote. Default: 127.0.0.1.
33
34
  # remote_agent_port - What port to bind the remote webserver to
34
35
  # start_resque_server_instrument - Used in special situations with certain Resque installs
@@ -55,6 +56,8 @@ module ScoutApm
55
56
  'direct_host',
56
57
  'disabled_instruments',
57
58
  'enable_background_jobs',
59
+ 'external_service_metric_limit',
60
+ 'external_service_metric_report_limit',
58
61
  'host',
59
62
  'hostname',
60
63
  'ignore',
@@ -69,6 +72,7 @@ module ScoutApm
69
72
  'name',
70
73
  'profile',
71
74
  'proxy',
75
+ 'record_queue_time',
72
76
  'remote_agent_host',
73
77
  'remote_agent_port',
74
78
  'report_format',
@@ -177,7 +181,10 @@ module ScoutApm
177
181
  'compress_payload' => BooleanCoercion.new,
178
182
  'database_metric_limit' => IntegerCoercion.new,
179
183
  'database_metric_report_limit' => IntegerCoercion.new,
184
+ 'external_service_metric_limit' => IntegerCoercion.new,
185
+ 'external_service_metric_report_limit' => IntegerCoercion.new,
180
186
  'instrument_http_url_length' => IntegerCoercion.new,
187
+ 'record_queue_time' => BooleanCoercion.new,
181
188
  'start_resque_server_instrument' => BooleanCoercion.new,
182
189
  'timeline_traces' => BooleanCoercion.new,
183
190
  'auto_instruments' => BooleanCoercion.new,
@@ -289,9 +296,12 @@ module ScoutApm
289
296
  'remote_agent_port' => 7721, # picked at random
290
297
  'database_metric_limit' => 5000, # The hard limit on db metrics
291
298
  'database_metric_report_limit' => 1000,
299
+ 'external_service_metric_limit' => 5000, # The hard limit on external service metrics
300
+ 'external_service_metric_report_limit' => 1000,
292
301
  'instrument_http_url_length' => 300,
293
302
  'start_resque_server_instrument' => true, # still only starts if Resque is detected
294
303
  'collect_remote_ip' => true,
304
+ 'record_queue_time' => true,
295
305
  'timeline_traces' => true,
296
306
  'auto_instruments' => false,
297
307
  'auto_instruments_ignore' => [],
@@ -422,7 +432,7 @@ module ScoutApm
422
432
  begin
423
433
  raw_file = File.read(@resolved_file_path)
424
434
  erb_file = ERB.new(raw_file).result(binding)
425
- parsed_yaml = YAML.load(erb_file)
435
+ parsed_yaml = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(erb_file) : YAML.load(erb_file)
426
436
  file_settings = parsed_yaml[app_environment]
427
437
 
428
438
  if file_settings.is_a? Hash
@@ -433,8 +443,8 @@ module ScoutApm
433
443
  logger.info("Couldn't find configuration in #{@resolved_file_path} for environment: #{app_environment}. Configuration in ENV will still be applied.")
434
444
  @file_loaded = false
435
445
  end
436
- rescue Exception => e # Explicit `Exception` handling to catch SyntaxError and anything else that ERB or YAML may throw
437
- logger.info("Failed loading configuration file (#{@resolved_file_path}): #{e.message}. ScoutAPM will continue starting with configuration from ENV and defaults")
446
+ rescue ScoutApm::AllExceptionsExceptOnesWeMustNotRescue => e # Everything except the most important exceptions we should never interfere with
447
+ logger.info("Failed loading configuration file (#{@resolved_file_path}): ScoutAPM will continue starting with configuration from ENV and defaults. Exception was #{e.class}: #{e.message}#{e.backtrace.map { |bt| "\n #{bt}" }.join('')}")
438
448
  @file_loaded = false
439
449
  end
440
450
  end
@@ -0,0 +1,12 @@
1
+ module ScoutApm
2
+ module AllExceptionsExceptOnesWeMustNotRescue
3
+ # Borrowed from https://github.com/rspec/rspec-support/blob/v3.8.0/lib/rspec/support.rb#L132-L140
4
+ # These exceptions are dangerous to rescue as rescuing them
5
+ # would interfere with things we should not interfere with.
6
+ AVOID_RESCUING = [NoMemoryError, SignalException, Interrupt, SystemExit]
7
+
8
+ def self.===(exception)
9
+ AVOID_RESCUING.none? { |ar| ar === exception }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,97 @@
1
+ # Note, this class must be Marshal Dumpable
2
+ module ScoutApm
3
+ class ExternalServiceMetricSet
4
+ include Enumerable
5
+
6
+ attr_reader :metrics # the raw metrics. You probably want #metrics_to_report
7
+
8
+ def marshal_dump
9
+ [ @metrics ]
10
+ end
11
+
12
+ def marshal_load(array)
13
+ @metrics = array.first
14
+ @context = ScoutApm::Agent.instance.context
15
+ end
16
+
17
+ def initialize(context)
18
+ @context = context
19
+
20
+ # A hash of ExternalServiceMetricStats values, keyed by ExternalServiceMetricStats.key
21
+ @metrics = Hash.new
22
+ end
23
+
24
+ # Need to look this up again if we end up as nil. Which I guess can happen
25
+ # after a Marshal load?
26
+ def context
27
+ @context ||= ScoutApm::Agent.instance.context
28
+ end
29
+
30
+ def each
31
+ metrics.each do |_key, external_service_metric_stat|
32
+ yield external_service_metric_stat
33
+ end
34
+ end
35
+
36
+ # Looks up a ExternalServiceMetricStats instance in the +@metrics+ hash. Sets the value to +other+ if no key
37
+ # Returns a ExternalServiceMetricStats instance
38
+ def lookup(other)
39
+ metrics[other.key] ||= other
40
+ end
41
+
42
+ # Take another set, and merge it with this one
43
+ def combine!(other)
44
+ other.each do |metric|
45
+ self << metric
46
+ end
47
+ self
48
+ end
49
+
50
+ # Add a single ExternalServiceMetricStats object to this set.
51
+ #
52
+ # Looks up an existing one under this key and merges, or just saves a new
53
+ # one under the key
54
+ def <<(stat)
55
+ existing_stat = metrics[stat.key]
56
+ if existing_stat
57
+ existing_stat.combine!(stat)
58
+ elsif at_limit?
59
+ # We're full up, can't add any more.
60
+ # Should I log this? It may get super noisy?
61
+ else
62
+ metrics[stat.key] = stat
63
+ end
64
+ end
65
+
66
+ def increment_transaction_count!
67
+ metrics.each do |_key, external_service_metric_stat|
68
+ external_service_metric_stat.increment_transaction_count!
69
+ end
70
+ end
71
+
72
+ def metrics_to_report
73
+ report_limit = context.config.value('external_service_metric_report_limit')
74
+ if metrics.size > report_limit
75
+ metrics.
76
+ values.
77
+ sort_by {|stat| stat.call_time }.
78
+ reverse.
79
+ take(report_limit)
80
+ else
81
+ metrics.values
82
+ end
83
+ end
84
+
85
+ def inspect
86
+ metrics.map {|key, metric|
87
+ "#{key.inspect} - Count: #{metric.call_count}, Total Time: #{"%.2f" % metric.call_time}"
88
+ }.join("\n")
89
+ end
90
+
91
+ def at_limit?
92
+ @limit ||= context.config.value('external_service_metric_limit')
93
+ metrics.size >= @limit
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,85 @@
1
+ module ScoutApm
2
+ class ExternalServiceMetricStats
3
+
4
+ DEFAULT_HISTOGRAM_SIZE = 50
5
+
6
+ attr_reader :domain_name
7
+ attr_reader :operation
8
+ attr_reader :scope
9
+
10
+ attr_reader :transaction_count
11
+
12
+ attr_reader :call_count
13
+ attr_reader :call_time
14
+
15
+ attr_reader :min_call_time
16
+ attr_reader :max_call_time
17
+
18
+ attr_reader :histogram
19
+
20
+ def initialize(domain_name, operation, scope, call_count, call_time)
21
+ @domain_name = domain_name
22
+ @operation = operation
23
+
24
+ @call_count = call_count
25
+
26
+ @call_time = call_time
27
+ @min_call_time = call_time
28
+ @max_call_time = call_time
29
+
30
+ # This histogram is for call_time
31
+ @histogram = NumericHistogram.new(DEFAULT_HISTOGRAM_SIZE)
32
+ @histogram.add(call_time)
33
+
34
+ @transaction_count = 0
35
+
36
+ @scope = scope
37
+ end
38
+
39
+ # Merge data in this scope. Used in ExternalServiceMetricSet
40
+ def key
41
+ @key ||= [domain_name, operation, scope]
42
+ end
43
+
44
+ # Combine data from another ExternalServiceMetricStats into +self+. Modifies and returns +self+
45
+ def combine!(other)
46
+ return self if other == self
47
+
48
+ @transaction_count += other.transaction_count
49
+ @call_count += other.call_count
50
+ @call_time += other.call_time
51
+
52
+ @min_call_time = other.min_call_time if @min_call_time.zero? or other.min_call_time < @min_call_time
53
+ @max_call_time = other.max_call_time if other.max_call_time > @max_call_time
54
+
55
+ @histogram.combine!(other.histogram)
56
+ self
57
+ end
58
+
59
+ def as_json
60
+ json_attributes = [
61
+ :domain_name,
62
+ :operation,
63
+ :scope,
64
+
65
+ :transaction_count,
66
+ :call_count,
67
+
68
+ :histogram,
69
+ :call_time,
70
+ :max_call_time,
71
+ :min_call_time,
72
+ ]
73
+
74
+ ScoutApm::AttributeArranger.call(self, json_attributes)
75
+ end
76
+
77
+ # Called by the Set on each ExternalServiceMetricStats object that it holds, only
78
+ # once during the recording of a transaction.
79
+ #
80
+ # Don't call elsewhere, and don't set to 1 in the initializer.
81
+ def increment_transaction_count!
82
+ @transaction_count += 1
83
+ end
84
+ end
85
+ end
@@ -26,6 +26,9 @@ module ScoutApm
26
26
  def track_db_query_metrics!(db_query_metric_set, options={})
27
27
  end
28
28
 
29
+ def track_external_service_metrics!(external_service_metric_set, options={})
30
+ end
31
+
29
32
  def track_slow_transaction!(slow_transaction)
30
33
  end
31
34
 
@@ -78,7 +78,7 @@ module ScoutApm
78
78
  end
79
79
 
80
80
  if adapter.nil?
81
- adapter = ActiveRecord::Base.configurations[env]["adapter"]
81
+ adapter = ActiveRecord::Base.configurations.to_h[env]["adapter"]
82
82
  end
83
83
 
84
84
  return adapter
@@ -20,6 +20,7 @@ module ScoutApm
20
20
  detect_from_config ||
21
21
  detect_from_heroku ||
22
22
  detect_from_capistrano ||
23
+ detect_from_mina ||
23
24
  detect_from_git
24
25
  end
25
26
 
@@ -43,6 +44,14 @@ module ScoutApm
43
44
  nil
44
45
  end
45
46
 
47
+ # https://github.com/mina-deploy/mina
48
+ def detect_from_mina
49
+ File.read(File.join(app_root, '.mina_git_revision')).strip
50
+ rescue
51
+ logger.debug "Unable to detect Git Revision from Mina: #{$!.message}"
52
+ nil
53
+ end
54
+
46
55
  def detect_from_git
47
56
  if File.directory?(".git")
48
57
  `git rev-parse --short HEAD`.strip
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: false
2
+
1
3
  module ScoutApm
2
4
  module Instant
3
5
 
@@ -32,6 +32,7 @@ module ScoutApm
32
32
  install_instrument(ScoutApm::Instruments::NetHttp)
33
33
  install_instrument(ScoutApm::Instruments::Typhoeus)
34
34
  install_instrument(ScoutApm::Instruments::HttpClient)
35
+ install_instrument(ScoutApm::Instruments::HTTP)
35
36
  install_instrument(ScoutApm::Instruments::Memcached)
36
37
  install_instrument(ScoutApm::Instruments::Redis)
37
38
  install_instrument(ScoutApm::Instruments::InfluxDB)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: false
2
+
1
3
  module ScoutApm
2
4
  module Instruments
3
5
  class Elasticsearch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: false
2
+
1
3
  # XXX: Is this file used?
2
4
  module ScoutApm
3
5
  module Instruments
@@ -0,0 +1,65 @@
1
+ module ScoutApm
2
+ module LayerConverters
3
+ class ExternalServiceConverter < ConverterBase
4
+ def initialize(*)
5
+ super
6
+ @external_service_metric_set = ExternalServiceMetricSet.new(context)
7
+ end
8
+
9
+ def register_hooks(walker)
10
+ super
11
+
12
+ return unless scope_layer
13
+
14
+ walker.on do |layer|
15
+ next if skip_layer?(layer)
16
+ stat = ExternalServiceMetricStats.new(
17
+ domain_name(layer),
18
+ operation_name(layer), # operation name/verb. GET/POST/PUT etc.
19
+ scope_layer.legacy_metric_name, # controller_scope
20
+ 1, # count, this is a single call, so 1
21
+ layer.total_call_time
22
+ )
23
+ @external_service_metric_set << stat
24
+ end
25
+ end
26
+
27
+ def record!
28
+ # Everything in the metric set here is from a single transaction, which
29
+ # we want to keep track of. (One web call did a User#find 10 times, but
30
+ # only due to 1 http request)
31
+ @external_service_metric_set.increment_transaction_count!
32
+ @store.track_external_service_metrics!(@external_service_metric_set)
33
+
34
+ nil # not returning anything in the layer results ... not used
35
+ end
36
+
37
+ def skip_layer?(layer)
38
+ layer.type != 'HTTP' ||
39
+ layer.limited? ||
40
+ super
41
+ end
42
+
43
+ private
44
+
45
+ # If we can't name the domain name, default to:
46
+ DEFAULT_DOMAIN = "Unknown"
47
+
48
+ def domain_name(layer)
49
+ domain = ""
50
+ desc_str = layer.desc.to_s
51
+ desc_str = 'http://' + desc_str unless desc_str =~ /^http/i
52
+ domain = URI.parse(desc_str).host
53
+ rescue
54
+ # Do nothing
55
+ ensure
56
+ domain = DEFAULT_DOMAIN if domain.to_s.blank?
57
+ domain
58
+ end
59
+
60
+ def operation_name(layer)
61
+ "all" # Hardcode to "all" until we support breakout by verb
62
+ end
63
+ end
64
+ end
65
+ end
@@ -11,6 +11,8 @@ module ScoutApm
11
11
  def record!
12
12
  return unless request.web?
13
13
 
14
+ return unless context.config.value('record_queue_time')
15
+
14
16
  return unless headers
15
17
 
16
18
  raw_start = locate_timestamp
@@ -70,6 +70,10 @@ module ScoutApm
70
70
 
71
71
  def build_logger
72
72
  logger_class.new(@log_destination)
73
+ rescue => e
74
+ logger = ::Logger.new(STDERR)
75
+ logger.error("Error while building ScoutApm logger: #{e.message}. Falling back to STDERR")
76
+ logger
73
77
  end
74
78
 
75
79
  def logger_class
@@ -83,11 +83,12 @@ module ScoutApm
83
83
  slow_jobs = reporting_period.slow_jobs_payload
84
84
  histograms = reporting_period.histograms
85
85
  db_query_metrics = reporting_period.db_query_metrics_payload
86
+ external_service_metrics = reporting_period.external_service_metrics_payload
86
87
  traces = (slow_transactions.map(&:span_trace) + slow_jobs.map(&:span_trace)).compact
87
88
 
88
89
  log_deliver(metrics, slow_transactions, metadata, slow_jobs, histograms)
89
90
 
90
- payload = ScoutApm::Serializers::PayloadSerializer.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, traces)
91
+ payload = ScoutApm::Serializers::PayloadSerializer.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, external_service_metrics, traces)
91
92
  logger.debug("Sending payload w/ Headers: #{headers.inspect}")
92
93
 
93
94
  reporter.report(payload, headers)
@@ -0,0 +1,15 @@
1
+ module ScoutApm
2
+ module Serializers
3
+ class ExternalServiceSerializerToJson
4
+ attr_reader :external_service_metrics
5
+
6
+ def initialize(external_service_metrics)
7
+ @external_service_metrics = external_service_metrics
8
+ end
9
+
10
+ def as_json
11
+ external_service_metrics.map{|metric| metric.as_json }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,9 +2,9 @@
2
2
  module ScoutApm
3
3
  module Serializers
4
4
  class PayloadSerializer
5
- def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, traces)
5
+ def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, external_service_metrics, traces)
6
6
  if ScoutApm::Agent.instance.context.config.value("report_format") == 'json'
7
- ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, traces)
7
+ ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, external_service_metrics, traces)
8
8
  else
9
9
  metadata = metadata.dup
10
10
  metadata.default = nil
@@ -22,7 +22,8 @@ module ScoutApm
22
22
  # payloads. At this point, the marshal code branch is
23
23
  # very rarely used anyway.
24
24
  :histograms => HistogramsSerializerToJson.new(histograms).as_json,
25
- :db_query_metrics => db_query_metrics)
25
+ :db_query_metrics => db_query_metrics,
26
+ :external_service_metrics => external_service_metrics)
26
27
  end
27
28
  end
28
29
 
@@ -2,7 +2,7 @@ module ScoutApm
2
2
  module Serializers
3
3
  module PayloadSerializerToJson
4
4
  class << self
5
- def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, traces)
5
+ def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics, external_service_metrics, traces)
6
6
  metadata.merge!({:payload_version => 2})
7
7
 
8
8
  jsonify_hash({:metadata => metadata,
@@ -14,6 +14,9 @@ module ScoutApm
14
14
  :db_metrics => {
15
15
  :query => DbQuerySerializerToJson.new(db_query_metrics).as_json,
16
16
  },
17
+ :es_metrics => {
18
+ :http => ExternalServiceSerializerToJson.new(external_service_metrics).as_json,
19
+ },
17
20
  :span_traces => traces.map{ |t| t.as_json },
18
21
  })
19
22
  end
@@ -52,6 +52,13 @@ module ScoutApm
52
52
  }
53
53
  end
54
54
 
55
+ def track_external_service_metrics!(external_service_metric_set, options={})
56
+ @mutex.synchronize {
57
+ period = find_period(options[:timestamp])
58
+ period.merge_external_service_metrics!(external_service_metric_set)
59
+ }
60
+ end
61
+
55
62
  def track_one!(type, name, value, options={})
56
63
  meta = MetricMeta.new("#{type}/#{name}")
57
64
  stat = MetricStats.new(false)
@@ -206,6 +213,8 @@ module ScoutApm
206
213
 
207
214
  attr_reader :db_query_metric_set
208
215
 
216
+ attr_reader :external_service_metric_set
217
+
209
218
  def initialize(timestamp, context)
210
219
  @timestamp = timestamp
211
220
 
@@ -216,6 +225,7 @@ module ScoutApm
216
225
 
217
226
  @metric_set = MetricSet.new
218
227
  @db_query_metric_set = DbQueryMetricSet.new(context)
228
+ @external_service_metric_set = ExternalServiceMetricSet.new(context)
219
229
 
220
230
  @jobs = Hash.new
221
231
  end
@@ -228,7 +238,8 @@ module ScoutApm
228
238
  merge_jobs!(other.jobs).
229
239
  merge_slow_jobs!(other.slow_jobs_payload).
230
240
  merge_histograms!(other.histograms).
231
- merge_db_query_metrics!(other.db_query_metric_set)
241
+ merge_db_query_metrics!(other.db_query_metric_set).
242
+ merge_external_service_metrics!(other.external_service_metric_set)
232
243
  self
233
244
  end
234
245
 
@@ -254,6 +265,15 @@ module ScoutApm
254
265
  self
255
266
  end
256
267
 
268
+ def merge_external_service_metrics!(other_metric_set)
269
+ if other_metric_set.nil?
270
+ logger.debug("Missing other_metric_set for merge_external_service_metrics - skipping.")
271
+ else
272
+ external_service_metric_set.combine!(other_metric_set)
273
+ end
274
+ self
275
+ end
276
+
257
277
  def merge_slow_transactions!(new_transactions)
258
278
  Array(new_transactions).each do |one_transaction|
259
279
  request_traces << one_transaction
@@ -316,6 +336,10 @@ module ScoutApm
316
336
  db_query_metric_set.metrics_to_report
317
337
  end
318
338
 
339
+ def external_service_metrics_payload
340
+ external_service_metric_set.metrics_to_report
341
+ end
342
+
319
343
  #################################
320
344
  # Debug Helpers
321
345
  #################################
@@ -117,7 +117,12 @@ module ScoutApm
117
117
  # Must follow layer.record_stop_time! as the total_call_time is used to determine if the layer is significant.
118
118
  return if layer_insignificant?(layer)
119
119
 
120
- @layers[-1].add_child(layer) if @layers.any?
120
+ # Check that the parent exists before calling a method on it, since some threading can get us into a weird state.
121
+ # this doesn't fix that state, but prevents exceptions from leaking out.
122
+ parent = @layers[-1]
123
+ if parent
124
+ parent.add_child(layer)
125
+ end
121
126
 
122
127
  # This must be called before checking if a backtrace should be collected as the call count influences our capture logic.
123
128
  # We call `#update_call_counts in stop layer to ensure the layer has a final desc. Layer#desc is updated during the AR instrumentation flow.
@@ -315,6 +320,7 @@ module ScoutApm
315
320
  :queue_time => LayerConverters::RequestQueueTimeConverter,
316
321
  :job => LayerConverters::JobConverter,
317
322
  :db => LayerConverters::DatabaseConverter,
323
+ :external_service => LayerConverters::ExternalServiceConverter,
318
324
 
319
325
  :slow_job => LayerConverters::SlowJobConverter,
320
326
  :slow_req => LayerConverters::SlowRequestConverter,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: false
2
+
1
3
  require 'scout_apm/environment'
2
4
 
3
5
  # Removes actual values from SQL. Used to both obfuscate the SQL and group
@@ -10,11 +12,13 @@ module ScoutApm
10
12
 
11
13
  PSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*\z|.freeze
12
14
  PSQL_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
15
+ PSQL_REMOVE_JSON_STRINGS = /:"(?:[^"]|"")*"/.freeze
13
16
  PSQL_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
14
17
  PSQL_AFTER_SELECT = /(?:SELECT\s+).*?(?:WHERE|FROM\z)/im.freeze # Should be everything between a FROM and a WHERE
15
18
  PSQL_PLACEHOLDER = /\$\d+/.freeze
16
19
  PSQL_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
17
20
  PSQL_AFTER_FROM = /(?:FROM\s+).*?(?:WHERE|\z)/im.freeze # Should be everything between a FROM and a WHERE
21
+ PSQL_AFTER_FROM_AS = /(?:FROM\s+).*?(?:AS|\z)/im.freeze # Should be everything between a FROM and AS without WHERE
18
22
  PSQL_AFTER_JOIN = /(?:JOIN\s+).*?\z/im.freeze
19
23
  PSQL_AFTER_WHERE = /(?:WHERE\s+).*?(?:SELECT|\z)/im.freeze
20
24
  PSQL_AFTER_SET = /(?:SET\s+).*?(?:WHERE|\z)/im.freeze
@@ -30,7 +34,8 @@ module ScoutApm
30
34
  SQLITE_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
31
35
 
32
36
  # => "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE (age > 50) ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY', N'@0 int', @0 = 10"
33
- SQLSERVER_EXECUTESQL = /EXEC sp_executesql N'(.*?)'.*/
37
+ SQLSERVER_REMOVE_EXECUTESQL = /EXEC sp_executesql (N')?/.freeze
38
+ SQLSERVER_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
34
39
  SQLSERVER_REMOVE_INTEGERS = /(?<!LIMIT )\b(?<!@)\d+\b/.freeze
35
40
  SQLSERVER_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
36
41
 
@@ -63,7 +68,8 @@ module ScoutApm
63
68
  private
64
69
 
65
70
  def to_s_sqlserver
66
- sql.gsub!(SQLSERVER_EXECUTESQL, '\1')
71
+ sql.gsub!(SQLSERVER_REMOVE_EXECUTESQL, '')
72
+ sql.gsub!(SQLSERVER_REMOVE_STRINGS, '?')
67
73
  sql.gsub!(SQLSERVER_REMOVE_INTEGERS, '?')
68
74
  sql.gsub!(SQLSERVER_IN_CLAUSE, 'IN (?)')
69
75
  sql
@@ -74,6 +80,7 @@ module ScoutApm
74
80
  sql.gsub!(PSQL_VAR_INTERPOLATION, '')
75
81
  # sql.gsub!(PSQL_REMOVE_STRINGS, '?')
76
82
  sql.gsub!(PSQL_AFTER_WHERE) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
83
+ sql.gsub!(PSQL_AFTER_FROM_AS) {|c| c.gsub(PSQL_REMOVE_JSON_STRINGS, ':"?"')}
77
84
  sql.gsub!(PSQL_AFTER_JOIN) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
78
85
  sql.gsub!(PSQL_AFTER_SET) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
79
86
  sql.gsub!(PSQL_REMOVE_INTEGERS, '?')
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "4.1.1"
2
+ VERSION = "5.1.1"
3
3
  end
data/lib/scout_apm.rb CHANGED
@@ -27,6 +27,7 @@ require 'rusage'
27
27
  #####################################
28
28
  require 'scout_apm/version'
29
29
 
30
+ require 'scout_apm/exceptions'
30
31
  require 'scout_apm/debug'
31
32
  require 'scout_apm/tracked_request'
32
33
  require 'scout_apm/layer'
@@ -42,6 +43,7 @@ require 'scout_apm/layer_converters/job_converter'
42
43
  require 'scout_apm/layer_converters/slow_job_converter'
43
44
  require 'scout_apm/layer_converters/metric_converter'
44
45
  require 'scout_apm/layer_converters/database_converter'
46
+ require 'scout_apm/layer_converters/external_service_converter'
45
47
  require 'scout_apm/layer_converters/slow_request_converter'
46
48
  require 'scout_apm/layer_converters/request_queue_time_converter'
47
49
  require 'scout_apm/layer_converters/allocation_metric_converter'
@@ -79,6 +81,7 @@ require 'scout_apm/histogram'
79
81
 
80
82
  require 'scout_apm/instruments/net_http'
81
83
  require 'scout_apm/instruments/http_client'
84
+ require 'scout_apm/instruments/http'
82
85
  require 'scout_apm/instruments/typhoeus'
83
86
  require 'scout_apm/instruments/moped'
84
87
  require 'scout_apm/instruments/mongoid'
@@ -130,6 +133,7 @@ require 'scout_apm/bucket_name_splitter'
130
133
  require 'scout_apm/stack_item'
131
134
  require 'scout_apm/metric_set'
132
135
  require 'scout_apm/db_query_metric_set'
136
+ require 'scout_apm/external_service_metric_set'
133
137
  require 'scout_apm/store'
134
138
  require 'scout_apm/fake_store'
135
139
  require 'scout_apm/tracer'
@@ -142,6 +146,7 @@ require 'scout_apm/synchronous_recorder'
142
146
  require 'scout_apm/metric_meta'
143
147
  require 'scout_apm/metric_stats'
144
148
  require 'scout_apm/db_query_metric_stats'
149
+ require 'scout_apm/external_service_metric_stats'
145
150
  require 'scout_apm/slow_transaction'
146
151
  require 'scout_apm/slow_job_record'
147
152
  require 'scout_apm/detailed_trace'
@@ -167,6 +172,7 @@ require 'scout_apm/serializers/slow_jobs_serializer_to_json'
167
172
  require 'scout_apm/serializers/metrics_to_json_serializer'
168
173
  require 'scout_apm/serializers/histograms_serializer_to_json'
169
174
  require 'scout_apm/serializers/db_query_serializer_to_json'
175
+ require 'scout_apm/serializers/external_service_serializer_to_json'
170
176
  require 'scout_apm/serializers/directive_serializer'
171
177
  require 'scout_apm/serializers/app_server_load_serializer'
172
178
 
@@ -217,8 +223,12 @@ if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR
217
223
  ScoutApm::Agent.instance.install
218
224
 
219
225
  if ScoutApm::Agent.instance.context.config.value("auto_instruments")
220
- ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled.")
221
- require 'scout_apm/auto_instrument'
226
+ if defined?(Parser::TreeRewriter)
227
+ ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled.")
228
+ require 'scout_apm/auto_instrument'
229
+ else # AutoInstruments is turned on, but we don't he the prerequisites to use it
230
+ ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled, but Parser::TreeRewriter is missing. Update 'parser' gem to >= 2.5.0.")
231
+ end
222
232
  else
223
233
  ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is disabled.")
224
234
  end
data/scout_apm.gemspec CHANGED
@@ -10,9 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.homepage = "https://github.com/scoutapp/scout_apm_ruby"
11
11
  s.summary = "Ruby application performance monitoring"
12
12
  s.description = "Monitors Ruby apps and reports detailed metrics on performance to Scout."
13
- s.license = "Proprietary (See LICENSE.md)"
14
-
15
- s.rubyforge_project = "scout_apm"
13
+ s.license = "MIT"
16
14
 
17
15
  s.files = `git ls-files`.split("\n")
18
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,67 @@
1
+ require 'test_helper'
2
+
3
+ require 'scout_apm/external_service_metric_set'
4
+
5
+ module ScoutApm
6
+ class ExternalServiceMetricSetTest < Minitest::Test
7
+ def test_hard_limit
8
+ config = make_fake_config(
9
+ 'external_service_metric_limit' => 5, # The hard limit on db metrics
10
+ 'external_service_metric_report_limit' => 2
11
+ )
12
+ context = ScoutApm::AgentContext.new().tap{|c| c.config = config }
13
+ set = ExternalServiceMetricSet.new(context)
14
+
15
+ set << fake_stat("a", 10)
16
+ set << fake_stat("b", 20)
17
+ set << fake_stat("c", 30)
18
+ set << fake_stat("d", 40)
19
+ set << fake_stat("e", 50)
20
+ set << fake_stat("f", 60)
21
+
22
+ assert_equal 5, set.metrics.size
23
+ end
24
+
25
+ def test_report_limit
26
+ config = make_fake_config(
27
+ 'external_service_metric_limit' => 50, # much larger max, uninterested in hitting it.
28
+ 'external_service_metric_report_limit' => 2
29
+ )
30
+ context = ScoutApm::AgentContext.new().tap{|c| c.config = config }
31
+ set = ExternalServiceMetricSet.new(context)
32
+ set << fake_stat("a", 10)
33
+ set << fake_stat("b", 20)
34
+ set << fake_stat("c", 30)
35
+ set << fake_stat("d", 40)
36
+ set << fake_stat("e", 50)
37
+ set << fake_stat("f", 60)
38
+
39
+ assert_equal 2, set.metrics_to_report.size
40
+ assert_equal ["f","e"], set.metrics_to_report.map{|m| m.key}
41
+ end
42
+
43
+ def test_combine
44
+ config = make_fake_config(
45
+ 'external_service_metric_limit' => 5, # The hard limit on db metrics
46
+ 'external_service_metric_report_limit' => 2
47
+ )
48
+ context = ScoutApm::AgentContext.new().tap{|c| c.config = config }
49
+ set1 = ExternalServiceMetricSet.new(context)
50
+ set1 << fake_stat("a", 10)
51
+ set1 << fake_stat("b", 20)
52
+ set2 = ExternalServiceMetricSet.new(context)
53
+ set2 << fake_stat("c", 10)
54
+ set2 << fake_stat("d", 20)
55
+
56
+ combined = set1.combine!(set2)
57
+ assert_equal ["a", "b", "c", "d"], combined.metrics.map{|_k, m| m.key}.sort
58
+ end
59
+
60
+ def fake_stat(key, call_time)
61
+ OpenStruct.new(
62
+ :key => key,
63
+ :call_time => call_time
64
+ )
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,106 @@
1
+ require 'test_helper'
2
+
3
+ require 'scout_apm/external_service_metric_stats'
4
+
5
+ module ScoutApm
6
+ class ExternalServiceMetricStatsTest < Minitest::Test
7
+ def test_as_json_empty_stats
8
+ stat = build("example.com", "GET", "Controller/public/index", 1, 10)
9
+
10
+ assert_equal({
11
+ :domain_name => "example.com",
12
+ :operation => "GET",
13
+ :call_count => 1,
14
+ :transaction_count => 0,
15
+ :scope => "Controller/public/index",
16
+ :histogram => [[10.0, 1]],
17
+
18
+ :max_call_time => 10.0,
19
+ :min_call_time => 10.0,
20
+ :call_time => 10.0
21
+ }, stat.as_json)
22
+ end
23
+
24
+ def test_increment_transaction_count
25
+ stat = build()
26
+ assert_equal 0, stat.transaction_count
27
+
28
+ stat.increment_transaction_count!
29
+
30
+ assert_equal 1, stat.transaction_count
31
+ end
32
+
33
+ def test_key_name
34
+ stat = build("example.com", "GET", "Controller/public/index")
35
+ assert_equal ["example.com", "GET", "Controller/public/index"], stat.key
36
+ end
37
+
38
+ def test_combine_min_call_time_picks_smallest
39
+ stat1, stat2 = build_pair
40
+ assert_equal 5.1, stat1.combine!(stat2).min_call_time
41
+ end
42
+
43
+ def test_combine_max_call_time_picks_largest
44
+ stat1, stat2 = build_pair
45
+ assert_equal 8.2, stat1.combine!(stat2).max_call_time
46
+ end
47
+
48
+ def test_combine_call_counts_adds
49
+ stat1, stat2 = build_pair
50
+ assert_equal 5, stat1.combine!(stat2).call_count
51
+ end
52
+
53
+ def test_combine_transaction_count_adds
54
+ stat1, stat2 = build_pair
55
+ 2.times { stat1.increment_transaction_count! }
56
+ 3.times { stat2.increment_transaction_count! }
57
+
58
+ assert_equal 5, stat1.combine!(stat2).call_count
59
+ end
60
+
61
+ def test_combine_doesnt_merge_with_self
62
+ stat = build
63
+ merged = stat.combine!(stat)
64
+
65
+ assert_equal DEFAULTS[:call_count], merged.call_count
66
+ assert_equal DEFAULTS[:call_time], merged.call_time
67
+ end
68
+
69
+ # A.combine!(B) should be the the same as B.combine!(A)
70
+ # Have to be a bit careful, since combine! is destructive, so make two pairs
71
+ # with same data to do both sides, then check that they result in the same
72
+ # answer
73
+ [:transaction_count, :call_count, :max_call_time, :min_call_time].each do |attr|
74
+ define_method :"test_combine_#{attr}_is_symmetric" do
75
+ stat1_a, stat2_a = build_pair
76
+ stat1_b, stat2_b = build_pair
77
+ merged_a = stat1_a.combine!(stat2_a)
78
+ merged_b = stat2_b.combine!(stat1_b)
79
+
80
+ assert_equal merged_a.send(attr), merged_b.send(attr)
81
+ end
82
+ end
83
+
84
+ #############
85
+ # Helpers #
86
+ #############
87
+ DEFAULTS = {
88
+ :call_count => 1,
89
+ :call_time => 10.0,
90
+ }
91
+
92
+ def build(domain_name="example.com",
93
+ operation="GET",
94
+ scope="Controller/public/index",
95
+ call_count=DEFAULTS[:call_count],
96
+ call_time=DEFAULTS[:call_time])
97
+ ExternalServiceMetricStats.new(domain_name, operation, scope, call_count, call_time)
98
+ end
99
+
100
+ def build_pair
101
+ stat1 = build("example.com", "GET", "Controller/public/index", 2, 5.1)
102
+ stat2 = build("example.com", "GET", "Controller/public/index", 3, 8.2)
103
+ [stat1, stat2]
104
+ end
105
+ end
106
+ end
@@ -8,7 +8,7 @@ class PayloadSerializerTest < Minitest::Test
8
8
  :unique_id => "unique_idz",
9
9
  :agent_version => 123
10
10
  }
11
- payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {}, [], [], [], {}, [])
11
+ payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {}, [], [], [], {}, {}, [])
12
12
 
13
13
  # symbol keys turn to strings
14
14
  formatted_metadata = {
@@ -49,7 +49,7 @@ class PayloadSerializerTest < Minitest::Test
49
49
  stats.total_exclusive_time = 0.078132088
50
50
  }
51
51
  }
52
- payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize({}, metrics, {}, [], [], [], {}, [])
52
+ payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize({}, metrics, {}, [], [], [], {}, {}, [])
53
53
  formatted_metrics = [
54
54
  {
55
55
  "key" => {
@@ -94,7 +94,7 @@ class PayloadSerializerTest < Minitest::Test
94
94
  :quotie => "here are some \"quotes\"",
95
95
  :payload_version => 2,
96
96
  }
97
- payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {}, [], [], [], {}, [])
97
+ payload = ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, {}, {}, [], [], [], {}, {}, [])
98
98
 
99
99
  # symbol keys turn to strings
100
100
  formatted_metadata = {
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: false
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  module ScoutApm
@@ -121,27 +123,27 @@ module ScoutApm
121
123
  end
122
124
 
123
125
  def test_sqlserver_integers
124
- skip "SQLServer Support requires Ruby 1.9+ For Regexes"
125
-
126
126
  sql = "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE (age > 50) ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY', N'@0 int', @0 = 10"
127
127
  ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :sqlserver }
128
- assert_equal %q|SELECT [users].* FROM [users] WHERE (age > ?) ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @0 ROWS ONLY|, ss.to_s
128
+ assert_equal "SELECT [users].* FROM [users] WHERE (age > ?) ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @0 ROWS ONLY?@0 int', @0 = ?", ss.to_s
129
129
  end
130
130
 
131
131
  def test_sqlserver_strings
132
- skip "SQLServer Support requires Ruby 1.9+ For Regexes"
132
+ sql = "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE first_name = N'john' AND last_name = N'doe' ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY', N'@0 int', @0 = 10"
133
+ ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :sqlserver }
134
+ assert_equal "SELECT [users].* FROM [users] WHERE first_name = N? AND last_name = N? ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @0 ROWS ONLY?@0 int', @0 = ?", ss.to_s
135
+ end
133
136
 
134
- sql = "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE [users].[email] = @0 ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @1 ROWS ONLY', N'@0 nvarchar(4000), @1 int', @0 = N'foo', @1 = 10"
137
+ def test_sqlserver_strings_no_executesql
138
+ sql = "EXEC Authenticate @username = N'abraham.lincoln', @password = N'somepassword!', @token = NULL, @app_name = N'Central Auth Service', @log_login = true, @ip_address = N'127.0.0.1', @external_type = NULL, @external_success = NULL"
135
139
  ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :sqlserver }
136
- assert_equal %q|SELECT [users].* FROM [users] WHERE [users].[email] = @0 ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @1 ROWS ONLY|, ss.to_s
140
+ assert_equal "EXEC Authenticate @username = N?, @password = N?, @token = NULL, @app_name = N?, @log_login = true, @ip_address = N?, @external_type = NULL, @external_success = NULL", ss.to_s
137
141
  end
138
142
 
139
143
  def test_sqlserver_in_clause
140
- skip "SQLServer Support requires Ruby 1.9+ For Regexes"
141
-
142
144
  sql = "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE (id IN (1,2,3)) ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY', N'@0 int', @0 = 10"
143
145
  ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :sqlserver }
144
- assert_equal %q|SELECT [users].* FROM [users] WHERE (id IN (?)) ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @0 ROWS ONLY|, ss.to_s
146
+ assert_equal "SELECT [users].* FROM [users] WHERE (id IN (?)) ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @0 ROWS ONLY?@0 int', @0 = ?", ss.to_s
145
147
  end
146
148
 
147
149
  def test_scrubs_invalid_encoding
@@ -192,6 +194,17 @@ module ScoutApm
192
194
  WHERE (title = ?)|, ss.to_s
193
195
  end
194
196
 
197
+ def test_postgres_insert_select_from_jsonb_to_recordset_with_as
198
+ sql = %q|
199
+ INSERT INTO foos(foo_id, bar_id, external_id, email_address, created_at, updated_at)
200
+ SELECT 123, 456, external_id, email_address, NOW(), NOW()
201
+ FROM jsonb_to_recordset($${"items":[{"external_id":1234,"email_address":"test@domain.com"}]}$$::jsonb->'items')
202
+ AS t(external_id integer, email_address varchar)
203
+ |
204
+ ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
205
+ assert_equal %q|INSERT INTO foos(foo_id, bar_id, external_id, email_address, created_at, updated_at) SELECT ?, ?, external_id, email_address, NOW(), NOW() FROM jsonb_to_recordset($${"items":[{"external_id":?,"email_address":"?"}]}$$::jsonb->'items') AS t(external_id integer, email_address varchar)|, ss.to_s
206
+ end
207
+
195
208
  def assert_faster_than(target_seconds)
196
209
  t1 = ::Time.now
197
210
  yield
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: 4.1.1
4
+ version: 5.1.1
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: 2021-06-08 00:00:00.000000000 Z
12
+ date: 2021-12-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
@@ -219,7 +219,6 @@ files:
219
219
  - ".github/workflows/test.yml"
220
220
  - ".gitignore"
221
221
  - ".rubocop.yml"
222
- - ".travis.yml"
223
222
  - CHANGELOG.markdown
224
223
  - Gemfile
225
224
  - Guardfile
@@ -281,8 +280,11 @@ files:
281
280
  - lib/scout_apm/error_service/periodic_work.rb
282
281
  - lib/scout_apm/error_service/railtie.rb
283
282
  - lib/scout_apm/error_service/sidekiq.rb
283
+ - lib/scout_apm/exceptions.rb
284
284
  - lib/scout_apm/extensions/config.rb
285
285
  - lib/scout_apm/extensions/transaction_callback_payload.rb
286
+ - lib/scout_apm/external_service_metric_set.rb
287
+ - lib/scout_apm/external_service_metric_stats.rb
286
288
  - lib/scout_apm/fake_store.rb
287
289
  - lib/scout_apm/framework_integrations/rails_2.rb
288
290
  - lib/scout_apm/framework_integrations/rails_3_or_4.rb
@@ -329,6 +331,7 @@ files:
329
331
  - lib/scout_apm/layer_converters/database_converter.rb
330
332
  - lib/scout_apm/layer_converters/depth_first_walker.rb
331
333
  - lib/scout_apm/layer_converters/error_converter.rb
334
+ - lib/scout_apm/layer_converters/external_service_converter.rb
332
335
  - lib/scout_apm/layer_converters/find_layer_by_type.rb
333
336
  - lib/scout_apm/layer_converters/histograms.rb
334
337
  - lib/scout_apm/layer_converters/job_converter.rb
@@ -360,6 +363,7 @@ files:
360
363
  - lib/scout_apm/serializers/app_server_load_serializer.rb
361
364
  - lib/scout_apm/serializers/db_query_serializer_to_json.rb
362
365
  - lib/scout_apm/serializers/directive_serializer.rb
366
+ - lib/scout_apm/serializers/external_service_serializer_to_json.rb
363
367
  - lib/scout_apm/serializers/histograms_serializer_to_json.rb
364
368
  - lib/scout_apm/serializers/jobs_serializer_to_json.rb
365
369
  - lib/scout_apm/serializers/metrics_to_json_serializer.rb
@@ -429,6 +433,8 @@ files:
429
433
  - test/unit/error_service/ignored_exceptions_test.rb
430
434
  - test/unit/extensions/periodic_callbacks_test.rb
431
435
  - test/unit/extensions/transaction_callbacks_test.rb
436
+ - test/unit/external_service_metric_set_test.rb
437
+ - test/unit/external_service_metric_stats_test.rb
432
438
  - test/unit/fake_store_test.rb
433
439
  - test/unit/git_revision_test.rb
434
440
  - test/unit/histogram_test.rb
@@ -464,7 +470,7 @@ files:
464
470
  - test/unit/utils/scm.rb
465
471
  homepage: https://github.com/scoutapp/scout_apm_ruby
466
472
  licenses:
467
- - Proprietary (See LICENSE.md)
473
+ - MIT
468
474
  metadata: {}
469
475
  post_install_message:
470
476
  rdoc_options: []
@@ -482,7 +488,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
482
488
  - !ruby/object:Gem::Version
483
489
  version: '0'
484
490
  requirements: []
485
- rubygems_version: 3.1.2
491
+ rubygems_version: 3.0.3
486
492
  signing_key:
487
493
  specification_version: 4
488
494
  summary: Ruby application performance monitoring
data/.travis.yml DELETED
@@ -1,25 +0,0 @@
1
- language: ruby
2
- dist: xenial
3
- cache: bundler
4
-
5
- matrix:
6
- include:
7
- - rvm: 2.1
8
- gemfile: gems/rails3.gemfile
9
- - rvm: 2.2
10
- - rvm: 2.3
11
- - rvm: 2.4
12
- - rvm: 2.5
13
- - rvm: 2.6
14
- - rvm: 2.7
15
- - rvm: 3.0
16
- - rvm: 2.6
17
- gemfile: gems/typhoeus.gemfile
18
- env: "SCOUT_TEST_FEATURES=typhoeus"
19
- - rvm: 2.6
20
- gemfile: gems/octoshark.gemfile
21
- - rvm: 2.6
22
- name: rubocop yo
23
- script: bundle exec rubocop
24
- - rvm: 2.6
25
- gemfile: gems/rails3.gemfile