skylight 5.0.0.beta → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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