skylight 4.3.2 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -3
  3. data/CONTRIBUTING.md +2 -8
  4. data/ext/extconf.rb +6 -5
  5. data/ext/libskylight.yml +7 -6
  6. data/ext/skylight_native.c +22 -99
  7. data/lib/skylight.rb +211 -14
  8. data/lib/skylight/api.rb +10 -3
  9. data/lib/skylight/cli.rb +4 -3
  10. data/lib/skylight/cli/doctor.rb +13 -14
  11. data/lib/skylight/cli/merger.rb +6 -4
  12. data/lib/skylight/config.rb +597 -127
  13. data/lib/skylight/deprecation.rb +17 -0
  14. data/lib/skylight/errors.rb +21 -6
  15. data/lib/skylight/extensions.rb +107 -0
  16. data/lib/skylight/extensions/source_location.rb +291 -0
  17. data/lib/skylight/formatters/http.rb +20 -0
  18. data/lib/skylight/gc.rb +109 -0
  19. data/lib/skylight/helpers.rb +69 -26
  20. data/lib/skylight/instrumenter.rb +326 -15
  21. data/lib/skylight/middleware.rb +138 -1
  22. data/lib/skylight/native.rb +52 -2
  23. data/lib/skylight/native_ext_fetcher.rb +4 -3
  24. data/lib/skylight/normalizers.rb +153 -0
  25. data/lib/skylight/normalizers/action_controller/process_action.rb +69 -0
  26. data/lib/skylight/normalizers/action_controller/send_file.rb +50 -0
  27. data/lib/skylight/normalizers/action_dispatch/process_middleware.rb +22 -0
  28. data/lib/skylight/normalizers/action_dispatch/route_set.rb +27 -0
  29. data/lib/skylight/normalizers/action_view/render_collection.rb +24 -0
  30. data/lib/skylight/normalizers/action_view/render_layout.rb +25 -0
  31. data/lib/skylight/normalizers/action_view/render_partial.rb +23 -0
  32. data/lib/skylight/normalizers/action_view/render_template.rb +23 -0
  33. data/lib/skylight/normalizers/active_job/perform.rb +86 -0
  34. data/lib/skylight/normalizers/active_model_serializers/render.rb +28 -0
  35. data/lib/skylight/normalizers/active_record/instantiation.rb +16 -0
  36. data/lib/skylight/normalizers/active_record/sql.rb +12 -0
  37. data/lib/skylight/normalizers/active_storage.rb +30 -0
  38. data/lib/skylight/normalizers/active_support/cache.rb +22 -0
  39. data/lib/skylight/normalizers/active_support/cache_clear.rb +16 -0
  40. data/lib/skylight/normalizers/active_support/cache_decrement.rb +16 -0
  41. data/lib/skylight/normalizers/active_support/cache_delete.rb +16 -0
  42. data/lib/skylight/normalizers/active_support/cache_exist.rb +16 -0
  43. data/lib/skylight/normalizers/active_support/cache_fetch_hit.rb +16 -0
  44. data/lib/skylight/normalizers/active_support/cache_generate.rb +16 -0
  45. data/lib/skylight/normalizers/active_support/cache_increment.rb +16 -0
  46. data/lib/skylight/normalizers/active_support/cache_read.rb +16 -0
  47. data/lib/skylight/normalizers/active_support/cache_read_multi.rb +16 -0
  48. data/lib/skylight/normalizers/active_support/cache_write.rb +16 -0
  49. data/lib/skylight/normalizers/coach/handler_finish.rb +46 -0
  50. data/lib/skylight/normalizers/coach/middleware_finish.rb +33 -0
  51. data/lib/skylight/normalizers/couch_potato/query.rb +20 -0
  52. data/lib/skylight/normalizers/data_mapper/sql.rb +12 -0
  53. data/lib/skylight/normalizers/default.rb +32 -0
  54. data/lib/skylight/normalizers/elasticsearch/request.rb +20 -0
  55. data/lib/skylight/normalizers/faraday/request.rb +40 -0
  56. data/lib/skylight/normalizers/grape/endpoint.rb +34 -0
  57. data/lib/skylight/normalizers/grape/endpoint_render.rb +25 -0
  58. data/lib/skylight/normalizers/grape/endpoint_run.rb +41 -0
  59. data/lib/skylight/normalizers/grape/endpoint_run_filters.rb +22 -0
  60. data/lib/skylight/normalizers/grape/format_response.rb +20 -0
  61. data/lib/skylight/normalizers/graphiti/render.rb +22 -0
  62. data/lib/skylight/normalizers/graphiti/resolve.rb +31 -0
  63. data/lib/skylight/normalizers/graphql/base.rb +132 -0
  64. data/lib/skylight/normalizers/render.rb +81 -0
  65. data/lib/skylight/normalizers/sequel/sql.rb +12 -0
  66. data/lib/skylight/normalizers/shrine.rb +34 -0
  67. data/lib/skylight/normalizers/sql.rb +45 -0
  68. data/lib/skylight/probes.rb +181 -0
  69. data/lib/skylight/probes/action_controller.rb +48 -0
  70. data/lib/skylight/probes/action_dispatch.rb +2 -0
  71. data/lib/skylight/probes/action_dispatch/request_id.rb +29 -0
  72. data/lib/skylight/probes/action_dispatch/routing/route_set.rb +28 -0
  73. data/lib/skylight/probes/action_view.rb +43 -0
  74. data/lib/skylight/probes/active_job.rb +27 -0
  75. data/lib/skylight/probes/active_job_enqueue.rb +41 -0
  76. data/lib/skylight/probes/active_model_serializers.rb +50 -0
  77. data/lib/skylight/probes/delayed_job.rb +149 -0
  78. data/lib/skylight/probes/elasticsearch.rb +38 -0
  79. data/lib/skylight/probes/excon.rb +25 -0
  80. data/lib/skylight/probes/excon/middleware.rb +66 -0
  81. data/lib/skylight/probes/faraday.rb +23 -0
  82. data/lib/skylight/probes/graphql.rb +43 -0
  83. data/lib/skylight/probes/httpclient.rb +44 -0
  84. data/lib/skylight/probes/middleware.rb +126 -0
  85. data/lib/skylight/probes/mongo.rb +164 -0
  86. data/lib/skylight/probes/mongoid.rb +13 -0
  87. data/lib/skylight/probes/net_http.rb +54 -0
  88. data/lib/skylight/probes/redis.rb +63 -0
  89. data/lib/skylight/probes/sequel.rb +33 -0
  90. data/lib/skylight/probes/sinatra.rb +63 -0
  91. data/lib/skylight/probes/sinatra_add_middleware.rb +10 -10
  92. data/lib/skylight/probes/tilt.rb +27 -0
  93. data/lib/skylight/railtie.rb +162 -18
  94. data/lib/skylight/sidekiq.rb +48 -0
  95. data/lib/skylight/subscriber.rb +110 -0
  96. data/lib/skylight/test.rb +146 -0
  97. data/lib/skylight/trace.rb +307 -10
  98. data/lib/skylight/user_config.rb +61 -0
  99. data/lib/skylight/util.rb +12 -0
  100. data/lib/skylight/util/allocation_free.rb +26 -0
  101. data/lib/skylight/util/clock.rb +56 -0
  102. data/lib/skylight/util/component.rb +5 -2
  103. data/lib/skylight/util/deploy.rb +7 -10
  104. data/lib/skylight/util/gzip.rb +20 -0
  105. data/lib/skylight/util/http.rb +4 -10
  106. data/lib/skylight/util/instrumenter_method.rb +26 -0
  107. data/lib/skylight/util/logging.rb +138 -0
  108. data/lib/skylight/util/lru_cache.rb +40 -0
  109. data/lib/skylight/util/platform.rb +1 -1
  110. data/lib/skylight/vendor/cli/thor/rake_compat.rb +1 -1
  111. data/lib/skylight/version.rb +5 -1
  112. data/lib/skylight/vm/gc.rb +68 -0
  113. metadata +126 -13
@@ -0,0 +1,22 @@
1
+ module Skylight
2
+ module Normalizers
3
+ module Grape
4
+ class EndpointRunFilters < Endpoint
5
+ register "endpoint_run_filters.grape"
6
+
7
+ CAT = "app.grape.filters".freeze
8
+
9
+ def normalize(_trace, _name, payload)
10
+ filters = payload[:filters]
11
+ type = payload[:type]
12
+
13
+ if (!filters || filters.empty?) || !type
14
+ return :skip
15
+ end
16
+
17
+ [CAT, "#{type.to_s.capitalize} Filters", nil]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module Skylight
2
+ module Normalizers
3
+ module Grape
4
+ class FormatResponse < Normalizer
5
+ register "format_response.grape"
6
+
7
+ CAT = "view.grape.format_response".freeze
8
+
9
+ def normalize(_trace, _name, payload)
10
+ if (formatter = payload[:formatter])
11
+ title = formatter.is_a?(Module) ? formatter.to_s : formatter.class.to_s
12
+ [CAT, title, nil]
13
+ else
14
+ :skip
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skylight
4
+ module Normalizers
5
+ module Graphiti
6
+ class Render < Normalizer
7
+ register "render.graphiti"
8
+
9
+ CAT = "view.render.graphiti"
10
+ ANONYMOUS = "<Anonymous Resource>"
11
+
12
+ def normalize(_trace, _name, payload)
13
+ resource_class = payload[:proxy]&.resource&.class
14
+ title = "Render #{resource_class&.name || ANONYMOUS}"
15
+ desc = nil
16
+
17
+ [CAT, title, desc]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skylight
4
+ module Normalizers
5
+ module Graphiti
6
+ class Resolve < Normalizer
7
+ register "resolve.graphiti"
8
+
9
+ CAT = "app.resolve.graphiti"
10
+
11
+ ANONYMOUS_RESOURCE = "<Anonymous Resource>"
12
+ ANONYMOUS_ADAPTER = "<Anonymous Adapter>"
13
+
14
+ def normalize(_trace, _name, payload)
15
+ resource = payload[:resource]
16
+
17
+ if (sideload = payload[:sideload])
18
+ type = sideload.type.to_s.split("_").map(&:capitalize).join(" ")
19
+ desc = "Custom Scope" if sideload.class.scope_proc
20
+ else
21
+ type = "Primary"
22
+ end
23
+
24
+ title = "Resolve #{type} #{resource.class.name || ANONYMOUS_RESOURCE}"
25
+
26
+ [CAT, title, desc]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector"
4
+
5
+ module Skylight::Normalizers::GraphQL
6
+ # Some AS::N events in GraphQL are not super useful.
7
+ # We are purposefully ignoring the following keys (and you probably shouldn't add them):
8
+ # - "graphql.analyze_multiplex"
9
+ # - "graphql.execute_field" (very frequently called)
10
+ # - "graphql.execute_field_lazy"
11
+
12
+ class Base < Skylight::Normalizers::Normalizer
13
+ ANONYMOUS = "[anonymous]"
14
+ CAT = "app.graphql"
15
+
16
+ if defined?(::GraphQL::VERSION) && Gem::Version.new(::GraphQL::VERSION) >= Gem::Version.new("1.10")
17
+ def self.register_graphql
18
+ register("#{key}.graphql")
19
+ end
20
+ else
21
+ def self.register_graphql
22
+ register("graphql.#{key}")
23
+ end
24
+ end
25
+
26
+ def self.inherited(klass)
27
+ super
28
+ klass.const_set(
29
+ :KEY,
30
+ ActiveSupport::Inflector.underscore(
31
+ ActiveSupport::Inflector.demodulize(klass.name)
32
+ ).freeze
33
+ )
34
+ end
35
+
36
+ def self.key
37
+ self::KEY
38
+ end
39
+
40
+ def normalize(_trace, _name, _payload)
41
+ [CAT, "graphql.#{key}", nil]
42
+ end
43
+
44
+ private
45
+
46
+ def key
47
+ self.class.key
48
+ end
49
+
50
+ def extract_query_name(query)
51
+ query&.context&.[](:skylight_endpoint) ||
52
+ query&.operation_name ||
53
+ ANONYMOUS
54
+ end
55
+ end
56
+
57
+ class Lex < Base
58
+ register_graphql
59
+ end
60
+
61
+ class Parse < Base
62
+ register_graphql
63
+ end
64
+
65
+ class Validate < Base
66
+ register_graphql
67
+ end
68
+
69
+ class ExecuteMultiplex < Base
70
+ register_graphql
71
+
72
+ def normalize_after(trace, _span, _name, payload)
73
+ # This is in normalize_after because the queries may not have
74
+ # an assigned operation name before they are executed.
75
+ # For example, if you send a single query with a defined operation name, e.g.:
76
+ # ```graphql
77
+ # query MyNamedQuery { user(id: 1) { name } }
78
+ # ```
79
+ # ... but do _not_ send the operationName request param, the GraphQL docs[1]
80
+ # specify that the executor should use the operation name from the definition.
81
+ #
82
+ # In graphql-ruby's case, the calculation of the operation name is lazy, and
83
+ # has not been done yet at the point where execute_multiplex starts.
84
+ # [1] https://graphql.org/learn/serving-over-http/#post-request
85
+ queries, has_errors = payload[:multiplex].queries.each_with_object([Set.new, Set.new]) do |query, (names, errors)|
86
+ names << extract_query_name(query)
87
+ errors << query.static_errors.any?
88
+ end
89
+
90
+ trace.endpoint = "graphql:#{queries.sort.join('+')}"
91
+ trace.compound_response_error_status = if has_errors.all?
92
+ :all
93
+ elsif has_errors.any?
94
+ :partial
95
+ end
96
+ end
97
+ end
98
+
99
+ class AnalyzeQuery < Base
100
+ register_graphql
101
+ end
102
+
103
+ class ExecuteQuery < Base
104
+ register_graphql
105
+
106
+ def normalize(trace, _name, payload)
107
+ query_name = extract_query_name(payload[:query])
108
+
109
+ if query_name == ANONYMOUS
110
+ meta = { mute_children: true }
111
+ end
112
+
113
+ # This is probably always overriden by execute_multiplex#normalize_after,
114
+ # but in the case of a single query, it will be the same value anyway.
115
+ trace.endpoint = "graphql:#{query_name}"
116
+
117
+ [CAT, "graphql.#{key}: #{query_name}", nil, meta]
118
+ end
119
+ end
120
+
121
+ class ExecuteQueryLazy < ExecuteQuery
122
+ register_graphql
123
+
124
+ def normalize(trace, _name, payload)
125
+ if payload[:query]
126
+ super
127
+ elsif payload[:multiplex]
128
+ [CAT, "graphql.#{key}.multiplex", nil]
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,81 @@
1
+ module Skylight
2
+ module Normalizers
3
+ # Base Normalizer for Rails rendering
4
+ class RenderNormalizer < Normalizer
5
+ include Skylight::Util::AllocationFree
6
+
7
+ def setup
8
+ @paths = []
9
+
10
+ Gem.path.each do |path|
11
+ @paths << "#{path}/bundler/gems".freeze
12
+ @paths << "#{path}/gems".freeze
13
+ @paths << path
14
+ end
15
+
16
+ @paths.concat(Array(config["normalizers.render.view_paths"]))
17
+ end
18
+
19
+ # Generic normalizer for renders
20
+ # @param category [String]
21
+ # @param payload [Hash]
22
+ # @option payload [String] :identifier
23
+ # @return [Array]
24
+ def normalize_render(category, payload)
25
+ if (path = payload[:identifier])
26
+ title = relative_path(path)
27
+ end
28
+
29
+ [category, title, nil]
30
+ end
31
+
32
+ def relative_path(path)
33
+ return path if relative_path?(path)
34
+
35
+ if (root = array_find(@paths) { |p| path.start_with?(p) })
36
+ start = root.size
37
+ start += 1 if path.getbyte(start) == SEPARATOR_BYTE
38
+
39
+ path[start, path.size].sub(
40
+ # Matches a Gem Version or 12-digit hex (sha)
41
+ # that is preceeded by a `-` and followed by `/`
42
+ # Also matches 'app/views/' if it exists
43
+ %r{-(?:#{Gem::Version::VERSION_PATTERN}|[0-9a-f]{12})/(?:app/views/)*},
44
+ ": ".freeze
45
+ )
46
+ else
47
+ "Absolute Path".freeze
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def relative_path?(path)
54
+ !absolute_path?(path)
55
+ end
56
+
57
+ SEPARATOR_BYTE = File::SEPARATOR.ord
58
+
59
+ if File.const_defined?(:NULL) ? File::NULL == "NUL" : RbConfig::CONFIG["host_os"] =~ /mingw|mswin32/
60
+ # This is a DOSish environment
61
+ ALT_SEPARATOR_BYTE = File::ALT_SEPARATOR&.ord
62
+ COLON_BYTE = ":".ord
63
+ SEPARATOR_BYTES = [SEPARATOR_BYTE, ALT_SEPARATOR_BYTE].freeze
64
+
65
+ def absolute_path?(path)
66
+ if alpha?(path.getbyte(0)) && path.getbyte(1) == COLON_BYTE
67
+ SEPARATOR_BYTES.include?(path.getbyte(2))
68
+ end
69
+ end
70
+
71
+ def alpha?(byte)
72
+ (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)
73
+ end
74
+ else
75
+ def absolute_path?(path)
76
+ path.getbyte(0) == SEPARATOR_BYTE
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,12 @@
1
+ require "skylight/normalizers/sql"
2
+
3
+ module Skylight
4
+ module Normalizers
5
+ module Sequel
6
+ # Normalizer for SQL requests
7
+ class SQL < Normalizers::SQL
8
+ register "sql.sequel"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ module Skylight
2
+ module Normalizers
3
+ class Shrine < Normalizer
4
+ TITLES = {
5
+ "upload.shrine" => "Upload",
6
+ "download.shrine" => "Download",
7
+ "open.shrine" => "Open",
8
+ "exists.shrine" => "Exists",
9
+ "delete.shrine" => "Delete",
10
+ "metadata.shrine" => "Metadata",
11
+ "mime_type.shrine" => "MIME Type",
12
+ "image_dimensions.shrine" => "Image Dimensions",
13
+ "signature.shrine" => "Signature",
14
+ "extension.shrine" => "Extension",
15
+ "derivation.shrine" => "Derivation",
16
+ "derivatives.shrine" => "Derivatives",
17
+ "data_uri.shrine" => "Data URI",
18
+ "remote_url.shrine" => "Remote URL"
19
+ }.freeze
20
+
21
+ TITLES.each_key do |key|
22
+ register key
23
+ end
24
+
25
+ def normalize(_trace, name, _payload)
26
+ title = ["Shrine", TITLES[name]].join(" ")
27
+
28
+ cat = "app.#{name.split('.').reverse.join('.')}"
29
+
30
+ [cat, title, nil]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Skylight
6
+ module Normalizers
7
+ # Normalizer for SQL requests
8
+ class SQL < Normalizer
9
+ CAT = "db.sql.query"
10
+
11
+ # @param trace [Skylight::Messages::Trace::Builder] ignored, only present to match API
12
+ # @param name [String] ignored, only present to match API
13
+ # @param payload [Hash]
14
+ # @option payload [String] [:name] The SQL operation
15
+ # @option payload [Hash] [:binds] The bound parameters
16
+ # @return [Array]
17
+ def normalize(_trace, name, payload)
18
+ case payload[:name]
19
+ when "SCHEMA", "CACHE"
20
+ return :skip
21
+ else
22
+ name = CAT
23
+ title = payload[:name] || "SQL"
24
+ end
25
+
26
+ # We can only handle UTF-8 encoded strings.
27
+ # (Construction method here avoids extra allocations)
28
+ sql = String.new.concat("<sk-sql>", payload[:sql], "</sk-sql>").force_encoding(Encoding::UTF_8)
29
+
30
+ unless sql.valid_encoding?
31
+ if config[:log_sql_parse_errors]
32
+ config.logger.error "[#{Skylight::SqlLexError.formatted_code}] Unable to extract binds from non-UTF-8 " \
33
+ "query. " \
34
+ "encoding=#{payload[:sql].encoding.name} " \
35
+ "sql=#{payload[:sql].inspect} "
36
+ end
37
+
38
+ sql = nil
39
+ end
40
+
41
+ [name, title, sql]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,181 @@
1
+ require "pathname"
2
+ require "active_support/inflector"
3
+
4
+ module Skylight
5
+ # @api private
6
+ module Probes
7
+ class ProbeRegistration
8
+ attr_reader :name, :const_name, :require_paths, :probe
9
+
10
+ def initialize(name, const_name, require_paths, probe)
11
+ @name = name
12
+ @const_name = const_name
13
+ @require_paths = Array(require_paths)
14
+ @probe = probe
15
+ end
16
+
17
+ def install
18
+ probe.install
19
+ rescue StandardError, LoadError => e
20
+ log_install_exception(e)
21
+ end
22
+
23
+ def constant_available?
24
+ Skylight::Probes.constant_available?(const_name)
25
+ end
26
+
27
+ private
28
+
29
+ def log_install_exception(err)
30
+ description = err.class.to_s
31
+ description << ": #{err.message}" unless err.message.empty?
32
+
33
+ backtrace = err.backtrace.map { |l| " #{l}" }.join("\n")
34
+
35
+ gems =
36
+ begin
37
+ Bundler.locked_gems.dependencies.map { |d| [d.name, d.requirement.to_s] }
38
+ rescue # rubocop:disable Lint/SuppressedException
39
+ end
40
+
41
+ error = "[SKYLIGHT] [#{Skylight::VERSION}] Encountered an error while installing the " \
42
+ "probe for #{const_name}. Please notify support@skylight.io with the debugging " \
43
+ "information below. It's recommended that you disable this probe until the " \
44
+ "issue is resolved." \
45
+ "\n\nERROR: #{description}\n\n#{backtrace}\n\n"
46
+
47
+ if gems
48
+ gems_string = gems.map { |g| " #{g[0]} #{g[1]}" }.join("\n")
49
+ error << "GEMS:\n\n#{gems_string}\n\n"
50
+ end
51
+
52
+ $stderr.puts(error)
53
+ end
54
+ end
55
+
56
+ class << self
57
+ def constant_available?(const_name)
58
+ ::ActiveSupport::Inflector.safe_constantize(const_name).present?
59
+ end
60
+
61
+ def install!
62
+ pending = registered.values - installed.values
63
+
64
+ pending.each do |registration|
65
+ if registration.constant_available?
66
+ install_probe(registration)
67
+ else
68
+ register_require_hook(registration)
69
+ end
70
+ end
71
+ end
72
+
73
+ def install_probe(registration)
74
+ return if installed.key?(registration.name)
75
+
76
+ installed[registration.name] = registration
77
+ registration.install
78
+ end
79
+
80
+ def add_path(path)
81
+ root = Pathname.new(path)
82
+ Pathname.glob(root.join("./**/*.rb")).each do |f|
83
+ name = f.relative_path_from(root).sub_ext("").to_s
84
+ if available.key?(name)
85
+ raise "duplicate probe name: #{name}; original=#{available[name]}; new=#{f}"
86
+ end
87
+
88
+ available[name] = f
89
+ end
90
+ end
91
+
92
+ def available
93
+ @available ||= {}
94
+ end
95
+
96
+ def probe(*probes)
97
+ unknown = probes.map(&:to_s) - available.keys
98
+ unless unknown.empty?
99
+ raise ArgumentError, "unknown probes: #{unknown.join(', ')}"
100
+ end
101
+
102
+ probes.each do |p|
103
+ require available[p.to_s]
104
+ end
105
+ end
106
+
107
+ def registered
108
+ @registered ||= {}
109
+ end
110
+
111
+ def require_hooks
112
+ @require_hooks ||= {}
113
+ end
114
+
115
+ def installed
116
+ @installed ||= {}
117
+ end
118
+
119
+ def register(name, *args)
120
+ if registered.key?(name)
121
+ raise "already registered: #{name}"
122
+ end
123
+
124
+ registered[name] = ProbeRegistration.new(name, *args)
125
+
126
+ true
127
+ end
128
+
129
+ def require_hook(require_path)
130
+ each_by_require_path(require_path) do |registration|
131
+ # Double check constant is available
132
+ next unless registration.constant_available?
133
+
134
+ install_probe(registration)
135
+
136
+ # Don't need this to be called again
137
+ unregister_require_hook(registration)
138
+ end
139
+ end
140
+
141
+ def register_require_hook(registration)
142
+ registration.require_paths.each do |p|
143
+ require_hooks[p] ||= []
144
+ require_hooks[p] << registration
145
+ end
146
+ end
147
+
148
+ def unregister_require_hook(registration)
149
+ registration.require_paths.each do |p|
150
+ require_hooks[p].delete(registration)
151
+ require_hooks.delete(p) if require_hooks[p].empty?
152
+ end
153
+ end
154
+
155
+ def each_by_require_path(require_path)
156
+ return unless require_hooks.key?(require_path)
157
+
158
+ # dup because we may be mutating the array
159
+ require_hooks[require_path].dup.each do |registration|
160
+ yield registration
161
+ end
162
+ end
163
+ end
164
+
165
+ add_path(File.expand_path("./probes", __dir__))
166
+ end
167
+ end
168
+
169
+ # @api private
170
+ module Kernel
171
+ # Unfortunately, we can't use prepend here, in part because RubyGems changes require with an alias
172
+ alias require_without_sk require
173
+
174
+ def require(name)
175
+ require_without_sk(name).tap do
176
+ Skylight::Probes.require_hook(name)
177
+ rescue Exception => e
178
+ warn("[SKYLIGHT] Rescued exception in require hook", e)
179
+ end
180
+ end
181
+ end