skylight 4.3.2 → 5.1.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +399 -336
  3. data/CLA.md +1 -1
  4. data/CONTRIBUTING.md +2 -8
  5. data/LICENSE.md +7 -17
  6. data/README.md +1 -1
  7. data/ext/extconf.rb +45 -56
  8. data/ext/libskylight.yml +10 -6
  9. data/ext/skylight_native.c +22 -99
  10. data/lib/skylight.rb +201 -14
  11. data/lib/skylight/api.rb +32 -21
  12. data/lib/skylight/cli.rb +48 -46
  13. data/lib/skylight/cli/doctor.rb +62 -63
  14. data/lib/skylight/cli/helpers.rb +19 -19
  15. data/lib/skylight/cli/merger.rb +142 -138
  16. data/lib/skylight/config.rb +634 -199
  17. data/lib/skylight/deprecation.rb +17 -0
  18. data/lib/skylight/errors.rb +23 -9
  19. data/lib/skylight/extensions.rb +95 -0
  20. data/lib/skylight/extensions/source_location.rb +291 -0
  21. data/lib/skylight/formatters/http.rb +18 -0
  22. data/lib/skylight/gc.rb +99 -0
  23. data/lib/skylight/helpers.rb +81 -36
  24. data/lib/skylight/instrumenter.rb +336 -18
  25. data/lib/skylight/middleware.rb +134 -1
  26. data/lib/skylight/native.rb +60 -12
  27. data/lib/skylight/native_ext_fetcher.rb +13 -14
  28. data/lib/skylight/normalizers.rb +157 -0
  29. data/lib/skylight/normalizers/action_controller/process_action.rb +68 -0
  30. data/lib/skylight/normalizers/action_controller/send_file.rb +51 -0
  31. data/lib/skylight/normalizers/action_dispatch/process_middleware.rb +22 -0
  32. data/lib/skylight/normalizers/action_dispatch/route_set.rb +27 -0
  33. data/lib/skylight/normalizers/action_view/render_collection.rb +24 -0
  34. data/lib/skylight/normalizers/action_view/render_layout.rb +25 -0
  35. data/lib/skylight/normalizers/action_view/render_partial.rb +23 -0
  36. data/lib/skylight/normalizers/action_view/render_template.rb +23 -0
  37. data/lib/skylight/normalizers/active_job/perform.rb +90 -0
  38. data/lib/skylight/normalizers/active_model_serializers/render.rb +32 -0
  39. data/lib/skylight/normalizers/active_record/instantiation.rb +16 -0
  40. data/lib/skylight/normalizers/active_record/sql.rb +12 -0
  41. data/lib/skylight/normalizers/active_storage.rb +28 -0
  42. data/lib/skylight/normalizers/active_support/cache.rb +11 -0
  43. data/lib/skylight/normalizers/active_support/cache_clear.rb +16 -0
  44. data/lib/skylight/normalizers/active_support/cache_decrement.rb +16 -0
  45. data/lib/skylight/normalizers/active_support/cache_delete.rb +16 -0
  46. data/lib/skylight/normalizers/active_support/cache_exist.rb +16 -0
  47. data/lib/skylight/normalizers/active_support/cache_fetch_hit.rb +16 -0
  48. data/lib/skylight/normalizers/active_support/cache_generate.rb +16 -0
  49. data/lib/skylight/normalizers/active_support/cache_increment.rb +16 -0
  50. data/lib/skylight/normalizers/active_support/cache_read.rb +16 -0
  51. data/lib/skylight/normalizers/active_support/cache_read_multi.rb +16 -0
  52. data/lib/skylight/normalizers/active_support/cache_write.rb +16 -0
  53. data/lib/skylight/normalizers/coach/handler_finish.rb +44 -0
  54. data/lib/skylight/normalizers/coach/middleware_finish.rb +33 -0
  55. data/lib/skylight/normalizers/couch_potato/query.rb +20 -0
  56. data/lib/skylight/normalizers/data_mapper/sql.rb +12 -0
  57. data/lib/skylight/normalizers/default.rb +24 -0
  58. data/lib/skylight/normalizers/elasticsearch/request.rb +20 -0
  59. data/lib/skylight/normalizers/faraday/request.rb +38 -0
  60. data/lib/skylight/normalizers/grape/endpoint.rb +28 -0
  61. data/lib/skylight/normalizers/grape/endpoint_render.rb +25 -0
  62. data/lib/skylight/normalizers/grape/endpoint_run.rb +39 -0
  63. data/lib/skylight/normalizers/grape/endpoint_run_filters.rb +20 -0
  64. data/lib/skylight/normalizers/grape/format_response.rb +20 -0
  65. data/lib/skylight/normalizers/graphiti/render.rb +22 -0
  66. data/lib/skylight/normalizers/graphiti/resolve.rb +31 -0
  67. data/lib/skylight/normalizers/graphql/base.rb +127 -0
  68. data/lib/skylight/normalizers/render.rb +79 -0
  69. data/lib/skylight/normalizers/sequel/sql.rb +12 -0
  70. data/lib/skylight/normalizers/shrine.rb +32 -0
  71. data/lib/skylight/normalizers/sql.rb +45 -0
  72. data/lib/skylight/probes.rb +173 -0
  73. data/lib/skylight/probes/action_controller.rb +52 -0
  74. data/lib/skylight/probes/action_dispatch.rb +2 -0
  75. data/lib/skylight/probes/action_dispatch/request_id.rb +33 -0
  76. data/lib/skylight/probes/action_dispatch/routing/route_set.rb +30 -0
  77. data/lib/skylight/probes/action_view.rb +42 -0
  78. data/lib/skylight/probes/active_job.rb +27 -0
  79. data/lib/skylight/probes/active_job_enqueue.rb +35 -0
  80. data/lib/skylight/probes/active_model_serializers.rb +50 -0
  81. data/lib/skylight/probes/delayed_job.rb +144 -0
  82. data/lib/skylight/probes/elasticsearch.rb +36 -0
  83. data/lib/skylight/probes/excon.rb +25 -0
  84. data/lib/skylight/probes/excon/middleware.rb +65 -0
  85. data/lib/skylight/probes/faraday.rb +23 -0
  86. data/lib/skylight/probes/graphql.rb +38 -0
  87. data/lib/skylight/probes/httpclient.rb +44 -0
  88. data/lib/skylight/probes/middleware.rb +135 -0
  89. data/lib/skylight/probes/mongo.rb +156 -0
  90. data/lib/skylight/probes/mongoid.rb +13 -0
  91. data/lib/skylight/probes/net_http.rb +54 -0
  92. data/lib/skylight/probes/redis.rb +51 -0
  93. data/lib/skylight/probes/sequel.rb +29 -0
  94. data/lib/skylight/probes/sinatra.rb +66 -0
  95. data/lib/skylight/probes/sinatra_add_middleware.rb +10 -10
  96. data/lib/skylight/probes/tilt.rb +25 -0
  97. data/lib/skylight/railtie.rb +157 -27
  98. data/lib/skylight/sidekiq.rb +47 -0
  99. data/lib/skylight/subscriber.rb +108 -0
  100. data/lib/skylight/test.rb +151 -0
  101. data/lib/skylight/trace.rb +325 -22
  102. data/lib/skylight/user_config.rb +58 -0
  103. data/lib/skylight/util.rb +12 -0
  104. data/lib/skylight/util/allocation_free.rb +26 -0
  105. data/lib/skylight/util/clock.rb +57 -0
  106. data/lib/skylight/util/component.rb +22 -22
  107. data/lib/skylight/util/deploy.rb +16 -21
  108. data/lib/skylight/util/gzip.rb +20 -0
  109. data/lib/skylight/util/http.rb +106 -113
  110. data/lib/skylight/util/instrumenter_method.rb +26 -0
  111. data/lib/skylight/util/logging.rb +136 -0
  112. data/lib/skylight/util/lru_cache.rb +36 -0
  113. data/lib/skylight/util/platform.rb +1 -5
  114. data/lib/skylight/util/ssl.rb +1 -25
  115. data/lib/skylight/vendor/cli/thor/rake_compat.rb +1 -1
  116. data/lib/skylight/version.rb +5 -1
  117. data/lib/skylight/vm/gc.rb +60 -0
  118. metadata +126 -13
@@ -0,0 +1,39 @@
1
+ module Skylight
2
+ module Normalizers
3
+ module Grape
4
+ class EndpointRun < Endpoint
5
+ register "endpoint_run.grape"
6
+
7
+ def normalize(trace, _name, payload)
8
+ trace.endpoint = get_endpoint_name(payload[:endpoint]) if payload[:endpoint]
9
+
10
+ # We don't necessarily want this to be all instrumented since it's fairly internal.
11
+ # However, it is a good place to get the endpoint name.
12
+ :skip
13
+ end
14
+
15
+ private
16
+
17
+ def get_endpoint_name(endpoint)
18
+ method = get_method(endpoint)
19
+ path = get_path(endpoint)
20
+ namespace = get_namespace(endpoint)
21
+
22
+ if namespace && !namespace.empty?
23
+ path = "/#{path}" if path[0] != "/"
24
+ path = "#{namespace}#{path}"
25
+ end
26
+
27
+ "#{base_app_name(endpoint)} [#{method}] #{path}".strip
28
+ end
29
+
30
+ def base_app_name(endpoint)
31
+ ep = endpoint.options[:for]
32
+ return ep.name if ep.name
33
+
34
+ ep.base.name if ep.respond_to?(:base) && ep.base.respond_to?(:name)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
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
+ return :skip if (!filters || filters.empty?) || !type
14
+
15
+ [CAT, "#{type.to_s.capitalize} Filters", nil]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ 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,127 @@
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(:KEY, ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(klass.name)).freeze)
29
+ end
30
+
31
+ def self.key
32
+ self::KEY
33
+ end
34
+
35
+ def normalize(_trace, _name, _payload)
36
+ [CAT, "graphql.#{key}", nil]
37
+ end
38
+
39
+ private
40
+
41
+ def key
42
+ self.class.key
43
+ end
44
+
45
+ def extract_query_name(query)
46
+ query&.context&.[](:skylight_endpoint) || query&.operation_name || ANONYMOUS
47
+ end
48
+ end
49
+
50
+ class Lex < Base
51
+ register_graphql
52
+ end
53
+
54
+ class Parse < Base
55
+ register_graphql
56
+ end
57
+
58
+ class Validate < Base
59
+ register_graphql
60
+ end
61
+
62
+ class ExecuteMultiplex < Base
63
+ register_graphql
64
+
65
+ def normalize_after(trace, _span, _name, payload)
66
+ # This is in normalize_after because the queries may not have
67
+ # an assigned operation name before they are executed.
68
+ # For example, if you send a single query with a defined operation name, e.g.:
69
+ # ```graphql
70
+ # query MyNamedQuery { user(id: 1) { name } }
71
+ # ```
72
+ # ... but do _not_ send the operationName request param, the GraphQL docs[1]
73
+ # specify that the executor should use the operation name from the definition.
74
+ #
75
+ # In graphql-ruby's case, the calculation of the operation name is lazy, and
76
+ # has not been done yet at the point where execute_multiplex starts.
77
+ # [1] https://graphql.org/learn/serving-over-http/#post-request
78
+ queries, has_errors =
79
+ payload[:multiplex]
80
+ .queries
81
+ .each_with_object([Set.new, Set.new]) do |query, (names, errors)|
82
+ names << extract_query_name(query)
83
+ errors << query.static_errors.any?
84
+ end
85
+
86
+ trace.endpoint = "graphql:#{queries.sort.join("+")}"
87
+ trace.compound_response_error_status =
88
+ if has_errors.all?
89
+ :all
90
+ elsif has_errors.any?
91
+ :partial
92
+ end
93
+ end
94
+ end
95
+
96
+ class AnalyzeQuery < Base
97
+ register_graphql
98
+ end
99
+
100
+ class ExecuteQuery < Base
101
+ register_graphql
102
+
103
+ def normalize(trace, _name, payload)
104
+ query_name = extract_query_name(payload[:query])
105
+
106
+ meta = { mute_children: true } if query_name == ANONYMOUS
107
+
108
+ # This is probably always overriden by execute_multiplex#normalize_after,
109
+ # but in the case of a single query, it will be the same value anyway.
110
+ trace.endpoint = "graphql:#{query_name}"
111
+
112
+ [CAT, "graphql.#{key}: #{query_name}", nil, meta]
113
+ end
114
+ end
115
+
116
+ class ExecuteQueryLazy < ExecuteQuery
117
+ register_graphql
118
+
119
+ def normalize(trace, _name, payload)
120
+ if payload[:query]
121
+ super
122
+ elsif payload[:multiplex]
123
+ [CAT, "graphql.#{key}.multiplex", nil]
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,79 @@
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
+ SEPARATOR_BYTES.include?(path.getbyte(2)) if alpha?(path.getbyte(0)) && path.getbyte(1) == COLON_BYTE
67
+ end
68
+
69
+ def alpha?(byte)
70
+ (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)
71
+ end
72
+ else
73
+ def absolute_path?(path)
74
+ path.getbyte(0) == SEPARATOR_BYTE
75
+ end
76
+ end
77
+ end
78
+ end
79
+ 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,32 @@
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 { |key| register key }
22
+
23
+ def normalize(_trace, name, _payload)
24
+ title = ["Shrine", TITLES[name]].join(" ")
25
+
26
+ cat = "app.#{name.split(".").reverse.join(".")}"
27
+
28
+ [cat, title, nil]
29
+ end
30
+ end
31
+ end
32
+ 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,173 @@
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
+ # rubocop:disable Lint/SuppressedException
36
+ gems =
37
+ begin
38
+ Bundler.locked_gems.dependencies.map { |d| [d.name, d.requirement.to_s] }
39
+ rescue StandardError
40
+ end
41
+
42
+ # rubocop:enable Lint/SuppressedException
43
+
44
+ error =
45
+ "[SKYLIGHT] [#{Skylight::VERSION}] Encountered an error while installing the " \
46
+ "probe for #{const_name}. Please notify support@skylight.io with the debugging " \
47
+ "information below. It's recommended that you disable this probe until the " \
48
+ "issue is resolved." \
49
+ "\n\nERROR: #{description}\n\n#{backtrace}\n\n"
50
+
51
+ if gems
52
+ gems_string = gems.map { |g| " #{g[0]} #{g[1]}" }.join("\n")
53
+ error << "GEMS:\n\n#{gems_string}\n\n"
54
+ end
55
+
56
+ $stderr.puts(error)
57
+ end
58
+ end
59
+
60
+ class << self
61
+ def constant_available?(const_name)
62
+ ::ActiveSupport::Inflector.safe_constantize(const_name).present?
63
+ end
64
+
65
+ def install!
66
+ pending = registered.values - installed.values
67
+
68
+ pending.each do |registration|
69
+ registration.constant_available? ? install_probe(registration) : register_require_hook(registration)
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
83
+ .glob(root.join("./**/*.rb"))
84
+ .each do |f|
85
+ name = f.relative_path_from(root).sub_ext("").to_s
86
+ raise "duplicate probe name: #{name}; original=#{available[name]}; new=#{f}" if available.key?(name)
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
+ raise ArgumentError, "unknown probes: #{unknown.join(", ")}" unless unknown.empty?
99
+
100
+ probes.each { |p| require available[p.to_s] }
101
+ end
102
+
103
+ def registered
104
+ @registered ||= {}
105
+ end
106
+
107
+ def require_hooks
108
+ @require_hooks ||= {}
109
+ end
110
+
111
+ def installed
112
+ @installed ||= {}
113
+ end
114
+
115
+ def register(name, *args)
116
+ raise "already registered: #{name}" if registered.key?(name)
117
+
118
+ registered[name] = ProbeRegistration.new(name, *args)
119
+
120
+ true
121
+ end
122
+
123
+ def require_hook(require_path)
124
+ each_by_require_path(require_path) do |registration|
125
+ # Double check constant is available
126
+ next unless registration.constant_available?
127
+
128
+ install_probe(registration)
129
+
130
+ # Don't need this to be called again
131
+ unregister_require_hook(registration)
132
+ end
133
+ end
134
+
135
+ def register_require_hook(registration)
136
+ registration.require_paths.each do |p|
137
+ require_hooks[p] ||= []
138
+ require_hooks[p] << registration
139
+ end
140
+ end
141
+
142
+ def unregister_require_hook(registration)
143
+ registration.require_paths.each do |p|
144
+ require_hooks[p].delete(registration)
145
+ require_hooks.delete(p) if require_hooks[p].empty?
146
+ end
147
+ end
148
+
149
+ def each_by_require_path(require_path)
150
+ return unless require_hooks.key?(require_path)
151
+
152
+ # dup because we may be mutating the array
153
+ require_hooks[require_path].dup.each { |registration| yield registration }
154
+ end
155
+ end
156
+
157
+ add_path(File.expand_path("./probes", __dir__))
158
+ end
159
+ end
160
+
161
+ # @api private
162
+ module Kernel
163
+ # Unfortunately, we can't use prepend here, in part because RubyGems changes require with an alias
164
+ alias require_without_sk require
165
+
166
+ def require(name)
167
+ require_without_sk(name).tap do
168
+ Skylight::Probes.require_hook(name)
169
+ rescue Exception => e
170
+ warn("[SKYLIGHT] Rescued exception in require hook", e)
171
+ end
172
+ end
173
+ end