skylight 4.2.2 → 5.0.0.beta2

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