scout_apm 3.0.0.pre13 → 3.0.0.pre14

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