scout_apm 4.0.4 → 5.0.0

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