skylight 4.3.2 → 5.0.1

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