skylight 5.0.0.beta → 5.0.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -6
  3. data/CONTRIBUTING.md +1 -1
  4. data/ext/extconf.rb +2 -2
  5. data/ext/libskylight.yml +7 -5
  6. data/lib/skylight.rb +9 -2
  7. data/lib/skylight/api.rb +3 -0
  8. data/lib/skylight/cli/doctor.rb +11 -13
  9. data/lib/skylight/config.rb +25 -32
  10. data/lib/skylight/deprecation.rb +3 -1
  11. data/lib/skylight/errors.rb +4 -4
  12. data/lib/skylight/extensions.rb +8 -0
  13. data/lib/skylight/extensions/source_location.rb +123 -81
  14. data/lib/skylight/formatters/http.rb +2 -1
  15. data/lib/skylight/helpers.rb +44 -35
  16. data/lib/skylight/instrumenter.rb +3 -2
  17. data/lib/skylight/middleware.rb +4 -4
  18. data/lib/skylight/native.rb +1 -1
  19. data/lib/skylight/native_ext_fetcher.rb +2 -2
  20. data/lib/skylight/normalizers.rb +6 -4
  21. data/lib/skylight/normalizers/action_controller/process_action.rb +1 -1
  22. data/lib/skylight/normalizers/action_dispatch/route_set.rb +1 -1
  23. data/lib/skylight/normalizers/active_job/perform.rb +5 -0
  24. data/lib/skylight/normalizers/graphql/base.rb +1 -0
  25. data/lib/skylight/normalizers/render.rb +1 -1
  26. data/lib/skylight/normalizers/shrine.rb +34 -0
  27. data/lib/skylight/normalizers/sql.rb +3 -2
  28. data/lib/skylight/probes.rb +38 -10
  29. data/lib/skylight/probes/active_job.rb +4 -6
  30. data/lib/skylight/probes/active_job_enqueue.rb +18 -14
  31. data/lib/skylight/probes/active_model_serializers.rb +2 -6
  32. data/lib/skylight/probes/delayed_job.rb +112 -25
  33. data/lib/skylight/probes/elasticsearch.rb +1 -1
  34. data/lib/skylight/probes/excon/middleware.rb +4 -4
  35. data/lib/skylight/probes/middleware.rb +2 -1
  36. data/lib/skylight/probes/mongo.rb +2 -1
  37. data/lib/skylight/probes/net_http.rb +0 -1
  38. data/lib/skylight/probes/redis.rb +6 -3
  39. data/lib/skylight/railtie.rb +1 -1
  40. data/lib/skylight/sidekiq.rb +12 -7
  41. data/lib/skylight/subscriber.rb +1 -1
  42. data/lib/skylight/trace.rb +10 -4
  43. data/lib/skylight/util/deploy.rb +3 -6
  44. data/lib/skylight/util/instrumenter_method.rb +11 -11
  45. data/lib/skylight/util/logging.rb +6 -6
  46. data/lib/skylight/util/lru_cache.rb +1 -3
  47. data/lib/skylight/util/platform.rb +1 -1
  48. data/lib/skylight/version.rb +1 -1
  49. metadata +27 -13
  50. data/lib/skylight/fanout.rb +0 -0
@@ -6,21 +6,25 @@ module Skylight
6
6
 
7
7
  def install
8
8
  ::ActiveJob::Base.around_enqueue do |job, block|
9
- begin
10
- job_class = job.class
11
- adapter_name = EnqueueProbe.normalize_adapter_name(job_class)
9
+ job_class = job.class
10
+ adapter_name = EnqueueProbe.normalize_adapter_name(job_class)
12
11
 
13
- # If this is an ActionMailer::DeliveryJob, we'll report this as the mailer title
14
- # and include ActionMailer::DeliveryJob in the description.
15
- name, job_class_name = Normalizers::ActiveJob::Perform.normalize_title(job)
16
- descriptors = ["adapter: '#{adapter_name}'", "queue: '#{job.queue_name}'"]
17
- descriptors << "job: '#{job_class_name}'" if job_class_name
18
- desc = "{ #{descriptors.join(', ')} }"
19
- rescue
20
- block.call
21
- else
22
- Skylight.instrument(title: "Enqueue #{name}", category: CAT, description: desc, &block)
23
- end
12
+ # If this is an ActionMailer::DeliveryJob, we'll report this as the mailer title
13
+ # and include ActionMailer::DeliveryJob in the description.
14
+ name, job_class_name = Normalizers::ActiveJob::Perform.normalize_title(job)
15
+ descriptors = ["adapter: '#{adapter_name}'", "queue: '#{job.queue_name}'"]
16
+ descriptors << "job: '#{job_class_name}'" if job_class_name
17
+ desc = "{ #{descriptors.join(', ')} }"
18
+ rescue
19
+ block.call
20
+ else
21
+ Skylight.instrument(
22
+ title: "Enqueue #{name}",
23
+ category: CAT,
24
+ description: desc,
25
+ internal: true,
26
+ &block
27
+ )
24
28
  end
25
29
 
26
30
  self.class.instance_eval do
@@ -14,12 +14,8 @@ module Skylight
14
14
 
15
15
  # File moved location between version
16
16
  %w[serializer serializers].each do |dir|
17
- # rubocop:disable Lint/SuppressedException
18
- begin
19
- require "active_model/#{dir}/version"
20
- rescue LoadError
21
- end
22
- # rubocop:enable Lint/SuppressedException
17
+ require "active_model/#{dir}/version"
18
+ rescue LoadError # rubocop:disable Lint/SuppressedException
23
19
  end
24
20
 
25
21
  if Gem.loaded_specs["active_model_serializers"]
@@ -1,46 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
1
5
  module Skylight
2
6
  module Probes
3
7
  module DelayedJob
4
- module Instrumentation
5
- include Skylight::Util::Logging
6
-
7
- def run(job, *)
8
- t { "Delayed::Job beginning trace" }
9
-
10
- handler_name =
11
- begin
12
- if defined?(::Delayed::PerformableMethod) && job.payload_object.is_a?(::Delayed::PerformableMethod)
13
- job.name
14
- else
15
- job.payload_object.class.name
16
- end
17
- rescue
18
- UNKNOWN
8
+ begin
9
+ require "delayed/plugin"
10
+
11
+ class Plugin < ::Delayed::Plugin
12
+ callbacks do |lifecycle|
13
+ lifecycle.around(:perform) do |worker, job, &block|
14
+ sk_instrument(worker, job, &block)
19
15
  end
20
16
 
21
- Skylight.trace(handler_name, "app.delayed_job.worker", "Delayed::Worker#run",
22
- component: :worker, segment: job.queue) { super }
17
+ lifecycle.after(:error) do |_worker, _job|
18
+ Skylight.trace&.segment = "error"
19
+ end
20
+ end
21
+
22
+ class << self
23
+ include Skylight::Util::Logging
24
+
25
+ def sk_instrument(_worker, job)
26
+ endpoint = Skylight::Probes::DelayedJob.handler_name(job)
27
+
28
+ Skylight.trace(endpoint,
29
+ "app.delayed_job.worker",
30
+ "Delayed::Worker#run",
31
+ component: :worker,
32
+ segment: job.queue,
33
+ meta: { source_location: "delayed_job" }) do
34
+ t { "Delayed::Job beginning trace" }
35
+ yield
36
+ end
37
+ end
38
+ end
23
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>"
24
45
 
25
- def handle_failed_job(*)
26
- super
27
- return unless Skylight.trace
46
+ def self.handler_name(job)
47
+ payload_object = if job.respond_to?(:payload_object_without_sk)
48
+ job.payload_object_without_sk
49
+ else
50
+ job.payload_object
51
+ end
52
+
53
+ payload_object_name(payload_object)
54
+ end
28
55
 
29
- Skylight.trace.segment = "error"
56
+ def self.payload_object_name(payload_object)
57
+ if payload_object.is_a?(::Delayed::PerformableMethod)
58
+ payload_object.display_name
59
+ else
60
+ # In the case of ActiveJob-wrapped jobs, there is quite a bit of job-specific metadata
61
+ # in `job.name`, which would break aggregation and potentially leak private data in job args.
62
+ # Use class name instead to avoid this.
63
+ payload_object.class.name
30
64
  end
65
+ rescue
66
+ UNKNOWN
31
67
  end
32
68
 
33
- class Probe
34
- UNKNOWN = "<Delayed::Job Unknown>".freeze
69
+ def self.payload_object_source_meta(payload_object)
70
+ if payload_object.is_a?(::Delayed::PerformableMethod)
71
+ if payload_object.object.is_a?(Module)
72
+ [:class_method, payload_object.object.name, payload_object.method_name.to_s]
73
+ else
74
+ [:instance_method, payload_object.object.class.name, payload_object.method_name.to_s]
75
+ end
76
+ else
77
+ [:instance_method, payload_object.class.name, "perform"]
78
+ end
79
+ end
35
80
 
81
+ class InstrumentationProxy < SimpleDelegator
82
+ def perform
83
+ source_meta = Skylight::Probes::DelayedJob.payload_object_source_meta(__getobj__)
84
+
85
+ opts = {
86
+ category: "app.delayed_job.job",
87
+ title: format_source(*source_meta),
88
+ meta: { source_location_hint: source_meta },
89
+ internal: true
90
+ }
91
+
92
+ Skylight.instrument(opts) { __getobj__.perform }
93
+ end
94
+
95
+ # Used by Delayed::Backend::Base to determine Job#name
96
+ def display_name
97
+ __getobj__.respond_to?(:display_name) ? __getobj__.display_name : __getobj__.class.name
98
+ end
99
+
100
+ private
101
+
102
+ def format_source(method_type, constant_name, method_name)
103
+ if method_type == :instance_method
104
+ "#{constant_name}##{method_name}"
105
+ else
106
+ "#{constant_name}.#{method_name}"
107
+ end
108
+ end
109
+ end
110
+
111
+ class Probe
36
112
  def install
37
- return unless validate_version
113
+ return unless validate_version && plugin_defined?
114
+
115
+ ::Delayed::Worker.plugins = [Skylight::Probes::DelayedJob::Plugin] | ::Delayed::Worker.plugins
116
+ ::Delayed::Backend::Base.class_eval do
117
+ alias_method :payload_object_without_sk, :payload_object
38
118
 
39
- ::Delayed::Worker.prepend(Instrumentation)
119
+ def payload_object
120
+ Skylight::Probes::DelayedJob::InstrumentationProxy.new(payload_object_without_sk)
121
+ end
122
+ end
40
123
  end
41
124
 
42
125
  private
43
126
 
127
+ def plugin_defined?
128
+ defined?(::Skylight::Probes::DelayedJob::Plugin)
129
+ end
130
+
44
131
  def validate_version
45
132
  spec = Gem.loaded_specs["delayed_job"]
46
133
  version = spec&.version
@@ -26,7 +26,7 @@ module Skylight
26
26
 
27
27
  def disable_skylight_probe(class_name)
28
28
  klass = ::ActiveSupport::Inflector.safe_constantize("Skylight::Probes::#{class_name}::Probe")
29
- (klass ? klass.disable { yield } : yield).tap { puts "re-enabling: #{klass}" }
29
+ (klass ? klass.disable { yield } : yield).tap { Skylight.log(:debug, "re-enabling: #{klass}") }
30
30
  end
31
31
  end
32
32
  end
@@ -6,7 +6,7 @@ module Skylight
6
6
  # Middleware for Excon that instruments requests
7
7
  class Middleware < ::Excon::Middleware::Base
8
8
  def initialize(*)
9
- @requests = {}
9
+ @requests = {}.compare_by_identity
10
10
  super
11
11
  end
12
12
 
@@ -38,19 +38,19 @@ module Skylight
38
38
  scheme = datum[:scheme]
39
39
  host = datum[:host]
40
40
  # TODO: Maybe don't show other default ports like 443
41
- port = datum[:port] != 80 ? datum[:port] : nil
41
+ port = datum[:port] == 80 ? nil : datum[:port]
42
42
  path = datum[:path]
43
43
  query = datum[:query]
44
44
 
45
45
  opts = Formatters::HTTP.build_opts(method, scheme, host, port, path, query)
46
46
 
47
- @requests[datum.object_id] = Skylight.instrument(opts)
47
+ @requests[datum] = Skylight.instrument(opts)
48
48
  rescue Exception => e
49
49
  Skylight.error "failed to begin instrumentation for Excon; msg=%s", e.message
50
50
  end
51
51
 
52
52
  def end_instrumentation(datum)
53
- if (request = @requests.delete(datum.object_id))
53
+ if (request = @requests.delete(datum))
54
54
  meta = {}
55
55
  if datum[:error].is_a?(Exception)
56
56
  meta[:exception_object] = datum[:error]
@@ -72,7 +72,8 @@ module Skylight
72
72
 
73
73
  source_file, source_line = method(__method__).super_method.source_location
74
74
 
75
- spans = Skylight.instrument(title: name, category: __sk_category, source_file: source_file, source_line: source_line)
75
+ spans = Skylight.instrument(title: name, category: __sk_category,
76
+ source_file: source_file, source_line: source_line)
76
77
 
77
78
  proxied_response =
78
79
  Skylight::Middleware.with_after_close(super(*args), debug_identifier: "Middleware: #{name}") do
@@ -113,7 +113,8 @@ module Skylight
113
113
  category: CAT,
114
114
  title: title,
115
115
  description: payload.empty? ? nil : payload.to_json,
116
- meta: { database: event.database_name }
116
+ meta: { database: event.database_name },
117
+ internal: true
117
118
  }
118
119
 
119
120
  @events[event.operation_id] = Skylight.instrument(opts)
@@ -14,7 +14,6 @@ module Skylight
14
14
 
15
15
  # If we're connected with a persistent socket
16
16
  host ||= address
17
- port ||= port
18
17
 
19
18
  path = req.path
20
19
  scheme = use_ssl? ? "https" : "http"
@@ -6,12 +6,14 @@ module Skylight
6
6
 
7
7
  PIPELINED_OPTS = {
8
8
  category: "db.redis.pipelined".freeze,
9
- title: "PIPELINE".freeze
9
+ title: "PIPELINE".freeze,
10
+ internal: true
10
11
  }.freeze
11
12
 
12
13
  MULTI_OPTS = {
13
14
  category: "db.redis.multi".freeze,
14
- title: "MULTI".freeze
15
+ title: "MULTI".freeze,
16
+ internal: true
15
17
  }.freeze
16
18
 
17
19
  module ClientInstrumentation
@@ -22,7 +24,8 @@ module Skylight
22
24
 
23
25
  opts = {
24
26
  category: "db.redis.command",
25
- title: command_name.upcase.to_s
27
+ title: command_name.upcase.to_s,
28
+ internal: true
26
29
  }
27
30
 
28
31
  Skylight.instrument(opts) { super }
@@ -99,7 +99,7 @@ module Skylight
99
99
  return nil
100
100
  end
101
101
 
102
- config = Config.load(file: path, environment: Rails.env.to_s)
102
+ config = Config.load(file: path, priority_key: Rails.env.to_s)
103
103
  config[:root] = Rails.root
104
104
 
105
105
  configure_logging(config, app)
@@ -19,16 +19,21 @@ module Skylight
19
19
  class ServerMiddleware
20
20
  include Util::Logging
21
21
 
22
- def call(_worker, job, queue)
22
+ def call(worker, job, queue)
23
23
  t { "Sidekiq middleware beginning trace" }
24
24
  title = job["wrapped"] || job["class"]
25
- Skylight.trace(title, "app.sidekiq.worker", title, segment: queue, component: :worker) do |trace|
26
- begin
27
- yield
28
- rescue Exception # includes Sidekiq::Shutdown
29
- trace.segment = "error" if trace
30
- raise
25
+
26
+ # TODO: Using hints here would be ideal but requires further refactoring
27
+ meta =
28
+ if (source_location = worker.method(:perform).source_location)
29
+ { source_file: source_location[0], source_line: source_location[1] }
31
30
  end
31
+
32
+ Skylight.trace(title, "app.sidekiq.worker", title, meta: meta, segment: queue, component: :worker) do |trace|
33
+ yield
34
+ rescue Exception # includes Sidekiq::Shutdown
35
+ trace.segment = "error" if trace
36
+ raise
32
37
  end
33
38
  end
34
39
  end
@@ -14,7 +14,7 @@ module Skylight
14
14
 
15
15
  def register!
16
16
  unregister!
17
- @normalizers.keys.each do |key|
17
+ @normalizers.keys.each do |key| # rubocop:disable Style/HashEachMethods
18
18
  @subscribers << ActiveSupport::Notifications.subscribe(key, self)
19
19
  end
20
20
  end
@@ -4,8 +4,9 @@ require "skylight/util/logging"
4
4
  module Skylight
5
5
  class Trace
6
6
  GC_CAT = "noise.gc".freeze
7
+ SYNTHETIC = "<synthetic>".freeze
7
8
 
8
- META_KEYS = %i[mute_children].freeze
9
+ META_KEYS = %i[mute_children database].freeze
9
10
 
10
11
  include Util::Logging
11
12
 
@@ -39,6 +40,8 @@ module Skylight
39
40
 
40
41
  @spans = []
41
42
 
43
+ preprocess_meta(meta) if meta
44
+
42
45
  # create the root node
43
46
  @root = start(native_get_started_at, cat, title, desc, meta, normalize: false)
44
47
 
@@ -217,7 +220,8 @@ module Skylight
217
220
 
218
221
  if time > 0
219
222
  t { fmt "tracking GC time; duration=%d", time }
220
- stop(start(now - time, GC_CAT, nil, nil, nil), now)
223
+ meta = { source_location: SYNTHETIC }
224
+ stop(start(now - time, GC_CAT, nil, nil, meta), now)
221
225
  end
222
226
  end
223
227
 
@@ -336,8 +340,10 @@ module Skylight
336
340
  def validate_meta(meta)
337
341
  unknown_keys = meta.keys - allowed_meta_keys
338
342
  if unknown_keys.any?
339
- warn "Unknown meta keys will be ignored; keys=#{unknown_keys.inspect}"
340
- unknown_keys.each { |key| meta.delete(key) }
343
+ unknown_keys.each do |key|
344
+ maybe_warn("unknown_meta:#{key}", "Unknown meta key will be ignored; key=#{key.inspect}")
345
+ meta.delete(key)
346
+ end
341
347
  end
342
348
  end
343
349
 
@@ -13,8 +13,7 @@ module Skylight
13
13
  end
14
14
 
15
15
  class EmptyDeploy
16
- attr_reader :config
17
- attr_reader :timestamp
16
+ attr_reader :config, :timestamp
18
17
 
19
18
  def initialize(config)
20
19
  @config = config
@@ -90,10 +89,8 @@ module Skylight
90
89
  def get_info
91
90
  info_path = config[:'heroku.dyno_info_path']
92
91
 
93
- if File.exist?(info_path)
94
- if (info = JSON.parse(File.read(info_path)))
95
- info["release"]
96
- end
92
+ if File.exist?(info_path) && (info = JSON.parse(File.read(info_path)))
93
+ info["release"]
97
94
  end
98
95
  end
99
96
  end
@@ -4,20 +4,20 @@ module Skylight
4
4
  def instrumenter_method(name, block: false)
5
5
  if block
6
6
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
7
- def #{name}(*args)
8
- unless instrumenter
9
- return yield if block_given?
10
- return
11
- end
12
-
13
- instrumenter.#{name}(*args) { yield }
14
- end
7
+ def #{name}(*args) # def mute(*args)
8
+ unless instrumenter # unless instrumenter
9
+ return yield if block_given? # return yield if block_given?
10
+ return # return
11
+ end # end
12
+ #
13
+ instrumenter.#{name}(*args) { yield } # instrumenter.mute(*args) { yield }
14
+ end # end
15
15
  RUBY
16
16
  else
17
17
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
18
- def #{name}(*args)
19
- instrumenter&.#{name}(*args)
20
- end
18
+ def #{name}(*args) # def config(*args)
19
+ instrumenter&.#{name}(*args) # instrumenter&.config(*args)
20
+ end # end
21
21
  RUBY
22
22
  end
23
23
  end