skylight 4.2.3 → 5.3.0

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +420 -331
  3. data/CLA.md +1 -1
  4. data/CONTRIBUTING.md +2 -8
  5. data/ERRORS.md +3 -0
  6. data/LICENSE.md +7 -17
  7. data/README.md +1 -1
  8. data/ext/extconf.rb +61 -56
  9. data/ext/libskylight.yml +8 -6
  10. data/ext/skylight_native.c +26 -100
  11. data/lib/skylight/api.rb +32 -21
  12. data/lib/skylight/cli/doctor.rb +64 -65
  13. data/lib/skylight/cli/helpers.rb +19 -19
  14. data/lib/skylight/cli/merger.rb +142 -138
  15. data/lib/skylight/cli.rb +48 -46
  16. data/lib/skylight/config.rb +640 -201
  17. data/lib/skylight/data/cacert.pem +730 -1023
  18. data/lib/skylight/deprecation.rb +17 -0
  19. data/lib/skylight/errors.rb +26 -9
  20. data/lib/skylight/extensions/source_location.rb +291 -0
  21. data/lib/skylight/extensions.rb +95 -0
  22. data/lib/skylight/formatters/http.rb +18 -0
  23. data/lib/skylight/gc.rb +99 -0
  24. data/lib/skylight/helpers.rb +81 -36
  25. data/lib/skylight/instrumenter.rb +336 -18
  26. data/lib/skylight/middleware.rb +147 -1
  27. data/lib/skylight/native.rb +60 -12
  28. data/lib/skylight/native_ext_fetcher.rb +13 -14
  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 +87 -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 +20 -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 +41 -0
  72. data/lib/skylight/normalizers.rb +157 -0
  73. data/lib/skylight/probes/action_controller.rb +52 -0
  74. data/lib/skylight/probes/action_dispatch/request_id.rb +33 -0
  75. data/lib/skylight/probes/action_dispatch/routing/route_set.rb +30 -0
  76. data/lib/skylight/probes/action_dispatch.rb +2 -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/active_record_async.rb +96 -0
  82. data/lib/skylight/probes/delayed_job.rb +144 -0
  83. data/lib/skylight/probes/elasticsearch.rb +36 -0
  84. data/lib/skylight/probes/excon/middleware.rb +65 -0
  85. data/lib/skylight/probes/excon.rb +25 -0
  86. data/lib/skylight/probes/faraday.rb +23 -0
  87. data/lib/skylight/probes/graphql.rb +38 -0
  88. data/lib/skylight/probes/httpclient.rb +44 -0
  89. data/lib/skylight/probes/middleware.rb +135 -0
  90. data/lib/skylight/probes/mongo.rb +156 -0
  91. data/lib/skylight/probes/mongoid.rb +13 -0
  92. data/lib/skylight/probes/net_http.rb +54 -0
  93. data/lib/skylight/probes/rack_builder.rb +37 -0
  94. data/lib/skylight/probes/redis.rb +51 -0
  95. data/lib/skylight/probes/sequel.rb +29 -0
  96. data/lib/skylight/probes/sinatra.rb +66 -0
  97. data/lib/skylight/probes/sinatra_add_middleware.rb +10 -10
  98. data/lib/skylight/probes/tilt.rb +25 -0
  99. data/lib/skylight/probes.rb +173 -0
  100. data/lib/skylight/railtie.rb +166 -28
  101. data/lib/skylight/sidekiq.rb +47 -0
  102. data/lib/skylight/sinatra.rb +1 -1
  103. data/lib/skylight/subscriber.rb +130 -0
  104. data/lib/skylight/test.rb +147 -0
  105. data/lib/skylight/trace.rb +325 -22
  106. data/lib/skylight/user_config.rb +58 -0
  107. data/lib/skylight/util/allocation_free.rb +26 -0
  108. data/lib/skylight/util/clock.rb +57 -0
  109. data/lib/skylight/util/component.rb +22 -22
  110. data/lib/skylight/util/deploy.rb +19 -24
  111. data/lib/skylight/util/gzip.rb +20 -0
  112. data/lib/skylight/util/http.rb +106 -113
  113. data/lib/skylight/util/instrumenter_method.rb +26 -0
  114. data/lib/skylight/util/logging.rb +136 -0
  115. data/lib/skylight/util/lru_cache.rb +36 -0
  116. data/lib/skylight/util/platform.rb +3 -7
  117. data/lib/skylight/util/ssl.rb +1 -25
  118. data/lib/skylight/util.rb +12 -0
  119. data/lib/skylight/vendor/cli/thor/rake_compat.rb +1 -1
  120. data/lib/skylight/version.rb +5 -1
  121. data/lib/skylight/vm/gc.rb +60 -0
  122. data/lib/skylight.rb +201 -14
  123. metadata +134 -18
@@ -0,0 +1,156 @@
1
+ module Skylight
2
+ module Probes
3
+ module Mongo
4
+ CAT = "db.mongo.command".freeze
5
+
6
+ class Probe
7
+ def install
8
+ ::Mongo::Monitoring::Global.subscribe(::Mongo::Monitoring::COMMAND, Subscriber.new)
9
+ end
10
+ end
11
+
12
+ class Subscriber
13
+ include Skylight::Util::Logging
14
+
15
+ COMMANDS = %i[insert find count distinct update findandmodify findAndModify delete aggregate].freeze
16
+
17
+ COMMAND_NAMES = { findandmodify: "findAndModify".freeze }.freeze
18
+
19
+ def initialize
20
+ @events = {}
21
+ end
22
+
23
+ def started(event)
24
+ begin_instrumentation(event)
25
+ end
26
+
27
+ def succeeded(event)
28
+ end_instrumentation(event)
29
+ end
30
+
31
+ def failed(event)
32
+ end_instrumentation(event)
33
+ end
34
+
35
+ # For logging
36
+ def config
37
+ Skylight.config
38
+ end
39
+
40
+ private
41
+
42
+ def begin_instrumentation(event)
43
+ return unless COMMANDS.include?(event.command_name.to_sym)
44
+
45
+ command_name = COMMAND_NAMES[event.command_name] || event.command_name.to_s
46
+
47
+ title = "#{event.database_name}.#{command_name}"
48
+
49
+ command = event.command
50
+
51
+ # Not sure if this will always exist
52
+ # Delete so the description will be less redundant
53
+ if (target = command[event.command_name])
54
+ title << " #{target}"
55
+ end
56
+
57
+ payload = {}
58
+
59
+ # Ruby Hashes are ordered based on insertion so do the most important ones first
60
+
61
+ add_value("key".freeze, command, payload)
62
+ add_bound("query".freeze, command, payload)
63
+ add_bound("filter".freeze, command, payload)
64
+ add_value("sort".freeze, command, payload)
65
+
66
+ add_bound("update".freeze, command, payload) if command_name == "findAndModify".freeze
67
+
68
+ add_value("remove".freeze, command, payload)
69
+ add_value("new".freeze, command, payload)
70
+
71
+ if (updates = command["updates".freeze])
72
+ # AFAICT the gem generally just sends one item in the updates array
73
+ update = updates[0]
74
+ update_payload = {}
75
+ add_bound("q".freeze, update, update_payload)
76
+ add_bound("u".freeze, update, update_payload)
77
+ add_value("multi".freeze, update, update_payload)
78
+ add_value("upsert".freeze, update, update_payload)
79
+
80
+ payload["updates".freeze] = [update_payload]
81
+
82
+ payload["updates".freeze] << "..." if updates.length > 1
83
+ end
84
+
85
+ if (deletes = command["deletes".freeze])
86
+ # AFAICT the gem generally just sends one item in the updates array
87
+ delete = deletes[0]
88
+ delete_payload = {}
89
+ add_bound("q".freeze, delete, delete_payload)
90
+ add_value("limit".freeze, delete, delete_payload)
91
+
92
+ payload["deletes".freeze] = [delete_payload]
93
+
94
+ payload["deletes".freeze] << "..." if deletes.length > 1
95
+ end
96
+
97
+ if (pipeline = command["pipeline".freeze])
98
+ payload["pipeline".freeze] = pipeline.map { |segment| extract_binds(segment) }
99
+ end
100
+
101
+ # We're ignoring documents from insert because they could have completely inconsistent
102
+ # format which would make it hard to merge.
103
+
104
+ opts = {
105
+ category: CAT,
106
+ title: title,
107
+ description: payload.empty? ? nil : payload.to_json,
108
+ meta: {
109
+ database: event.database_name
110
+ },
111
+ internal: true
112
+ }
113
+
114
+ @events[event.operation_id] = Skylight.instrument(opts)
115
+ rescue Exception => e
116
+ error "failed to begin instrumentation for Mongo; msg=%s", e.message
117
+ end
118
+
119
+ def end_instrumentation(event)
120
+ if (original_event = @events.delete(event.operation_id))
121
+ meta = {}
122
+ if event.is_a?(::Mongo::Monitoring::Event::CommandFailed)
123
+ meta[:exception] = ["CommandFailed", event.message]
124
+ end
125
+ Skylight.done(original_event, meta)
126
+ end
127
+ rescue Exception => e
128
+ error "failed to end instrumentation for Mongo; msg=%s", e.message
129
+ end
130
+
131
+ def add_value(key, command, payload)
132
+ if command.key?(key)
133
+ value = command[key]
134
+ payload[key] = value
135
+ end
136
+ end
137
+
138
+ def add_bound(key, command, payload)
139
+ if (value = command[key])
140
+ payload[key] = extract_binds(value)
141
+ end
142
+ end
143
+
144
+ def extract_binds(hash)
145
+ ret = {}
146
+
147
+ hash.each { |k, v| ret[k] = v.is_a?(Hash) ? extract_binds(v) : "?".freeze }
148
+
149
+ ret
150
+ end
151
+ end
152
+ end
153
+
154
+ register(:mongo, "Mongo", "mongo", Mongo::Probe.new)
155
+ end
156
+ end
@@ -0,0 +1,13 @@
1
+ module Skylight
2
+ module Probes
3
+ module Mongoid
4
+ class Probe
5
+ def install
6
+ Skylight::Probes.probe(:mongo)
7
+ end
8
+ end
9
+ end
10
+
11
+ register(:mongoid, "Mongoid", "mongoid", Mongoid::Probe.new)
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ require "skylight/formatters/http"
2
+
3
+ module Skylight
4
+ module Probes
5
+ module NetHTTP
6
+ module Instrumentation
7
+ def request(req, *)
8
+ return super if !started? || Probes::NetHTTP::Probe.disabled?
9
+
10
+ method = req.method
11
+
12
+ # req['host'] also includes special handling for default ports
13
+ host, port = req["host"] ? req["host"].split(":") : nil
14
+
15
+ # If we're connected with a persistent socket
16
+ host ||= address
17
+
18
+ path = req.path
19
+ scheme = use_ssl? ? "https" : "http"
20
+
21
+ # Contained in the path
22
+ query = nil
23
+
24
+ opts = Formatters::HTTP.build_opts(method, scheme, host, port, path, query)
25
+
26
+ Skylight.instrument(opts) { super }
27
+ end
28
+ end
29
+
30
+ # Probe for instrumenting Net::HTTP requests. Works by monkeypatching the default Net::HTTP#request method.
31
+ class Probe
32
+ DISABLED_KEY = :__skylight_net_http_disabled
33
+
34
+ def self.disable
35
+ state_was = Thread.current[DISABLED_KEY]
36
+ Thread.current[DISABLED_KEY] = true
37
+ yield
38
+ ensure
39
+ Thread.current[DISABLED_KEY] = state_was
40
+ end
41
+
42
+ def self.disabled?
43
+ !!Thread.current[DISABLED_KEY]
44
+ end
45
+
46
+ def install
47
+ Net::HTTP.prepend(Instrumentation)
48
+ end
49
+ end
50
+ end
51
+
52
+ register(:net_http, "Net::HTTP", "net/http", NetHTTP::Probe.new)
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ module Skylight
2
+ module Probes
3
+ module Rack
4
+ module Builder
5
+ module Instrumentation
6
+ def use(middleware, *args, &block)
7
+ if @map
8
+ mapping = @map
9
+ @map = nil
10
+ @use << proc { |app| generate_map(app, mapping) }
11
+ end
12
+ @use << proc do |app|
13
+ middleware
14
+ .new(app, *args, &block)
15
+ .tap do |middleware_instance|
16
+ Skylight::Probes::Middleware::Instrumentation.sk_instrument_middleware(middleware_instance)
17
+ end
18
+ end
19
+ end
20
+ ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
21
+ end
22
+
23
+ class Probe
24
+ def install
25
+ if defined?(::Rack.release) && Gem::Version.new(::Rack.release) >= ::Gem::Version.new("1.4") &&
26
+ defined?(::Rack::Builder)
27
+ require "skylight/probes/middleware"
28
+ ::Rack::Builder.prepend(Instrumentation)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ register(:rack_builder, "Rack::Builder", "rack/builder", Skylight::Probes::Rack::Builder::Probe.new)
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ module Skylight
2
+ module Probes
3
+ module Redis
4
+ # Unfortunately, because of the nature of pipelining, there's no way for us to
5
+ # give a time breakdown on the individual items.
6
+
7
+ PIPELINED_OPTS = { category: "db.redis.pipelined".freeze, title: "PIPELINE".freeze, internal: true }.freeze
8
+
9
+ MULTI_OPTS = { category: "db.redis.multi".freeze, title: "MULTI".freeze, internal: true }.freeze
10
+
11
+ module ClientInstrumentation
12
+ def call(command, *)
13
+ command_name = command[0]
14
+
15
+ return super if command_name == :auth
16
+
17
+ opts = { category: "db.redis.command", title: command_name.upcase.to_s, internal: true }
18
+
19
+ Skylight.instrument(opts) { super }
20
+ end
21
+ end
22
+
23
+ module Instrumentation
24
+ def pipelined(*)
25
+ Skylight.instrument(PIPELINED_OPTS) { super }
26
+ end
27
+
28
+ def multi(*)
29
+ Skylight.instrument(MULTI_OPTS) { super }
30
+ end
31
+ end
32
+
33
+ class Probe
34
+ def install
35
+ version = defined?(::Redis::VERSION) ? Gem::Version.new(::Redis::VERSION) : nil
36
+
37
+ if !version || version < Gem::Version.new("3.0.0")
38
+ Skylight.error "The installed version of Redis doesn't support Middlewares. " \
39
+ "At least version 3.0.0 is required."
40
+ return
41
+ end
42
+
43
+ ::Redis::Client.prepend(ClientInstrumentation)
44
+ ::Redis.prepend(Instrumentation)
45
+ end
46
+ end
47
+ end
48
+
49
+ register(:redis, "Redis", "redis", Redis::Probe.new)
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ # Supports 3.12.0+
2
+ module Skylight
3
+ module Probes
4
+ module Sequel
5
+ class Probe
6
+ def install
7
+ require "sequel/database/logging"
8
+
9
+ method_name = ::Sequel::Database.method_defined?(:log_connection_yield) ? "log_connection_yield" : "log_yield"
10
+
11
+ mod =
12
+ Module.new do
13
+ define_method method_name do |sql, *args, &block|
14
+ super(sql, *args) do
15
+ ::ActiveSupport::Notifications.instrument("sql.sequel", sql: sql, name: "SQL", binds: args) do
16
+ block.call
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ ::Sequel::Database.prepend(mod)
23
+ end
24
+ end
25
+ end
26
+
27
+ register(:sequel, "Sequel", "sequel", Sequel::Probe.new)
28
+ end
29
+ end
@@ -0,0 +1,66 @@
1
+ module Skylight
2
+ module Probes
3
+ module Sinatra
4
+ module ClassInstrumentation
5
+ def compile!(verb, path, *)
6
+ super.tap do |_, _, keys_or_wrapper, wrapper|
7
+ wrapper ||= keys_or_wrapper
8
+
9
+ # Deal with the situation where the path is a regex, and the default behavior
10
+ # of Ruby stringification produces an unreadable mess
11
+ if path.is_a?(Regexp)
12
+ human_readable = "<sk-regex>%r{#{path.source}}</sk-regex>"
13
+ wrapper.instance_variable_set(:@route_name, "#{verb} #{human_readable}")
14
+ else
15
+ wrapper.instance_variable_set(:@route_name, "#{verb} #{path}")
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ module Instrumentation
22
+ def dispatch!(*)
23
+ super.tap do
24
+ if (trace = Skylight.instrumenter&.current_trace) && (route = env["sinatra.route"])
25
+ # Include the app's mount point (if available)
26
+ script_name = trace.instrumenter.config.sinatra_route_prefixes? && env["SCRIPT_NAME"]
27
+
28
+ trace.endpoint =
29
+ if script_name && !script_name.empty?
30
+ verb, path = route.split(" ", 2)
31
+ "#{verb} [#{script_name}]#{path}"
32
+ else
33
+ route
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def compile_template(engine, data, options, *)
40
+ super.tap do |template|
41
+ if defined?(::Tilt::Template) && template.is_a?(::Tilt::Template)
42
+ # Pass along a useful "virtual path" to Tilt. The Tilt probe will handle
43
+ # instrumenting correctly.
44
+ virtual_path = data.is_a?(Symbol) ? data.to_s : "Inline template (#{engine})"
45
+ template.instance_variable_set(:@__sky_virtual_path, virtual_path)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ class Probe
52
+ def install
53
+ if ::Sinatra::VERSION < "1.4.0"
54
+ Skylight.error "Sinatra must be version 1.4.0 or greater."
55
+ return
56
+ end
57
+
58
+ ::Sinatra::Base.singleton_class.prepend(ClassInstrumentation)
59
+ ::Sinatra::Base.prepend(Instrumentation)
60
+ end
61
+ end
62
+ end
63
+
64
+ register(:sinatra, "Sinatra::Base", "sinatra/base", Sinatra::Probe.new)
65
+ end
66
+ end
@@ -1,20 +1,20 @@
1
1
  module Skylight
2
2
  module Probes
3
3
  module Sinatra
4
- class Probe
5
- def install
6
- class << ::Sinatra::Base
7
- alias_method :build_without_sk, :build
4
+ module Instrumentation
5
+ def setup_default_middleware(builder)
6
+ builder.use Skylight::Middleware
7
+ super
8
+ end
9
+ end
8
10
 
9
- def build(*args, &block)
10
- use Skylight::Middleware
11
- build_without_sk(*args, &block)
12
- end
13
- end
11
+ class AddMiddlewareProbe
12
+ def install
13
+ ::Sinatra::Base.singleton_class.prepend(Instrumentation)
14
14
  end
15
15
  end
16
16
  end
17
17
 
18
- Skylight::Core::Probes.register(:sinatra_add_middleware, "Sinatra::Base", "sinatra/base", Sinatra::Probe.new)
18
+ register(:sinatra_add_middleware, "Sinatra::Base", "sinatra/base", Sinatra::AddMiddlewareProbe.new)
19
19
  end
20
20
  end
@@ -0,0 +1,25 @@
1
+ # Should support 0.2+, though not tested against older versions
2
+ module Skylight
3
+ module Probes
4
+ module Tilt
5
+ module Instrumentation
6
+ def render(*args, &block)
7
+ opts = {
8
+ category: "view.render.template",
9
+ title: @__sky_virtual_path || options[:sky_virtual_path] || basename || "Unknown template name"
10
+ }
11
+
12
+ Skylight.instrument(opts) { super(*args, &block) }
13
+ end
14
+ end
15
+
16
+ class Probe
17
+ def install
18
+ ::Tilt::Template.prepend(Instrumentation)
19
+ end
20
+ end
21
+ end
22
+
23
+ register(:tilt, "Tilt::Template", "tilt/template", Tilt::Probe.new)
24
+ end
25
+ 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