skylight 4.2.2 → 5.0.0.beta2

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/CONTRIBUTING.md +1 -7
  4. data/ext/extconf.rb +6 -5
  5. data/ext/libskylight.yml +5 -6
  6. data/ext/skylight_native.c +24 -100
  7. data/lib/skylight.rb +204 -14
  8. data/lib/skylight/api.rb +7 -3
  9. data/lib/skylight/cli.rb +4 -3
  10. data/lib/skylight/cli/doctor.rb +13 -14
  11. data/lib/skylight/cli/merger.rb +6 -4
  12. data/lib/skylight/config.rb +604 -127
  13. data/lib/skylight/deprecation.rb +17 -0
  14. data/lib/skylight/errors.rb +17 -2
  15. data/lib/skylight/extensions.rb +107 -0
  16. data/lib/skylight/extensions/source_location.rb +280 -0
  17. data/lib/skylight/formatters/http.rb +19 -0
  18. data/lib/skylight/gc.rb +109 -0
  19. data/lib/skylight/helpers.rb +18 -2
  20. data/lib/skylight/instrumenter.rb +326 -15
  21. data/lib/skylight/middleware.rb +138 -1
  22. data/lib/skylight/native.rb +51 -1
  23. data/lib/skylight/native_ext_fetcher.rb +4 -3
  24. data/lib/skylight/normalizers.rb +151 -0
  25. data/lib/skylight/normalizers/action_controller/process_action.rb +69 -0
  26. data/lib/skylight/normalizers/action_controller/send_file.rb +50 -0
  27. data/lib/skylight/normalizers/action_dispatch/process_middleware.rb +22 -0
  28. data/lib/skylight/normalizers/action_dispatch/route_set.rb +27 -0
  29. data/lib/skylight/normalizers/action_view/render_collection.rb +24 -0
  30. data/lib/skylight/normalizers/action_view/render_layout.rb +25 -0
  31. data/lib/skylight/normalizers/action_view/render_partial.rb +23 -0
  32. data/lib/skylight/normalizers/action_view/render_template.rb +23 -0
  33. data/lib/skylight/normalizers/active_job/perform.rb +86 -0
  34. data/lib/skylight/normalizers/active_model_serializers/render.rb +28 -0
  35. data/lib/skylight/normalizers/active_record/instantiation.rb +16 -0
  36. data/lib/skylight/normalizers/active_record/sql.rb +12 -0
  37. data/lib/skylight/normalizers/active_storage.rb +30 -0
  38. data/lib/skylight/normalizers/active_support/cache.rb +22 -0
  39. data/lib/skylight/normalizers/active_support/cache_clear.rb +16 -0
  40. data/lib/skylight/normalizers/active_support/cache_decrement.rb +16 -0
  41. data/lib/skylight/normalizers/active_support/cache_delete.rb +16 -0
  42. data/lib/skylight/normalizers/active_support/cache_exist.rb +16 -0
  43. data/lib/skylight/normalizers/active_support/cache_fetch_hit.rb +16 -0
  44. data/lib/skylight/normalizers/active_support/cache_generate.rb +16 -0
  45. data/lib/skylight/normalizers/active_support/cache_increment.rb +16 -0
  46. data/lib/skylight/normalizers/active_support/cache_read.rb +16 -0
  47. data/lib/skylight/normalizers/active_support/cache_read_multi.rb +16 -0
  48. data/lib/skylight/normalizers/active_support/cache_write.rb +16 -0
  49. data/lib/skylight/normalizers/coach/handler_finish.rb +46 -0
  50. data/lib/skylight/normalizers/coach/middleware_finish.rb +33 -0
  51. data/lib/skylight/normalizers/couch_potato/query.rb +20 -0
  52. data/lib/skylight/normalizers/data_mapper/sql.rb +12 -0
  53. data/lib/skylight/normalizers/default.rb +32 -0
  54. data/lib/skylight/normalizers/elasticsearch/request.rb +20 -0
  55. data/lib/skylight/normalizers/faraday/request.rb +40 -0
  56. data/lib/skylight/normalizers/grape/endpoint.rb +34 -0
  57. data/lib/skylight/normalizers/grape/endpoint_render.rb +25 -0
  58. data/lib/skylight/normalizers/grape/endpoint_run.rb +41 -0
  59. data/lib/skylight/normalizers/grape/endpoint_run_filters.rb +22 -0
  60. data/lib/skylight/normalizers/grape/format_response.rb +20 -0
  61. data/lib/skylight/normalizers/graphiti/render.rb +22 -0
  62. data/lib/skylight/normalizers/graphiti/resolve.rb +31 -0
  63. data/lib/skylight/normalizers/graphql/base.rb +132 -0
  64. data/lib/skylight/normalizers/render.rb +81 -0
  65. data/lib/skylight/normalizers/sequel/sql.rb +12 -0
  66. data/lib/skylight/normalizers/sql.rb +45 -0
  67. data/lib/skylight/probes.rb +181 -0
  68. data/lib/skylight/probes/action_controller.rb +48 -0
  69. data/lib/skylight/probes/action_dispatch.rb +2 -0
  70. data/lib/skylight/probes/action_dispatch/request_id.rb +29 -0
  71. data/lib/skylight/probes/action_dispatch/routing/route_set.rb +28 -0
  72. data/lib/skylight/probes/action_view.rb +43 -0
  73. data/lib/skylight/probes/active_job.rb +27 -0
  74. data/lib/skylight/probes/active_job_enqueue.rb +35 -0
  75. data/lib/skylight/probes/active_model_serializers.rb +50 -0
  76. data/lib/skylight/probes/delayed_job.rb +148 -0
  77. data/lib/skylight/probes/elasticsearch.rb +38 -0
  78. data/lib/skylight/probes/excon.rb +25 -0
  79. data/lib/skylight/probes/excon/middleware.rb +66 -0
  80. data/lib/skylight/probes/faraday.rb +23 -0
  81. data/lib/skylight/probes/graphql.rb +43 -0
  82. data/lib/skylight/probes/httpclient.rb +44 -0
  83. data/lib/skylight/probes/middleware.rb +126 -0
  84. data/lib/skylight/probes/mongo.rb +163 -0
  85. data/lib/skylight/probes/mongoid.rb +13 -0
  86. data/lib/skylight/probes/net_http.rb +54 -0
  87. data/lib/skylight/probes/redis.rb +60 -0
  88. data/lib/skylight/probes/sequel.rb +33 -0
  89. data/lib/skylight/probes/sinatra.rb +63 -0
  90. data/lib/skylight/probes/sinatra_add_middleware.rb +10 -10
  91. data/lib/skylight/probes/tilt.rb +27 -0
  92. data/lib/skylight/railtie.rb +162 -18
  93. data/lib/skylight/sidekiq.rb +48 -0
  94. data/lib/skylight/subscriber.rb +110 -0
  95. data/lib/skylight/test.rb +146 -0
  96. data/lib/skylight/trace.rb +305 -10
  97. data/lib/skylight/user_config.rb +61 -0
  98. data/lib/skylight/util.rb +12 -0
  99. data/lib/skylight/util/allocation_free.rb +26 -0
  100. data/lib/skylight/util/clock.rb +56 -0
  101. data/lib/skylight/util/component.rb +5 -2
  102. data/lib/skylight/util/deploy.rb +7 -10
  103. data/lib/skylight/util/gzip.rb +20 -0
  104. data/lib/skylight/util/http.rb +5 -11
  105. data/lib/skylight/util/instrumenter_method.rb +26 -0
  106. data/lib/skylight/util/logging.rb +138 -0
  107. data/lib/skylight/util/lru_cache.rb +40 -0
  108. data/lib/skylight/vendor/cli/thor/rake_compat.rb +1 -1
  109. data/lib/skylight/version.rb +5 -1
  110. data/lib/skylight/vm/gc.rb +68 -0
  111. metadata +117 -19
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/deprecation"
4
+
5
+ module Skylight
6
+ SKYLIGHT_GEM_ROOT = "#{File.expand_path('../..', __dir__)}/"
7
+
8
+ class Deprecation < ActiveSupport::Deprecation
9
+ private
10
+
11
+ def ignored_callstack(path)
12
+ path.start_with?(SKYLIGHT_GEM_ROOT)
13
+ end
14
+ end
15
+
16
+ DEPRECATOR = Deprecation.new("6.0", "skylight")
17
+ end
@@ -1,4 +1,9 @@
1
+ require "json"
2
+
1
3
  module Skylight
4
+ # @api private
5
+ class ConfigError < RuntimeError; end
6
+
2
7
  class NativeError < StandardError
3
8
  @classes = {}
4
9
 
@@ -29,13 +34,17 @@ module Skylight
29
34
  9999
30
35
  end
31
36
 
37
+ def self.formatted_code
38
+ format("%<code>04d", code: code)
39
+ end
40
+
32
41
  def self.message
33
42
  "Encountered an unknown internal error"
34
43
  end
35
44
 
36
45
  def initialize(method_name)
37
46
  @method_name = method_name
38
- super(format("[E%<code>04d] %<message>s [%<meth>s]", code: code, message: message, meth: method_name))
47
+ super(format("[E%<code>04d] %<message>s [%<meth>s]", code: code, message: self.class.message, meth: method_name))
39
48
  end
40
49
 
41
50
  def code
@@ -43,9 +52,12 @@ module Skylight
43
52
  end
44
53
 
45
54
  def formatted_code
46
- format("%04d", code)
55
+ self.class.formatted_code
47
56
  end
48
57
 
58
+ # E0002
59
+ # Too many unique descriptions - daemon only
60
+
49
61
  # E0003
50
62
  register(3, "MaximumTraceSpans", "Exceeded maximum number of spans in a trace.")
51
63
 
@@ -54,5 +66,8 @@ module Skylight
54
66
 
55
67
  # E0005
56
68
  register(5, "InstrumenterUnrecoverable", "Instrumenter is not running.")
69
+
70
+ # E0006
71
+ register(6, "InvalidUtf8", "Invalid UTF-8")
57
72
  end
58
73
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector"
4
+
5
+ module Skylight
6
+ module Extensions
7
+ class Collection
8
+ def initialize(config, extensions = [])
9
+ @config = config
10
+ @extensions = extensions
11
+ end
12
+
13
+ def enable!(ext_name)
14
+ return if enabled?(ext_name)
15
+
16
+ find_by_name(ext_name) do |ext_class|
17
+ extensions << ext_class.new(config)
18
+ rememoize!
19
+ end
20
+ end
21
+
22
+ def disable!(ext_name)
23
+ find_by_name(ext_name) do |ext_class|
24
+ extensions.reject! { |x| x.is_a?(ext_class) }
25
+ rememoize!
26
+ end
27
+ end
28
+
29
+ def enabled?(ext_name)
30
+ return unless (ext_class = find_by_name(ext_name))
31
+
32
+ !!extensions.detect { |x| x.is_a?(ext_class) }
33
+ end
34
+
35
+ def process_trace_meta(meta)
36
+ extensions.each do |ext|
37
+ ext.process_trace_meta(meta)
38
+ end
39
+ end
40
+
41
+ # meta is a mutable hash that will be passed to the instrumenter.
42
+ # This method bridges Skylight.instrument and instrumenter.instrument.
43
+ def process_instrument_options(opts, meta)
44
+ extensions.each do |ext|
45
+ ext.process_instrument_options(opts, meta)
46
+ end
47
+ end
48
+
49
+ def process_normalizer_meta(payload, meta, **opts)
50
+ extensions.each do |ext|
51
+ ext.process_normalizer_meta(payload, meta, **opts)
52
+ end
53
+ end
54
+
55
+ def trace_preprocess_meta(meta)
56
+ extensions.each do |ext|
57
+ ext.trace_preprocess_meta(meta)
58
+ end
59
+ end
60
+
61
+ def allowed_meta_keys
62
+ @allowed_meta_keys ||= extensions.flat_map(&:allowed_meta_keys).uniq
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :extensions, :config
68
+
69
+ def find_by_name(ext_name)
70
+ begin
71
+ Skylight::Extensions.const_get(
72
+ ActiveSupport::Inflector.classify(ext_name)
73
+ )
74
+ rescue NameError
75
+ return nil
76
+ end.tap do |const|
77
+ yield const if block_given?
78
+ end
79
+ end
80
+
81
+ def rememoize!
82
+ @allowed_meta_keys = nil
83
+ allowed_meta_keys
84
+ end
85
+ end
86
+
87
+ class Extension
88
+ def initialize(config)
89
+ @config = config
90
+ end
91
+
92
+ def process_trace_meta(_meta); end
93
+
94
+ def process_instrument_options(_opts, _meta); end
95
+
96
+ def process_normalizer_meta(_payload, _meta, **opts); end
97
+
98
+ def trace_preprocess_meta(_meta); end
99
+
100
+ def allowed_meta_keys
101
+ []
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ require "skylight/extensions/source_location"
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "skylight/util/lru_cache"
4
+ require "active_support/dependencies"
5
+
6
+ module Skylight
7
+ module Extensions
8
+ class SourceLocation < Extension
9
+ attr_reader :config
10
+
11
+ include Util::Logging
12
+
13
+ META_KEYS = %i[source_location source_file source_line].freeze
14
+
15
+ def initialize(*)
16
+ super
17
+ @caller_cache = Util::LruCache.new(100)
18
+ @instance_method_source_location_cache = Util::LruCache.new(100)
19
+ gem_require_trie # memoize this at startup
20
+ end
21
+
22
+ def process_trace_meta(meta)
23
+ unless meta[:source_location] || meta[:source_file]
24
+ warn "Ignoring source_line without source_file" if meta[:source_line]
25
+ if (location = find_caller)
26
+ meta[:source_file] = location.absolute_path
27
+ meta[:source_line] = location.lineno
28
+ end
29
+ end
30
+ end
31
+
32
+ def process_instrument_options(opts, meta)
33
+ source_location = opts[:source_location] || opts[:meta]&.[](:source_location)
34
+ source_file = opts[:source_file] || opts[:meta]&.[](:source_file)
35
+ source_line = opts[:source_line] || opts[:meta]&.[](:source_line)
36
+ source_name_hint, const_name, method_name = opts[:source_location_hint] ||
37
+ opts[:meta]&.[](:source_location_hint)
38
+
39
+ if source_location
40
+ meta[:source_location] = source_location
41
+ elsif source_name_hint
42
+ source_location = dispatch_hinted_source_location(source_name_hint, const_name, method_name)
43
+ meta[:source_file], meta[:source_line] = source_location
44
+ meta.delete(:source_location_hint) if source_location
45
+ elsif source_file
46
+ meta[:source_file] = source_file
47
+ meta[:source_line] = source_line
48
+ else
49
+ warn "Ignoring source_line without source_file" if source_line
50
+ if (location = find_caller(cache_key: opts.hash))
51
+ meta[:source_file] = location.absolute_path
52
+ meta[:source_line] = location.lineno
53
+ end
54
+ end
55
+
56
+ meta
57
+ end
58
+
59
+ def process_normalizer_meta(payload, meta, **opts)
60
+ if opts[:source_location] && (opts[:source_file] || opts[:source_line])
61
+ warn "Found both source_location and source_file or source_line in normalizer\n" \
62
+ " location=#{opts[:source_location]}; file=#{opts[:source_file]}; line=#{opts[:source_line]}"
63
+ end
64
+
65
+ sl =
66
+ if (source_name, constant_name, method_name = opts[:source_location_hint])
67
+ dispatch_hinted_source_location(
68
+ source_name,
69
+ constant_name,
70
+ method_name
71
+ )
72
+ elsif opts[:source_file]
73
+ [opts[:source_file], opts[:source_line]]
74
+ end
75
+
76
+ sl ||= source_location(payload, meta, cache_key: opts[:cache_key])
77
+
78
+ if sl
79
+ debug("normalizer source_location=#{sl}")
80
+ meta[:source_file], meta[:source_line] = sl
81
+ end
82
+
83
+ meta
84
+ end
85
+
86
+ def trace_preprocess_meta(meta)
87
+ source_line = meta.delete(:source_line)
88
+ source_file = meta.delete(:source_file)
89
+
90
+ if meta[:source_location]
91
+ if source_file || source_line
92
+ warn "Found both source_location and source_file or source_line, using source_location\n" \
93
+ " location=#{meta[:source_location]}; file=#{source_file}; line=#{source_line}"
94
+ end
95
+
96
+ unless meta[:source_location].is_a?(String)
97
+ warn "Found non-string value for source_location; skipping"
98
+ meta.delete(:source_location)
99
+ end
100
+ elsif source_file
101
+ meta[:source_location] = sanitize_source_location(source_file, source_line)
102
+ elsif source_line
103
+ warn "Ignoring source_line without source_file; source_line=#{source_line}"
104
+ end
105
+
106
+ if meta[:source_location]
107
+ debug("source_location=#{meta[:source_location]}")
108
+ end
109
+ end
110
+
111
+ def allowed_meta_keys
112
+ META_KEYS
113
+ end
114
+
115
+ protected
116
+
117
+ def dispatch_hinted_source_location(source_name, const_name, method_name)
118
+ return unless const_name && method_name
119
+
120
+ instance_method_source_location(const_name, method_name, source_name: source_name)
121
+ end
122
+
123
+ # from normalizers.rb
124
+ # Returns an array of file and line
125
+ def source_location(payload, meta, cache_key: nil)
126
+ # FIXME: what should precedence be?
127
+ if meta.is_a?(Hash) && meta[:source_location]
128
+ meta.delete(:source_location)
129
+ elsif payload.is_a?(Hash) && payload[:sk_source_location]
130
+ payload[:sk_source_location]
131
+ elsif (location = find_caller(cache_key: cache_key))
132
+ [location.absolute_path, location.lineno]
133
+ end
134
+ end
135
+
136
+ def find_caller(cache_key: nil)
137
+ if cache_key
138
+ @caller_cache.fetch(cache_key) { find_caller_inner }
139
+ else
140
+ find_caller_inner
141
+ end
142
+ end
143
+
144
+ def project_path?(path)
145
+ return false unless path
146
+
147
+ # Must be in the project root
148
+ return false unless path.start_with?(config.root.to_s)
149
+ # Must not be Bundler's vendor location
150
+ return false if defined?(Bundler) && path.start_with?(Bundler.bundle_path.to_s)
151
+ # Must not be Ruby files
152
+ return false if path.include?("/ruby-#{RUBY_VERSION}/lib/ruby/")
153
+
154
+ # So it must be a project file
155
+ true
156
+ end
157
+
158
+ def instance_method_source_location(constant_name, method_name, source_name: :instance_method)
159
+ @instance_method_source_location_cache.fetch([constant_name, method_name, source_name]) do
160
+ if (constant = ::ActiveSupport::Dependencies.safe_constantize(constant_name))
161
+ if constant.instance_methods.include?(:"before_instrument_#{method_name}")
162
+ method_name = :"before_instrument_#{method_name}"
163
+ end
164
+ begin
165
+ unbound_method = case source_name
166
+ when :instance_method
167
+ find_instance_method(constant, method_name)
168
+ when :own_instance_method
169
+ find_own_instance_method(constant, method_name)
170
+ when :instance_method_super
171
+ find_instance_method_super(constant, method_name)
172
+ when :class_method
173
+ find_class_method(constant, method_name)
174
+ end
175
+
176
+ unbound_method&.source_location
177
+ rescue NameError
178
+ nil
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def sanitize_source_location(path, line)
185
+ # Do this first since gems may be vendored in the app repo. However, it might be slower.
186
+ # Should we cache matches?
187
+ if (gem_name = find_source_gem(path))
188
+ path = gem_name
189
+ line = nil
190
+ elsif project_path?(path)
191
+ # Get relative path to root
192
+ path = Pathname.new(path).relative_path_from(config.root).to_s
193
+ else
194
+ return
195
+ end
196
+
197
+ line ? "#{path}:#{line}" : path
198
+ end
199
+
200
+ private
201
+
202
+ def gem_require_trie
203
+ @gem_require_trie ||= begin
204
+ trie = {}
205
+
206
+ Gem.loaded_specs.each do |name, spec|
207
+ next if config.source_location_ignored_gems&.include?(name)
208
+
209
+ spec.full_require_paths.each do |path|
210
+ t1 = trie
211
+
212
+ path.split(File::SEPARATOR).each do |segment|
213
+ t1[segment] ||= {}
214
+ t1 = t1[segment]
215
+ end
216
+
217
+ t1[:name] = name
218
+ end
219
+ end
220
+
221
+ trie
222
+ end
223
+ end
224
+
225
+ def find_source_gem(path)
226
+ return nil unless path
227
+
228
+ trie = gem_require_trie
229
+
230
+ path.split(File::SEPARATOR).each do |segment|
231
+ trie = trie[segment]
232
+ break unless trie
233
+ return trie[:name] if trie[:name]
234
+ end
235
+
236
+ nil
237
+ end
238
+
239
+ def find_caller_inner
240
+ # Start at file before this one
241
+ # NOTE: We could start farther back now to avoid more Skylight files
242
+ caller_locations(1).find do |l|
243
+ find_source_gem(l.absolute_path) || project_path?(l.absolute_path)
244
+ end
245
+ end
246
+
247
+ # walks up the inheritance tree until it finds the last method
248
+ # without a super_method definition.
249
+ def find_instance_method_super(constant, method_name)
250
+ return unless (unbound_method = find_instance_method(constant, method_name))
251
+
252
+ while unbound_method.super_method
253
+ unbound_method = unbound_method.super_method
254
+ end
255
+
256
+ unbound_method
257
+ end
258
+
259
+ # walks up the inheritance tree until it finds the instance method
260
+ # belonging to the constant given (skip prepended modules)
261
+ def find_own_instance_method(constant, method_name)
262
+ return unless (unbound_method = find_instance_method(constant, method_name))
263
+
264
+ while unbound_method.owner != constant && unbound_method.super_method
265
+ unbound_method = unbound_method.super_method
266
+ end
267
+
268
+ unbound_method if unbound_method.owner == constant
269
+ end
270
+
271
+ def find_instance_method(constant, method_name)
272
+ constant.instance_method(method_name)
273
+ end
274
+
275
+ def find_class_method(constant, method_name)
276
+ constant.method(method_name)
277
+ end
278
+ end
279
+ end
280
+ end