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,96 @@
1
+ module Skylight
2
+ module Probes
3
+ module ActiveRecord
4
+ module FutureResult
5
+ # Applied to ActiveSupport::Notifications::Event
6
+ module AsyncEventExtensions
7
+ # Notify Skylight that the event has started
8
+ def __sk_start!
9
+ subscriber = Skylight.instrumenter.subscriber
10
+ subscriber.start(name, nil, payload)
11
+ trace = Skylight.instrumenter.current_trace
12
+
13
+ # Set a finisher to end the event
14
+ @__sk_finisher = ->(name, payload) do
15
+ subscriber.with_trace(trace) { subscriber.finish(name, nil, payload) }
16
+ end
17
+
18
+ # End it immediately if we've actually already ended
19
+ __sk_finish! if @end
20
+ rescue StandardError => e
21
+ Skylight.error("Unable to start event for FutureResult: #{e}")
22
+ end
23
+
24
+ # Notify Skylight that the event has finished
25
+ def __sk_finish!
26
+ return unless @__sk_finisher
27
+
28
+ @__sk_finisher.call(name, payload)
29
+ @__sk_finisher = nil
30
+ rescue StandardError => e
31
+ Skylight.error("Unable to finish event for FutureResult: #{e}")
32
+ end
33
+
34
+ # When the event is marked as finish make sure we notify Skylight
35
+ def finish!
36
+ super
37
+ __sk_finish!
38
+ end
39
+ end
40
+
41
+ # Applied to FutureResult
42
+ module Instrumentation
43
+ def result(*, **)
44
+ # This instruments the whole FutureResult so that we know when we were executing (potenially) async.
45
+ ActiveSupport::Notifications.instrument("future_result.active_record", { args: @args, kwargs: @kwargs }) do
46
+ super
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def execute_or_wait(*, **)
53
+ # At this point we're actually waiting for the query to have finished executing.
54
+
55
+ begin
56
+ # If the query has already started async, the @event_buffer will be defined.
57
+ # We grab the events (currently only the SQL queries), extend them with our
58
+ # special methods and notify Skylight.
59
+ # We act as if the event has just stared, though the query may already have been
60
+ # running. This means we're essentially just logging blocking time right now.
61
+
62
+ # Dup here just in case more get added somehow during the super call
63
+ events = @event_buffer&.instance_variable_get(:@events).dup
64
+
65
+ events&.each do |event|
66
+ event.singleton_class.prepend(AsyncEventExtensions)
67
+ event.__sk_start!
68
+ end
69
+ rescue StandardError => e
70
+ Skylight.error("Unable to start events for FutureResult: #{e}")
71
+ end
72
+
73
+ super
74
+ ensure
75
+ # Once we've actually got a result, we mark each one as finished.
76
+ # Note that it may have already finished, but if it didn't we need to say so now.
77
+ events&.reverse_each(&:__sk_finish!)
78
+ end
79
+ end
80
+
81
+ class Probe
82
+ def install
83
+ ::ActiveRecord::FutureResult.prepend(Instrumentation)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ register(
90
+ :active_record_async,
91
+ "ActiveRecord::FutureResult",
92
+ "active_record/future_result",
93
+ ActiveRecord::FutureResult::Probe.new
94
+ )
95
+ end
96
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module Skylight
6
+ module Probes
7
+ module DelayedJob
8
+ begin
9
+ require "delayed/plugin"
10
+
11
+ class Plugin < ::Delayed::Plugin
12
+ callbacks do |lifecycle|
13
+ lifecycle.around(:perform) { |worker, job, &block| sk_instrument(worker, job, &block) }
14
+
15
+ lifecycle.after(:error) { |_worker, _job| Skylight.trace&.segment = "error" }
16
+ end
17
+
18
+ class << self
19
+ include Skylight::Util::Logging
20
+
21
+ def sk_instrument(_worker, job)
22
+ endpoint = Skylight::Probes::DelayedJob.handler_name(job)
23
+
24
+ Skylight.trace(
25
+ endpoint,
26
+ "app.delayed_job.worker",
27
+ "Delayed::Worker#run",
28
+ component: :worker,
29
+ segment: job.queue,
30
+ meta: {
31
+ source_location: "delayed_job"
32
+ }
33
+ ) do
34
+ t { "Delayed::Job beginning trace" }
35
+ yield
36
+ end
37
+ end
38
+ end
39
+ end
40
+ rescue LoadError
41
+ $stderr.puts "[SKYLIGHT] The delayed_job probe was requested, but Delayed::Plugin was not defined."
42
+ end
43
+
44
+ UNKNOWN = "<Delayed::Job Unknown>"
45
+
46
+ def self.handler_name(job)
47
+ payload_object =
48
+ job.respond_to?(:payload_object_without_sk) ? job.payload_object_without_sk : job.payload_object
49
+
50
+ payload_object_name(payload_object)
51
+ end
52
+
53
+ def self.payload_object_name(payload_object)
54
+ if payload_object.is_a?(::Delayed::PerformableMethod)
55
+ payload_object.display_name
56
+ else
57
+ # In the case of ActiveJob-wrapped jobs, there is quite a bit of job-specific metadata
58
+ # in `job.name`, which would break aggregation and potentially leak private data in job args.
59
+ # Use class name instead to avoid this.
60
+ payload_object.class.name
61
+ end
62
+ rescue StandardError
63
+ UNKNOWN
64
+ end
65
+
66
+ def self.payload_object_source_meta(payload_object)
67
+ if payload_object.is_a?(::Delayed::PerformableMethod)
68
+ if payload_object.object.is_a?(Module)
69
+ [:class_method, payload_object.object.name, payload_object.method_name.to_s]
70
+ else
71
+ [:instance_method, payload_object.object.class.name, payload_object.method_name.to_s]
72
+ end
73
+ else
74
+ [:instance_method, payload_object.class.name, "perform"]
75
+ end
76
+ end
77
+
78
+ class InstrumentationProxy < SimpleDelegator
79
+ def perform
80
+ source_meta = Skylight::Probes::DelayedJob.payload_object_source_meta(__getobj__)
81
+
82
+ opts = {
83
+ category: "app.delayed_job.job",
84
+ title: format_source(*source_meta),
85
+ meta: {
86
+ source_location_hint: source_meta
87
+ },
88
+ internal: true
89
+ }
90
+
91
+ Skylight.instrument(opts) { __getobj__.perform }
92
+ end
93
+
94
+ # Used by Delayed::Backend::Base to determine Job#name
95
+ def display_name
96
+ __getobj__.respond_to?(:display_name) ? __getobj__.display_name : __getobj__.class.name
97
+ end
98
+
99
+ private
100
+
101
+ def format_source(method_type, constant_name, method_name)
102
+ method_type == :instance_method ? "#{constant_name}##{method_name}" : "#{constant_name}.#{method_name}"
103
+ end
104
+ end
105
+
106
+ class Probe
107
+ def install
108
+ return unless validate_version && plugin_defined?
109
+
110
+ ::Delayed::Worker.plugins = [Skylight::Probes::DelayedJob::Plugin] | ::Delayed::Worker.plugins
111
+ ::Delayed::Backend::Base.class_eval do
112
+ alias_method :payload_object_without_sk, :payload_object
113
+
114
+ def payload_object
115
+ Skylight::Probes::DelayedJob::InstrumentationProxy.new(payload_object_without_sk)
116
+ end
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def plugin_defined?
123
+ defined?(::Skylight::Probes::DelayedJob::Plugin)
124
+ end
125
+
126
+ def validate_version
127
+ spec = Gem.loaded_specs["delayed_job"]
128
+ version = spec&.version
129
+
130
+ if !version || version < Gem::Version.new("4.0.0")
131
+ Skylight.error "The installed version of DelayedJob is not supported on Skylight. " \
132
+ "Your jobs will not be tracked."
133
+
134
+ return false
135
+ end
136
+
137
+ true
138
+ end
139
+ end
140
+ end
141
+
142
+ register(:delayed_job, "Delayed::Worker", "delayed_job", DelayedJob::Probe.new)
143
+ end
144
+ end
@@ -0,0 +1,36 @@
1
+ module Skylight
2
+ module Probes
3
+ module Elasticsearch
4
+ class Probe
5
+ def install
6
+ # Prepending doesn't work here since this a module that's already been included
7
+ ::Elasticsearch::Transport::Transport::Base.class_eval do
8
+ alias_method :perform_request_without_sk, :perform_request
9
+ def perform_request(method, path, *args, &block)
10
+ ActiveSupport::Notifications.instrument(
11
+ "request.elasticsearch",
12
+ name: "Request",
13
+ method: method,
14
+ path: path
15
+ ) do
16
+ # Prevent HTTP-related probes from firing
17
+ Skylight::Normalizers::Faraday::Request.disable do
18
+ disable_skylight_probe(:NetHTTP) do
19
+ disable_skylight_probe(:HTTPClient) { perform_request_without_sk(method, path, *args, &block) }
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def disable_skylight_probe(class_name)
26
+ klass = ::ActiveSupport::Inflector.safe_constantize("Skylight::Probes::#{class_name}::Probe")
27
+ (klass ? klass.disable { yield } : yield).tap { Skylight.log(:debug, "re-enabling: #{klass}") }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ register(:elasticsearch, "Elasticsearch", "elasticsearch", Elasticsearch::Probe.new)
35
+ end
36
+ end
@@ -0,0 +1,65 @@
1
+ require "skylight/formatters/http"
2
+
3
+ module Skylight
4
+ module Probes
5
+ module Excon
6
+ # Middleware for Excon that instruments requests
7
+ class Middleware < ::Excon::Middleware::Base
8
+ def initialize(*)
9
+ @requests = {}.compare_by_identity
10
+ super
11
+ end
12
+
13
+ # TODO: Review the following:
14
+ # - Consider whether a LIFO queue would be sufficient
15
+ # - Check that errors can't be called without a request
16
+
17
+ def request_call(datum)
18
+ begin_instrumentation(datum)
19
+ super
20
+ end
21
+
22
+ def response_call(datum)
23
+ super
24
+ ensure
25
+ end_instrumentation(datum)
26
+ end
27
+
28
+ def error_call(datum)
29
+ super
30
+ ensure
31
+ end_instrumentation(datum)
32
+ end
33
+
34
+ private
35
+
36
+ def begin_instrumentation(datum)
37
+ method = datum[:method].to_s
38
+ scheme = datum[:scheme]
39
+ host = datum[:host]
40
+
41
+ # TODO: Maybe don't show other default ports like 443
42
+ port = datum[:port] == 80 ? nil : datum[:port]
43
+ path = datum[:path]
44
+ query = datum[:query]
45
+
46
+ opts = Formatters::HTTP.build_opts(method, scheme, host, port, path, query)
47
+
48
+ @requests[datum] = Skylight.instrument(opts)
49
+ rescue Exception => e
50
+ Skylight.error "failed to begin instrumentation for Excon; msg=%s", e.message
51
+ end
52
+
53
+ def end_instrumentation(datum)
54
+ if (request = @requests.delete(datum))
55
+ meta = {}
56
+ meta[:exception_object] = datum[:error] if datum[:error].is_a?(Exception)
57
+ Skylight.done(request, meta)
58
+ end
59
+ rescue Exception => e
60
+ Skylight.error "failed to end instrumentation for Excon; msg=%s", e.message
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,25 @@
1
+ module Skylight
2
+ module Probes
3
+ module Excon
4
+ # Probe for instrumenting Excon requests. Installs {Excon::Middleware} to achieve this.
5
+ class Probe
6
+ def install
7
+ if defined?(::Excon::Middleware)
8
+ # Don't require until installation since it depends on Excon being loaded
9
+ require "skylight/probes/excon/middleware"
10
+
11
+ idx = ::Excon.defaults[:middlewares].index(::Excon::Middleware::Instrumentor)
12
+
13
+ # TODO: Handle possibility of idx being nil
14
+ ::Excon.defaults[:middlewares].insert(idx, Probes::Excon::Middleware)
15
+ else
16
+ Skylight.error "The installed version of Excon doesn't support Middlewares. " \
17
+ "The Excon probe will be disabled."
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ register(:excon, "Excon", "excon", Excon::Probe.new)
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Skylight
2
+ module Probes
3
+ module Faraday
4
+ module Instrumentation
5
+ def builder
6
+ unless defined?(@__sk__setup)
7
+ @__sk__setup = true
8
+ @builder.insert 0, ::Faraday::Request::Instrumentation
9
+ end
10
+ @builder
11
+ end
12
+ end
13
+
14
+ class Probe
15
+ def install
16
+ ::Faraday::Connection.prepend(Instrumentation)
17
+ end
18
+ end
19
+ end
20
+
21
+ register(:faraday, "Faraday", "faraday", Faraday::Probe.new)
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector"
4
+
5
+ module Skylight
6
+ module Probes
7
+ module GraphQL
8
+ module Instrumentation
9
+ def initialize(*, **)
10
+ super
11
+
12
+ return unless defined?(@tracers)
13
+
14
+ unless @tracers.include?(::GraphQL::Tracing::ActiveSupportNotificationsTracing)
15
+ @tracers << ::GraphQL::Tracing::ActiveSupportNotificationsTracing
16
+ end
17
+ end
18
+ end
19
+
20
+ class Probe
21
+ def install
22
+ tracing_klass_name = "::GraphQL::Tracing::ActiveSupportNotificationsTracing"
23
+ klasses_to_probe = %w[::GraphQL::Execution::Multiplex ::GraphQL::Query]
24
+
25
+ return unless ([tracing_klass_name] + klasses_to_probe).all?(&method(:safe_constantize))
26
+
27
+ klasses_to_probe.each { |klass_name| safe_constantize(klass_name).prepend(Instrumentation) }
28
+ end
29
+
30
+ def safe_constantize(klass_name)
31
+ ActiveSupport::Inflector.safe_constantize(klass_name)
32
+ end
33
+ end
34
+ end
35
+
36
+ register(:graphql, "GraphQL", "graphql", GraphQL::Probe.new)
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ require "skylight/formatters/http"
2
+
3
+ module Skylight
4
+ module Probes
5
+ module HTTPClient
6
+ module Instrumentation
7
+ # HTTPClient has request methods on the class object itself,
8
+ # but they internally instantiate a client and perform the method
9
+ # on that, so this instance method override will cover both
10
+ # `HTTPClient.get(...)` and `HTTPClient.new.get(...)`
11
+
12
+ def do_request(method, uri, *)
13
+ return super if Probes::HTTPClient::Probe.disabled?
14
+
15
+ opts = Formatters::HTTP.build_opts(method, uri.scheme, uri.host, uri.port, uri.path, uri.query)
16
+
17
+ Skylight.instrument(opts) { super }
18
+ end
19
+ end
20
+
21
+ class Probe
22
+ DISABLED_KEY = :__skylight_httpclient_disabled
23
+
24
+ def self.disable
25
+ old_value = Thread.current[DISABLED_KEY]
26
+ Thread.current[DISABLED_KEY] = true
27
+ yield
28
+ ensure
29
+ Thread.current[DISABLED_KEY] = old_value
30
+ end
31
+
32
+ def self.disabled?
33
+ !!Thread.current[DISABLED_KEY]
34
+ end
35
+
36
+ def install
37
+ ::HTTPClient.prepend(Instrumentation)
38
+ end
39
+ end
40
+ end
41
+
42
+ register(:httpclient, "HTTPClient", "httpclient", HTTPClient::Probe.new)
43
+ end
44
+ end
@@ -0,0 +1,135 @@
1
+ module Skylight
2
+ module Probes
3
+ module Middleware
4
+ # for Rails >= 6.0, which includes InstrumentationProxy
5
+ module InstrumentationExtensions
6
+ def initialize(middleware, class_name)
7
+ super
8
+
9
+ # NOTE: Caching here leads to better performance, but will not notice if the method is overridden
10
+ # We don't have access to the config here so we can't check whether source locations are enabled.
11
+ # However, this only happens once per middleware so it should be minimal impact.
12
+ @payload[:sk_source_location] =
13
+ begin
14
+ if middleware.is_a?(Proc)
15
+ middleware.source_location
16
+ elsif middleware.respond_to?(:call)
17
+ middleware.method(:call).source_location
18
+ end
19
+ rescue StandardError
20
+ nil
21
+ end
22
+ end
23
+ end
24
+
25
+ # for Rails <= 5.2 ActionDispatch::MiddlewareStack::Middleware
26
+ module Instrumentation
27
+ def build(*)
28
+ Instrumentation.sk_instrument_middleware(super)
29
+ end
30
+
31
+ def self.sk_instrument_middleware(middleware)
32
+ return middleware if middleware.is_a?(Skylight::Middleware)
33
+
34
+ # Not sure how this would actually happen
35
+ return middleware if middleware.respond_to?(:__has_sk__)
36
+
37
+ # On Rails 3, ActionDispatch::Session::CookieStore is frozen, for one
38
+ return middleware if middleware.frozen?
39
+
40
+ Skylight::Probes::Middleware::Probe.add_instrumentation(middleware)
41
+
42
+ middleware
43
+ end
44
+ end
45
+
46
+ class Probe
47
+ DISABLED_KEY = :__skylight_middleware_disabled
48
+
49
+ def self.disable!
50
+ @disabled = true
51
+ end
52
+
53
+ def self.enable!
54
+ @disabled = false
55
+ end
56
+
57
+ def self.disabled?
58
+ !!@disabled
59
+ end
60
+
61
+ module InstanceInstrumentation
62
+ def call(*args)
63
+ return super(*args) if Skylight::Probes::Middleware::Probe.disabled?
64
+
65
+ trace = Skylight.instrumenter&.current_trace
66
+ return super(*args) unless trace
67
+
68
+ begin
69
+ name = self.class.name || __sk_default_name
70
+
71
+ trace.endpoint = name
72
+
73
+ source_file, source_line = method(__method__).super_method.source_location
74
+
75
+ spans =
76
+ Skylight.instrument(
77
+ title: name,
78
+ category: __sk_category,
79
+ source_file: source_file,
80
+ source_line: source_line
81
+ )
82
+
83
+ proxied_response =
84
+ Skylight::Middleware.with_after_close(super(*args), debug_identifier: "Middleware: #{name}") do
85
+ Skylight.done(spans)
86
+ end
87
+ rescue Exception => e
88
+ Skylight.done(spans, exception_object: e)
89
+ raise
90
+ ensure
91
+ unless e || proxied_response
92
+ # If we've gotten to this point, the most likely scenario is that
93
+ # a throw/catch has bypassed a portion of the callstack. Since these spans would not otherwise
94
+ # be closed, mark them deferred to indicate that they should be implicitly closed.
95
+ # See Trace#deferred_spans or Trace#stop for more information.
96
+ Skylight.done(spans, defer: true)
97
+ end
98
+ end
99
+ end
100
+
101
+ def __sk_default_name
102
+ "Anonymous Middleware"
103
+ end
104
+
105
+ def __sk_category
106
+ "rack.middleware"
107
+ end
108
+
109
+ def __has_sk__
110
+ true
111
+ end
112
+ end
113
+
114
+ def self.add_instrumentation(middleware)
115
+ middleware.singleton_class.prepend(InstanceInstrumentation)
116
+ end
117
+
118
+ def install
119
+ if defined?(::ActionDispatch::MiddlewareStack::InstrumentationProxy)
120
+ ::ActionDispatch::MiddlewareStack::InstrumentationProxy.prepend(InstrumentationExtensions)
121
+ else
122
+ ::ActionDispatch::MiddlewareStack::Middleware.prepend(Instrumentation)
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ register(
129
+ :middleware,
130
+ "ActionDispatch::MiddlewareStack::Middleware",
131
+ "actionpack/action_dispatch",
132
+ Middleware::Probe.new
133
+ )
134
+ end
135
+ end