scout_apm 3.0.0.pre13 → 3.0.0.pre14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/lib/scout_apm.rb +20 -10
  3. data/lib/scout_apm/agent.rb +114 -319
  4. data/lib/scout_apm/agent/exit_handler.rb +66 -0
  5. data/lib/scout_apm/agent/preconditions.rb +69 -0
  6. data/lib/scout_apm/agent_context.rb +234 -0
  7. data/lib/scout_apm/app_server_load.rb +24 -14
  8. data/lib/scout_apm/background_job_integrations/resque.rb +7 -8
  9. data/lib/scout_apm/background_job_integrations/sidekiq.rb +2 -2
  10. data/lib/scout_apm/background_recorder.rb +8 -3
  11. data/lib/scout_apm/background_worker.rb +14 -7
  12. data/lib/scout_apm/config.rb +35 -26
  13. data/lib/scout_apm/context.rb +11 -4
  14. data/lib/scout_apm/db_query_metric_set.rb +17 -5
  15. data/lib/scout_apm/debug.rb +1 -1
  16. data/lib/scout_apm/environment.rb +10 -14
  17. data/lib/scout_apm/framework_integrations/sinatra.rb +1 -1
  18. data/lib/scout_apm/git_revision.rb +13 -8
  19. data/lib/scout_apm/histogram.rb +1 -1
  20. data/lib/scout_apm/instant/middleware.rb +7 -7
  21. data/lib/scout_apm/instant_reporting.rb +7 -7
  22. data/lib/scout_apm/instrument_manager.rb +87 -0
  23. data/lib/scout_apm/instruments/action_controller_rails_2.rb +12 -7
  24. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +17 -12
  25. data/lib/scout_apm/instruments/action_view.rb +11 -7
  26. data/lib/scout_apm/instruments/active_record.rb +25 -11
  27. data/lib/scout_apm/instruments/elasticsearch.rb +10 -6
  28. data/lib/scout_apm/instruments/grape.rb +12 -8
  29. data/lib/scout_apm/instruments/http_client.rb +10 -6
  30. data/lib/scout_apm/instruments/influxdb.rb +10 -6
  31. data/lib/scout_apm/instruments/middleware_detailed.rb +11 -5
  32. data/lib/scout_apm/instruments/middleware_summary.rb +11 -5
  33. data/lib/scout_apm/instruments/mongoid.rb +10 -5
  34. data/lib/scout_apm/instruments/moped.rb +11 -6
  35. data/lib/scout_apm/instruments/net_http.rb +10 -6
  36. data/lib/scout_apm/instruments/percentile_sampler.rb +8 -6
  37. data/lib/scout_apm/instruments/process/process_cpu.rb +8 -4
  38. data/lib/scout_apm/instruments/process/process_memory.rb +15 -10
  39. data/lib/scout_apm/instruments/rails_router.rb +12 -6
  40. data/lib/scout_apm/instruments/redis.rb +10 -6
  41. data/lib/scout_apm/instruments/samplers.rb +11 -0
  42. data/lib/scout_apm/instruments/sinatra.rb +5 -4
  43. data/lib/scout_apm/layaway.rb +21 -20
  44. data/lib/scout_apm/layaway_file.rb +8 -3
  45. data/lib/scout_apm/layer.rb +3 -3
  46. data/lib/scout_apm/layer_converters/converter_base.rb +6 -7
  47. data/lib/scout_apm/layer_converters/database_converter.rb +1 -1
  48. data/lib/scout_apm/layer_converters/histograms.rb +2 -2
  49. data/lib/scout_apm/layer_converters/slow_job_converter.rb +4 -3
  50. data/lib/scout_apm/layer_converters/slow_request_converter.rb +5 -4
  51. data/lib/scout_apm/logger.rb +143 -0
  52. data/lib/scout_apm/middleware.rb +7 -9
  53. data/lib/scout_apm/periodic_work.rb +28 -0
  54. data/lib/scout_apm/reporter.rb +14 -8
  55. data/lib/scout_apm/reporting.rb +135 -0
  56. data/lib/scout_apm/request_manager.rb +4 -6
  57. data/lib/scout_apm/serializers/payload_serializer.rb +1 -1
  58. data/lib/scout_apm/slow_job_policy.rb +6 -2
  59. data/lib/scout_apm/slow_job_record.rb +5 -5
  60. data/lib/scout_apm/slow_request_policy.rb +6 -2
  61. data/lib/scout_apm/slow_transaction.rb +5 -5
  62. data/lib/scout_apm/store.rb +22 -16
  63. data/lib/scout_apm/synchronous_recorder.rb +7 -3
  64. data/lib/scout_apm/tasks/doctor.rb +75 -0
  65. data/lib/scout_apm/tasks/support.rb +22 -0
  66. data/lib/scout_apm/tracer.rb +5 -5
  67. data/lib/scout_apm/tracked_request.rb +43 -19
  68. data/lib/scout_apm/utils/active_record_metric_name.rb +66 -8
  69. data/lib/scout_apm/utils/backtrace_parser.rb +1 -1
  70. data/lib/scout_apm/utils/installed_gems.rb +7 -3
  71. data/lib/scout_apm/utils/klass_helper.rb +8 -2
  72. data/lib/scout_apm/utils/scm.rb +1 -1
  73. data/lib/scout_apm/utils/sql_sanitizer.rb +3 -3
  74. data/lib/scout_apm/version.rb +1 -1
  75. data/lib/tasks/doctor.rake +11 -0
  76. data/scout_apm.gemspec +1 -0
  77. data/test/test_helper.rb +17 -2
  78. data/test/unit/agent_test.rb +1 -54
  79. data/test/unit/config_test.rb +9 -5
  80. data/test/unit/context_test.rb +4 -4
  81. data/test/unit/db_query_metric_set_test.rb +11 -4
  82. data/test/unit/fake_store_test.rb +1 -1
  83. data/test/unit/git_revision_test.rb +3 -3
  84. data/test/unit/instruments/net_http_test.rb +2 -1
  85. data/test/unit/instruments/percentile_sampler_test.rb +5 -9
  86. data/test/unit/layaway_test.rb +10 -5
  87. data/test/unit/layer_converters/metric_converter_test.rb +2 -2
  88. data/test/unit/slow_request_policy_test.rb +7 -3
  89. data/test/unit/sql_sanitizer_test.rb +0 -6
  90. data/test/unit/store_test.rb +11 -8
  91. data/test/unit/utils/active_record_metric_name_test.rb +45 -7
  92. metadata +27 -5
  93. data/lib/scout_apm/agent/logging.rb +0 -74
  94. data/lib/scout_apm/agent/reporting.rb +0 -129
  95. data/lib/scout_apm/utils/null_logger.rb +0 -13
@@ -4,10 +4,14 @@
4
4
 
5
5
  module ScoutApm
6
6
  class SynchronousRecorder
7
- attr_reader :logger
7
+ attr_reader :context
8
8
 
9
- def initialize(logger)
10
- @logger = logger
9
+ def initialize(context)
10
+ @context = context
11
+ end
12
+
13
+ def logger
14
+ context.logger
11
15
  end
12
16
 
13
17
  def start
@@ -0,0 +1,75 @@
1
+ module ScoutApm
2
+ module Tasks
3
+ class Doctor
4
+ def self.run!
5
+ new.run!
6
+ end
7
+
8
+ def initialize()
9
+ end
10
+
11
+ def run!
12
+ puts "Scout Doctor"
13
+ puts "============"
14
+ puts
15
+ puts "Detected App Server: #{agent_context.environment.app_server_integration.name}"
16
+ # puts "Detected Background Job: #{agent_context.environment.background_job_integration.name}"
17
+ puts
18
+ puts "Instruments:"
19
+ puts "----------------------------------------"
20
+ puts installed_instruments
21
+ puts
22
+ puts
23
+ puts "Configuration Settings:"
24
+ puts "-------------|------------------------------|-------"
25
+ puts " From | Key | Value "
26
+ puts "-------------|------------------------------|-------"
27
+ puts configuration_settings
28
+ puts
29
+ puts
30
+ puts "Misc:"
31
+ puts "---------------"
32
+ puts "Layaway Files stored at: #{agent_context.layaway.directory}"
33
+ puts "Logs stored at: #{log_details}"
34
+
35
+ end
36
+
37
+ def agent_context
38
+ ScoutApm::Agent.instance.context
39
+ end
40
+
41
+ def installed_instruments
42
+ ScoutApm::Agent.
43
+ instance.
44
+ instrument_manager.
45
+ installed_instruments.
46
+ map{|instance| "#{instance.installed? ? "Installed " : "Not Installed"} - #{instance.class.to_s}"}.
47
+ join("\n")
48
+ end
49
+
50
+ def configuration_settings
51
+ all_settings = agent_context.config.all_settings
52
+
53
+ longest_key = all_settings.
54
+ map{|setting| setting[:key] }.
55
+ inject(0) { |len, key| key.length > len ? key.length : len }
56
+
57
+ format_string = "%12s | %-#{longest_key}s | %s"
58
+
59
+ all_settings.
60
+ map{|setting| sprintf format_string, setting[:source], setting[:key], setting[:value]}.
61
+ join("\n")
62
+ end
63
+
64
+ def log_details
65
+ if agent_context.logger.log_destination == STDOUT
66
+ "STDOUT"
67
+ elsif agent_context.logger.log_destination == STDERR
68
+ "STDERR"
69
+ else
70
+ "#{agent_context.logger.log_file_path}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,22 @@
1
+ module ScoutApm
2
+ module Tasks
3
+ class Support
4
+ def self.run!
5
+ puts "Support Task"
6
+ new.run!
7
+ end
8
+
9
+ def initialize
10
+ @doctor = ScoutApm::Tasks::Doctor.new
11
+ end
12
+
13
+ def run!
14
+ instruments = @doctor.installed_instruments
15
+ config = @doctor.configuration_settings
16
+ collect_logs
17
+
18
+ post_data
19
+ end
20
+ end
21
+ end
22
+ end
@@ -45,13 +45,13 @@ module ScoutApm
45
45
  # type - "View" or "ActiveRecord" and similar
46
46
  # name - "users/show", "App#find"
47
47
  def instrument_method(method_name, options = {})
48
- ScoutApm::Agent.instance.logger.info "Instrumenting #{method_name}"
48
+ ScoutApm::Agent.instance.context.logger.info "Instrumenting #{method_name}"
49
49
  type = options[:type] || "Custom"
50
50
  name = options[:name] || "#{self.name}/#{method_name.to_s}"
51
51
 
52
52
  instrumented_name, uninstrumented_name = _determine_instrumented_name(method_name, type)
53
53
 
54
- ScoutApm::Agent.instance.logger.info "Instrumenting #{instrumented_name}, #{uninstrumented_name}"
54
+ ScoutApm::Agent.instance.context.logger.info "Instrumenting #{instrumented_name}, #{uninstrumented_name}"
55
55
 
56
56
  return if !_instrumentable?(method_name) or _instrumented?(instrumented_name, method_name)
57
57
 
@@ -91,7 +91,7 @@ module ScoutApm
91
91
  name = begin
92
92
  "#{name}"
93
93
  rescue => e
94
- ScoutApm::Agent.instance.logger.error("Error raised while interpreting instrumented name: %s, %s" % ['#{name}', e.message])
94
+ ScoutApm::Agent.instance.context.logger.error("Error raised while interpreting instrumented name: %s, %s" % ['#{name}', e.message])
95
95
  "Unknown"
96
96
  end
97
97
 
@@ -110,14 +110,14 @@ module ScoutApm
110
110
  # The method must exist to be instrumented.
111
111
  def _instrumentable?(method_name)
112
112
  exists = method_defined?(method_name) || private_method_defined?(method_name)
113
- ScoutApm::Agent.instance.logger.warn "The method [#{self.name}##{method_name}] does not exist and will not be instrumented" unless exists
113
+ ScoutApm::Agent.instance.context.logger.warn "The method [#{self.name}##{method_name}] does not exist and will not be instrumented" unless exists
114
114
  exists
115
115
  end
116
116
 
117
117
  # +True+ if the method is already instrumented.
118
118
  def _instrumented?(instrumented_name, method_name)
119
119
  instrumented = method_defined?(instrumented_name)
120
- ScoutApm::Agent.instance.logger.warn("The method [#{self.name}##{method_name}] has already been instrumented") if instrumented
120
+ ScoutApm::Agent.instance.context.logger.warn("The method [#{self.name}##{method_name}] has already been instrumented") if instrumented
121
121
  instrumented
122
122
  end
123
123
 
@@ -39,26 +39,23 @@ module ScoutApm
39
39
  # this is set in the controller instumentation (ActionControllerRails3Rails4 according)
40
40
  attr_accessor :instant_key
41
41
 
42
- # Whereas the instant_key gets set per-request in reponse to a URL param, dev_trace is set in the config file
43
- attr_accessor :dev_trace
44
-
45
42
  # An object that responds to `record!(TrackedRequest)` to store this tracked request
46
43
  attr_reader :recorder
47
44
 
48
- def initialize(store)
45
+ def initialize(agent_context, store)
46
+ @agent_context = agent_context
49
47
  @store = store #this is passed in so we can use a real store (normal operation) or fake store (instant mode only)
50
48
  @layers = []
51
49
  @call_set = Hash.new { |h, k| h[k] = CallSet.new }
52
50
  @annotations = {}
53
51
  @ignoring_children = 0
54
- @context = Context.new
52
+ @context = Context.new(agent_context)
55
53
  @root_layer = nil
56
54
  @error = false
57
55
  @stopping = false
58
56
  @instant_key = nil
59
57
  @mem_start = mem_usage
60
- @dev_trace = ScoutApm::Agent.instance.config.value('dev_trace') && ScoutApm::Agent.instance.environment.env == "development"
61
- @recorder = ScoutApm::Agent.instance.recorder
58
+ @recorder = agent_context.recorder
62
59
 
63
60
  ignore_request! if @recorder.nil?
64
61
  end
@@ -113,7 +110,7 @@ module ScoutApm
113
110
  if finalized?
114
111
  stop_request
115
112
  else
116
- continue_sampling_for_layers if ScoutApm::Agent.instance.config.value('profile')
113
+ continue_sampling_for_layers if @agent_context.config.value('profile')
117
114
  end
118
115
  end
119
116
 
@@ -157,9 +154,15 @@ module ScoutApm
157
154
  @call_set[layer.name].update!(layer.desc)
158
155
  end
159
156
 
157
+ # Grab backtraces more aggressively when running in dev trace mode
158
+ def backtrace_threshold
159
+ @agent_context.dev_trace_enabled? ? 0.05 : 0.5 # the minimum threshold in seconds to record the backtrace for a metric.
160
+ end
161
+
160
162
  # This may be in bytes or KB based on the OSX. We store this as-is here and only do conversion to MB in Layer Converters.
163
+ # XXX: Move this to environment?
161
164
  def mem_usage
162
- ScoutApm::Instruments::Process::ProcessMemory.rss
165
+ ScoutApm::Instruments::Process::ProcessMemory.new(@agent_context).rss
163
166
  end
164
167
 
165
168
  def capture_mem_delta!
@@ -196,7 +199,7 @@ module ScoutApm
196
199
  def stop_request
197
200
  @stopping = true
198
201
 
199
- if ScoutApm::Agent.instance.config.value('profile')
202
+ if @agent_context.config.value('profile')
200
203
  ScoutApm::Instruments::Stacks.stop_sampling(true)
201
204
  ScoutApm::Instruments::Stacks.update_indexes(0, 0)
202
205
  end
@@ -288,7 +291,7 @@ module ScoutApm
288
291
  restore_store if @store.nil?
289
292
 
290
293
  # Bail out early if the user asked us to ignore this uri
291
- return if ScoutApm::Agent.instance.ignored_uris.ignore?(annotations[:uri])
294
+ return if @agent_context.ignored_uris.ignore?(annotations[:uri])
292
295
 
293
296
  converters = [
294
297
  LayerConverters::Histograms,
@@ -306,7 +309,7 @@ module ScoutApm
306
309
  layer_finder = LayerConverters::FindLayerByType.new(self)
307
310
  walker = LayerConverters::DepthFirstWalker.new(self.root_layer)
308
311
  converters = converters.map do |klass|
309
- instance = klass.new(self, layer_finder, @store)
312
+ instance = klass.new(@agent_context, self, layer_finder, @store)
310
313
  instance.register_hooks(walker)
311
314
  instance
312
315
  end
@@ -319,8 +322,26 @@ module ScoutApm
319
322
  trace = converter.call
320
323
  ScoutApm::InstantReporting.new(trace, instant_key).call
321
324
  end
325
+
326
+ if web? || job?
327
+ ensure_background_worker
328
+ end
329
+ end
330
+
331
+ # Ensure the background worker thread is up & running - a fallback if other
332
+ # detection doesn't achieve this at boot.
333
+ def ensure_background_worker
334
+ agent = ScoutApm::Agent.instance
335
+ agent.start
336
+
337
+ if agent.start_background_worker(:quiet)
338
+ agent.logger.info("Force Started BG Worker")
339
+ end
340
+ rescue => e
341
+ true
322
342
  end
323
343
 
344
+
324
345
  # Only call this after the request is complete
325
346
  def unique_name
326
347
  return nil if ignoring_request?
@@ -385,11 +406,6 @@ module ScoutApm
385
406
  @ignoring_children > 0
386
407
  end
387
408
 
388
- # Grab backtraces more aggressively when running in dev trace mode
389
- def backtrace_threshold
390
- dev_trace ? 0.05 : 0.5 # the minimum threshold in seconds to record the backtrace for a metric.
391
- end
392
-
393
409
  ################################################################################
394
410
  # Ignoring the rest of a request
395
411
  ################################################################################
@@ -399,6 +415,10 @@ module ScoutApm
399
415
  # layers, and delete any existing layer info. This class will still exist,
400
416
  # and respond to methods as normal, but `record!` won't be called, and no
401
417
  # data will be recorded.
418
+ #
419
+ # We still need to keep track of the current layer depth (via
420
+ # @ignoring_depth counter) so we know when to report that the class was
421
+ # "reported", and ready to be recreated for the next request.
402
422
 
403
423
  def ignore_request!
404
424
  return if @ignoring_request
@@ -434,9 +454,13 @@ module ScoutApm
434
454
  end
435
455
 
436
456
  def logger
437
- ScoutApm::Agent.instance.logger
457
+ @agent_context.logger
438
458
  end
439
459
 
460
+ ###########################
461
+ # Serialization Helpers
462
+ ###########################
463
+
440
464
  # Actually go fetch & make-real any lazily created data.
441
465
  # Clean up any cleverness in objects.
442
466
  # Makes this object ready to be Marshal Dumped (or otherwise serialized)
@@ -449,7 +473,7 @@ module ScoutApm
449
473
  # Go re-fetch the store based on what the Agent's official one is. Used
450
474
  # after hydrating a dumped TrackedRequest
451
475
  def restore_store
452
- @store = ScoutApm::Agent.instance.store
476
+ @store = @agent_context.store
453
477
  end
454
478
  end
455
479
  end
@@ -1,12 +1,11 @@
1
1
  module ScoutApm
2
2
  module Utils
3
3
  class ActiveRecordMetricName
4
- DEFAULT_METRIC = "SQL/Unknown"
5
-
6
4
  attr_reader :sql, :name
5
+ DEFAULT_METRIC = 'SQL/other'.freeze
7
6
 
8
7
  def initialize(sql, name)
9
- @sql = sql
8
+ @sql = sql || ""
10
9
  @name = name.to_s
11
10
  end
12
11
 
@@ -17,13 +16,11 @@ module ScoutApm
17
16
  # name: Place Load
18
17
  # metric_name: Place/find
19
18
  def to_s
20
- return DEFAULT_METRIC unless name
21
- return DEFAULT_METRIC unless model && operation
22
-
23
- if parsed = parse_operation
19
+ parsed = parse_operation
20
+ if parsed
24
21
  "#{model}/#{parsed}"
25
22
  else
26
- "SQL/other"
23
+ regex_name(sql)
27
24
  end
28
25
  end
29
26
 
@@ -76,6 +73,67 @@ module ScoutApm
76
73
  end
77
74
  end
78
75
  end
76
+
77
+
78
+ ########################
79
+ # Regex based naming #
80
+ ########################
81
+ #
82
+ WHITE_SPACE = '\s*'
83
+ REGEX_OPERATION = '(SELECT|UPDATE|INSERT|DELETE)'
84
+ FROM = 'FROM'
85
+ INTO = 'INTO'
86
+ NON_GREEDY_CONSUME = '.*?'
87
+ TABLE = '(?:"|`)?(.*?)(?:"|`)?\s'
88
+ COUNT = 'COUNT\(.*?\)'
89
+
90
+ SELECT_REGEX = /\A#{WHITE_SPACE}(SELECT)#{WHITE_SPACE}(#{COUNT})?#{NON_GREEDY_CONSUME}#{FROM}#{WHITE_SPACE}#{TABLE}/i.freeze
91
+ UPDATE_REGEX = /\A#{WHITE_SPACE}(UPDATE)#{WHITE_SPACE}#{TABLE}/i.freeze
92
+ INSERT_REGEX = /\A#{WHITE_SPACE}(INSERT)#{WHITE_SPACE}#{INTO}#{WHITE_SPACE}#{TABLE}/i.freeze
93
+ DELETE_REGEX = /\A#{WHITE_SPACE}(DELETE)#{WHITE_SPACE}#{FROM}#{TABLE}/i.freeze
94
+
95
+ COUNT_LABEL = 'count'.freeze
96
+ SELECT_LABEL = 'find'.freeze
97
+ UPDATE_LABEL = 'save'.freeze
98
+ INSERT_LABEL = 'create'.freeze
99
+ DELETE_LABEL = 'destroy'.freeze
100
+ UNKNOWN_LABEL = 'SQL/other'.freeze
101
+
102
+ # Attempt to do some basic parsing of SQL via regexes to extract the SQL
103
+ # verb (select, update, etc) and the table being operated on.
104
+ #
105
+ # This is a fallback from what ActiveRecord gives us, we prefer its
106
+ # names. But sometimes it is giving us a no-name query, and we have to
107
+ # attempt to figure it out ourselves.
108
+ #
109
+ # This relies on ActiveSupport's classify method. If it's not present,
110
+ # just skip the attempt to rename here. This could happen in a Grape or
111
+ # Sinatra application that doesn't import ActiveSupport. At this point,
112
+ # you're already using ActiveRecord, so it's likely loaded anyway.
113
+ def regex_name(sql)
114
+ # We rely on the ActiveSupport inflections code here. Bail early if we can't use it.
115
+ return UNKNOWN_LABEL unless UNKNOWN_LABEL.respond_to?(:classify)
116
+
117
+ if match = SELECT_REGEX.match(sql)
118
+ operation =
119
+ if match[2]
120
+ COUNT_LABEL
121
+ else
122
+ SELECT_LABEL
123
+ end
124
+ "#{match[3].classify}/#{operation}"
125
+ elsif match = UPDATE_REGEX.match(sql)
126
+ "#{match[2].classify}/#{UPDATE_LABEL}"
127
+ elsif match = INSERT_REGEX.match(sql)
128
+ "#{match[2].classify}/#{INSERT_LABEL}"
129
+ elsif match = DELETE_REGEX.match(sql)
130
+ "#{match[2].classify}/#{DELETE_LABEL}"
131
+ else
132
+ UNKNOWN_LABEL
133
+ end
134
+ rescue
135
+ UNKNOWN_LABEL
136
+ end
79
137
  end
80
138
  end
81
139
  end
@@ -11,7 +11,7 @@ module ScoutApm
11
11
 
12
12
  attr_reader :call_stack
13
13
 
14
- def initialize(call_stack, root=ScoutApm::Environment.instance.root)
14
+ def initialize(call_stack, root=ScoutApm::Agent.instance.context.environment.root)
15
15
  @call_stack = call_stack
16
16
  # We can't use a constant as it'd be too early to fetch environment info
17
17
  #
@@ -1,10 +1,14 @@
1
1
  module ScoutApm
2
2
  module Utils
3
3
  class InstalledGems
4
- attr_reader :logger
4
+ attr_reader :context
5
5
 
6
- def initialize(logger=ScoutApm::Agent.instance.logger)
7
- @logger = logger
6
+ def initialize(context)
7
+ @context = context
8
+ end
9
+
10
+ def logger
11
+ context.logger
8
12
  end
9
13
 
10
14
  def run
@@ -5,6 +5,12 @@ module ScoutApm
5
5
  # KlassHelper.defined?("ActiveRecord::Base") #=> true / false
6
6
 
7
7
  def self.defined?(*names)
8
+ lookup(*names) != :missing_class
9
+ end
10
+
11
+ # KlassHelper.lookup("ActiveRecord::Base") => ActiveRecord::Base
12
+ # KlassHelper.lookup("ActiveRecord::SomethingThatDoesNotExist") => :missing_class
13
+ def self.lookup(*names)
8
14
  if names.length == 1
9
15
  names = names[0].split("::")
10
16
  end
@@ -15,11 +21,11 @@ module ScoutApm
15
21
  begin
16
22
  obj = obj.const_get(name)
17
23
  rescue NameError
18
- return false
24
+ return :missing_class
19
25
  end
20
26
  end
21
27
 
22
- true
28
+ obj
23
29
  end
24
30
  end
25
31
  end