scout_apm 4.0.4 → 5.0.0

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +4 -0
  3. data/CHANGELOG.markdown +33 -0
  4. data/LICENSE.md +21 -28
  5. data/gems/typhoeus.gemfile +3 -0
  6. data/lib/scout_apm/agent/preconditions.rb +3 -3
  7. data/lib/scout_apm/auto_instrument/rails.rb +0 -1
  8. data/lib/scout_apm/background_job_integrations/shoryuken.rb +2 -0
  9. data/lib/scout_apm/background_job_integrations/sidekiq.rb +13 -2
  10. data/lib/scout_apm/config.rb +10 -0
  11. data/lib/scout_apm/error_service/middleware.rb +2 -2
  12. data/lib/scout_apm/error_service/payload.rb +1 -1
  13. data/lib/scout_apm/error_service.rb +3 -1
  14. data/lib/scout_apm/external_service_metric_set.rb +97 -0
  15. data/lib/scout_apm/external_service_metric_stats.rb +85 -0
  16. data/lib/scout_apm/fake_store.rb +3 -0
  17. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +5 -1
  18. data/lib/scout_apm/git_revision.rb +9 -0
  19. data/lib/scout_apm/ignored_uris.rb +3 -1
  20. data/lib/scout_apm/instant/middleware.rb +2 -0
  21. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +5 -2
  22. data/lib/scout_apm/instruments/action_view.rb +13 -5
  23. data/lib/scout_apm/instruments/active_record.rb +2 -0
  24. data/lib/scout_apm/instruments/elasticsearch.rb +2 -0
  25. data/lib/scout_apm/instruments/sinatra.rb +2 -0
  26. data/lib/scout_apm/instruments/typhoeus.rb +8 -6
  27. data/lib/scout_apm/layer_converters/external_service_converter.rb +65 -0
  28. data/lib/scout_apm/layer_converters/find_layer_by_type.rb +4 -0
  29. data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +2 -0
  30. data/lib/scout_apm/logger.rb +2 -2
  31. data/lib/scout_apm/reporting.rb +2 -1
  32. data/lib/scout_apm/serializers/external_service_serializer_to_json.rb +15 -0
  33. data/lib/scout_apm/serializers/payload_serializer.rb +4 -3
  34. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +4 -1
  35. data/lib/scout_apm/store.rb +21 -1
  36. data/lib/scout_apm/tracked_request.rb +7 -1
  37. data/lib/scout_apm/utils/sql_sanitizer.rb +32 -6
  38. data/lib/scout_apm/version.rb +1 -1
  39. data/lib/scout_apm.rb +5 -0
  40. data/scout_apm.gemspec +0 -2
  41. data/test/unit/auto_instrument/anonymous_block_value.rb +7 -0
  42. data/test/unit/auto_instrument/hanging_method.rb +6 -0
  43. data/test/unit/auto_instrument_test.rb +8 -0
  44. data/test/unit/external_service_metric_set_test.rb +67 -0
  45. data/test/unit/external_service_metric_stats_test.rb +106 -0
  46. data/test/unit/ignored_uris_test.rb +6 -0
  47. data/test/unit/instruments/typhoeus_test.rb +42 -0
  48. data/test/unit/remote/{test_message.rb → message_test.rb} +0 -0
  49. data/test/unit/remote/{test_router.rb → route_test.rb} +0 -0
  50. data/test/unit/remote/{test_server.rb → server_test.rb} +4 -1
  51. data/test/unit/serializers/payload_serializer_test.rb +3 -3
  52. data/test/unit/sql_sanitizer_test.rb +19 -2
  53. metadata +16 -9
  54. data/.travis.yml +0 -22
  55. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +0 -32
  56. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +0 -32
@@ -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
@@ -5,6 +5,10 @@
5
5
  # show
6
6
  # render :update
7
7
  # end
8
+
9
+ # This doesn't cache the negative result when searching for a controller / job,
10
+ # so that we can ask again later after more of the request has occurred and
11
+ # correctly find it.
8
12
  module ScoutApm
9
13
  module LayerConverters
10
14
  class FindLayerByType
@@ -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
@@ -69,14 +69,14 @@ module ScoutApm
69
69
  private
70
70
 
71
71
  def build_logger
72
- logger_class.new(@log_destination)
72
+ logger_class.new(@log_destination) rescue logger_class.new
73
73
  end
74
74
 
75
75
  def logger_class
76
76
  klass = @opts.fetch(:logger_class, ::Logger)
77
77
  case klass
78
78
  when String
79
- result = KlassHelper.lookup(klass)
79
+ result = Utils::KlassHelper.lookup(klass)
80
80
  if result == :missing_class
81
81
  ::Logger
82
82
  else
@@ -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,11 @@ module ScoutApm
254
265
  self
255
266
  end
256
267
 
268
+ def merge_external_service_metrics!(other_metric_set)
269
+ external_service_metric_set.combine!(other_metric_set)
270
+ self
271
+ end
272
+
257
273
  def merge_slow_transactions!(new_transactions)
258
274
  Array(new_transactions).each do |one_transaction|
259
275
  request_traces << one_transaction
@@ -316,6 +332,10 @@ module ScoutApm
316
332
  db_query_metric_set.metrics_to_report
317
333
  end
318
334
 
335
+ def external_service_metrics_payload
336
+ external_service_metric_set.metrics_to_report
337
+ end
338
+
319
339
  #################################
320
340
  # Debug Helpers
321
341
  #################################
@@ -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
@@ -5,12 +7,34 @@ require 'scout_apm/environment'
5
7
  module ScoutApm
6
8
  module Utils
7
9
  class SqlSanitizer
8
- if ScoutApm::Environment.instance.ruby_187?
9
- require 'scout_apm/utils/sql_sanitizer_regex_1_8_7'
10
- else
11
- require 'scout_apm/utils/sql_sanitizer_regex'
12
- end
13
- include ScoutApm::Utils::SqlRegex
10
+ MULTIPLE_SPACES = %r|\s+|.freeze
11
+ MULTIPLE_QUESTIONS = /\?(,\?)+/.freeze
12
+
13
+ PSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*\z|.freeze
14
+ PSQL_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
15
+ PSQL_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
16
+ PSQL_AFTER_SELECT = /(?:SELECT\s+).*?(?:WHERE|FROM\z)/im.freeze # Should be everything between a FROM and a WHERE
17
+ PSQL_PLACEHOLDER = /\$\d+/.freeze
18
+ PSQL_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
19
+ PSQL_AFTER_FROM = /(?:FROM\s+).*?(?:WHERE|\z)/im.freeze # Should be everything between a FROM and a WHERE
20
+ PSQL_AFTER_JOIN = /(?:JOIN\s+).*?\z/im.freeze
21
+ PSQL_AFTER_WHERE = /(?:WHERE\s+).*?(?:SELECT|\z)/im.freeze
22
+ PSQL_AFTER_SET = /(?:SET\s+).*?(?:WHERE|\z)/im.freeze
23
+
24
+ MYSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
25
+ MYSQL_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
26
+ MYSQL_REMOVE_SINGLE_QUOTE_STRINGS = %r{'(?:\\'|[^']|'')*'}.freeze
27
+ MYSQL_REMOVE_DOUBLE_QUOTE_STRINGS = %r{"(?:\\"|[^"]|"")*"}.freeze
28
+ MYSQL_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
29
+
30
+ SQLITE_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
31
+ SQLITE_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
32
+ SQLITE_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
33
+
34
+ # => "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"
35
+ SQLSERVER_EXECUTESQL = /EXEC sp_executesql N'(.*?)'.*/
36
+ SQLSERVER_REMOVE_INTEGERS = /(?<!LIMIT )\b(?<!@)\d+\b/.freeze
37
+ SQLSERVER_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
14
38
 
15
39
  attr_accessor :database_engine
16
40
 
@@ -50,7 +74,9 @@ module ScoutApm
50
74
  def to_s_postgres
51
75
  sql.gsub!(PSQL_PLACEHOLDER, '?')
52
76
  sql.gsub!(PSQL_VAR_INTERPOLATION, '')
77
+ # sql.gsub!(PSQL_REMOVE_STRINGS, '?')
53
78
  sql.gsub!(PSQL_AFTER_WHERE) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
79
+ sql.gsub!(PSQL_AFTER_JOIN) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
54
80
  sql.gsub!(PSQL_AFTER_SET) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
55
81
  sql.gsub!(PSQL_REMOVE_INTEGERS, '?')
56
82
  sql.gsub!(PSQL_IN_CLAUSE, 'IN (?)')
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "4.0.4"
2
+ VERSION = "5.0.0"
3
3
  end
data/lib/scout_apm.rb CHANGED
@@ -42,6 +42,7 @@ require 'scout_apm/layer_converters/job_converter'
42
42
  require 'scout_apm/layer_converters/slow_job_converter'
43
43
  require 'scout_apm/layer_converters/metric_converter'
44
44
  require 'scout_apm/layer_converters/database_converter'
45
+ require 'scout_apm/layer_converters/external_service_converter'
45
46
  require 'scout_apm/layer_converters/slow_request_converter'
46
47
  require 'scout_apm/layer_converters/request_queue_time_converter'
47
48
  require 'scout_apm/layer_converters/allocation_metric_converter'
@@ -130,6 +131,7 @@ require 'scout_apm/bucket_name_splitter'
130
131
  require 'scout_apm/stack_item'
131
132
  require 'scout_apm/metric_set'
132
133
  require 'scout_apm/db_query_metric_set'
134
+ require 'scout_apm/external_service_metric_set'
133
135
  require 'scout_apm/store'
134
136
  require 'scout_apm/fake_store'
135
137
  require 'scout_apm/tracer'
@@ -142,6 +144,7 @@ require 'scout_apm/synchronous_recorder'
142
144
  require 'scout_apm/metric_meta'
143
145
  require 'scout_apm/metric_stats'
144
146
  require 'scout_apm/db_query_metric_stats'
147
+ require 'scout_apm/external_service_metric_stats'
145
148
  require 'scout_apm/slow_transaction'
146
149
  require 'scout_apm/slow_job_record'
147
150
  require 'scout_apm/detailed_trace'
@@ -167,6 +170,7 @@ require 'scout_apm/serializers/slow_jobs_serializer_to_json'
167
170
  require 'scout_apm/serializers/metrics_to_json_serializer'
168
171
  require 'scout_apm/serializers/histograms_serializer_to_json'
169
172
  require 'scout_apm/serializers/db_query_serializer_to_json'
173
+ require 'scout_apm/serializers/external_service_serializer_to_json'
170
174
  require 'scout_apm/serializers/directive_serializer'
171
175
  require 'scout_apm/serializers/app_server_load_serializer'
172
176
 
@@ -193,6 +197,7 @@ require 'scout_apm/tasks/support'
193
197
  require 'scout_apm/extensions/config'
194
198
  require 'scout_apm/extensions/transaction_callback_payload'
195
199
 
200
+ require 'scout_apm/error'
196
201
  require 'scout_apm/error_service'
197
202
  require 'scout_apm/error_service/middleware'
198
203
  require 'scout_apm/error_service/notifier'
data/scout_apm.gemspec CHANGED
@@ -12,8 +12,6 @@ Gem::Specification.new do |s|
12
12
  s.description = "Monitors Ruby apps and reports detailed metrics on performance to Scout."
13
13
  s.license = "Proprietary (See LICENSE.md)"
14
14
 
15
- s.rubyforge_project = "scout_apm"
16
-
17
15
  s.files = `git ls-files`.split("\n")
18
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -0,0 +1,7 @@
1
+ class TestController < ApplicationController
2
+ def index
3
+ quests = policy_scope(Quest.open)
4
+ quests.each { _1.current_user = current_user }
5
+ respond_with_proto quests
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ class TestController < ApplicationController
2
+ end
3
+
4
+ def hanging_method
5
+ Test.first
6
+ end
@@ -51,4 +51,12 @@ class AutoInstrumentTest < Minitest::Test
51
51
  assert_equal instrumented_source("assignments"),
52
52
  normalize_backtrace(::ScoutApm::AutoInstrument::Rails.rewrite(source_path("assignments")))
53
53
  end
54
+
55
+ def test_hanging_method_rewrite
56
+ ::ScoutApm::AutoInstrument::Rails.rewrite(source_path("hanging_method"))
57
+ end
58
+
59
+ def test_anonymous_block_value
60
+ ::ScoutApm::AutoInstrument::Rails.rewrite(source_path("anonymous_block_value"))
61
+ end
54
62
  end if defined? ScoutApm::AutoInstrument
@@ -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
@@ -13,4 +13,10 @@ class IgnoredUrlsTest < Minitest::Test
13
13
  i = ScoutApm::IgnoredUris.new(["/slow", "/health"])
14
14
  assert_equal false, i.ignore?("/users/2/health")
15
15
  end
16
+
17
+ def test_does_not_ignore_empty_string
18
+ i = ScoutApm::IgnoredUris.new(["", "/admin"])
19
+ assert_equal false, i.ignore?("/users/2/health")
20
+ assert_equal true, i.ignore?("/admin/dashboard")
21
+ end
16
22
  end
@@ -0,0 +1,42 @@
1
+ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("typhoeus")
2
+ require 'test_helper'
3
+
4
+ require 'scout_apm/instruments/typhoeus'
5
+
6
+ require 'typhoeus'
7
+
8
+ class TyphoeusTest < Minitest::Test
9
+ def setup
10
+ @context = ScoutApm::AgentContext.new
11
+ @recorder = FakeRecorder.new
12
+ ScoutApm::Agent.instance.context.recorder = @recorder
13
+ ScoutApm::Instruments::Typhoeus.new(@context).install
14
+ end
15
+
16
+ def test_instruments_typhoeus_hydra
17
+ hydra = Typhoeus::Hydra.new
18
+ 2.times.map{ hydra.queue(Typhoeus::Request.new("example.com", followlocation: true)) }
19
+
20
+ assert_equal "2 requests", hydra.scout_desc
21
+
22
+ hydra.run
23
+ assert_equal "0 requests", hydra.scout_desc
24
+ assert_recorded(@recorder, "HTTP", "Hydra", "2 requests")
25
+ end
26
+
27
+ def test_instruments_typhoeus
28
+ Typhoeus.get("example.com", followlocation: true)
29
+ assert_recorded(@recorder, "HTTP", "get", "example.com")
30
+ end
31
+
32
+ private
33
+
34
+ def assert_recorded(recorder, type, name, desc = nil)
35
+ req = recorder.requests.first
36
+ assert req, "recorder recorded no layers"
37
+ assert_equal type, req.root_layer.type
38
+ assert_equal name, req.root_layer.name
39
+ assert_equal desc, req.root_layer.desc if !desc.nil?
40
+ end
41
+ end
42
+ end
File without changes
@@ -8,8 +8,11 @@ class TestRemoteServer < Minitest::Test
8
8
  logger_io = StringIO.new
9
9
  server = ScoutApm::Remote::Server.new(bind, port, router, Logger.new(logger_io))
10
10
 
11
+ # Cannot test this if we can't require webrick. Ruby 3 stopped including by default
12
+ skip unless server.require_webrick
13
+
11
14
  server.start
12
- sleep 0.01 # Let the server finish starting. The assert should instead allow a time
15
+ sleep 0.05 # Let the server finish starting. The assert should instead allow a time
13
16
  assert server.running?
14
17
  end
15
18
  end