skylight 4.3.2 → 5.1.1

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