skylight 4.2.1 → 5.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/CONTRIBUTING.md +1 -7
  4. data/ext/extconf.rb +4 -3
  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 +3 -2
  11. data/lib/skylight/cli/merger.rb +6 -4
  12. data/lib/skylight/config.rb +603 -126
  13. data/lib/skylight/deprecation.rb +15 -0
  14. data/lib/skylight/errors.rb +17 -2
  15. data/lib/skylight/extensions.rb +99 -0
  16. data/lib/skylight/extensions/source_location.rb +249 -0
  17. data/lib/skylight/fanout.rb +0 -0
  18. data/lib/skylight/formatters/http.rb +19 -0
  19. data/lib/skylight/gc.rb +109 -0
  20. data/lib/skylight/helpers.rb +18 -2
  21. data/lib/skylight/instrumenter.rb +325 -15
  22. data/lib/skylight/middleware.rb +138 -1
  23. data/lib/skylight/native.rb +51 -1
  24. data/lib/skylight/native_ext_fetcher.rb +2 -1
  25. data/lib/skylight/normalizers.rb +151 -0
  26. data/lib/skylight/normalizers/action_controller/process_action.rb +69 -0
  27. data/lib/skylight/normalizers/action_controller/send_file.rb +50 -0
  28. data/lib/skylight/normalizers/action_dispatch/process_middleware.rb +22 -0
  29. data/lib/skylight/normalizers/action_dispatch/route_set.rb +27 -0
  30. data/lib/skylight/normalizers/action_view/render_collection.rb +24 -0
  31. data/lib/skylight/normalizers/action_view/render_layout.rb +25 -0
  32. data/lib/skylight/normalizers/action_view/render_partial.rb +23 -0
  33. data/lib/skylight/normalizers/action_view/render_template.rb +23 -0
  34. data/lib/skylight/normalizers/active_job/perform.rb +81 -0
  35. data/lib/skylight/normalizers/active_model_serializers/render.rb +28 -0
  36. data/lib/skylight/normalizers/active_record/instantiation.rb +16 -0
  37. data/lib/skylight/normalizers/active_record/sql.rb +12 -0
  38. data/lib/skylight/normalizers/active_storage.rb +30 -0
  39. data/lib/skylight/normalizers/active_support/cache.rb +22 -0
  40. data/lib/skylight/normalizers/active_support/cache_clear.rb +16 -0
  41. data/lib/skylight/normalizers/active_support/cache_decrement.rb +16 -0
  42. data/lib/skylight/normalizers/active_support/cache_delete.rb +16 -0
  43. data/lib/skylight/normalizers/active_support/cache_exist.rb +16 -0
  44. data/lib/skylight/normalizers/active_support/cache_fetch_hit.rb +16 -0
  45. data/lib/skylight/normalizers/active_support/cache_generate.rb +16 -0
  46. data/lib/skylight/normalizers/active_support/cache_increment.rb +16 -0
  47. data/lib/skylight/normalizers/active_support/cache_read.rb +16 -0
  48. data/lib/skylight/normalizers/active_support/cache_read_multi.rb +16 -0
  49. data/lib/skylight/normalizers/active_support/cache_write.rb +16 -0
  50. data/lib/skylight/normalizers/coach/handler_finish.rb +46 -0
  51. data/lib/skylight/normalizers/coach/middleware_finish.rb +33 -0
  52. data/lib/skylight/normalizers/couch_potato/query.rb +20 -0
  53. data/lib/skylight/normalizers/data_mapper/sql.rb +12 -0
  54. data/lib/skylight/normalizers/default.rb +32 -0
  55. data/lib/skylight/normalizers/elasticsearch/request.rb +20 -0
  56. data/lib/skylight/normalizers/faraday/request.rb +40 -0
  57. data/lib/skylight/normalizers/grape/endpoint.rb +34 -0
  58. data/lib/skylight/normalizers/grape/endpoint_render.rb +25 -0
  59. data/lib/skylight/normalizers/grape/endpoint_run.rb +41 -0
  60. data/lib/skylight/normalizers/grape/endpoint_run_filters.rb +22 -0
  61. data/lib/skylight/normalizers/grape/format_response.rb +20 -0
  62. data/lib/skylight/normalizers/graphiti/render.rb +22 -0
  63. data/lib/skylight/normalizers/graphiti/resolve.rb +31 -0
  64. data/lib/skylight/normalizers/graphql/base.rb +131 -0
  65. data/lib/skylight/normalizers/render.rb +81 -0
  66. data/lib/skylight/normalizers/sequel/sql.rb +12 -0
  67. data/lib/skylight/normalizers/sql.rb +44 -0
  68. data/lib/skylight/probes.rb +153 -0
  69. data/lib/skylight/probes/action_controller.rb +48 -0
  70. data/lib/skylight/probes/action_dispatch.rb +2 -0
  71. data/lib/skylight/probes/action_dispatch/request_id.rb +29 -0
  72. data/lib/skylight/probes/action_dispatch/routing/route_set.rb +28 -0
  73. data/lib/skylight/probes/action_view.rb +43 -0
  74. data/lib/skylight/probes/active_job.rb +29 -0
  75. data/lib/skylight/probes/active_job_enqueue.rb +37 -0
  76. data/lib/skylight/probes/active_model_serializers.rb +54 -0
  77. data/lib/skylight/probes/delayed_job.rb +62 -0
  78. data/lib/skylight/probes/elasticsearch.rb +38 -0
  79. data/lib/skylight/probes/excon.rb +25 -0
  80. data/lib/skylight/probes/excon/middleware.rb +66 -0
  81. data/lib/skylight/probes/faraday.rb +23 -0
  82. data/lib/skylight/probes/graphql.rb +43 -0
  83. data/lib/skylight/probes/httpclient.rb +44 -0
  84. data/lib/skylight/probes/middleware.rb +125 -0
  85. data/lib/skylight/probes/mongo.rb +163 -0
  86. data/lib/skylight/probes/mongoid.rb +13 -0
  87. data/lib/skylight/probes/net_http.rb +55 -0
  88. data/lib/skylight/probes/redis.rb +60 -0
  89. data/lib/skylight/probes/sequel.rb +33 -0
  90. data/lib/skylight/probes/sinatra.rb +63 -0
  91. data/lib/skylight/probes/sinatra_add_middleware.rb +10 -10
  92. data/lib/skylight/probes/tilt.rb +27 -0
  93. data/lib/skylight/railtie.rb +162 -18
  94. data/lib/skylight/sidekiq.rb +43 -0
  95. data/lib/skylight/subscriber.rb +110 -0
  96. data/lib/skylight/test.rb +146 -0
  97. data/lib/skylight/trace.rb +301 -10
  98. data/lib/skylight/user_config.rb +61 -0
  99. data/lib/skylight/util.rb +12 -0
  100. data/lib/skylight/util/allocation_free.rb +26 -0
  101. data/lib/skylight/util/clock.rb +56 -0
  102. data/lib/skylight/util/component.rb +5 -2
  103. data/lib/skylight/util/deploy.rb +4 -4
  104. data/lib/skylight/util/gzip.rb +20 -0
  105. data/lib/skylight/util/http.rb +4 -10
  106. data/lib/skylight/util/instrumenter_method.rb +26 -0
  107. data/lib/skylight/util/logging.rb +138 -0
  108. data/lib/skylight/util/lru_cache.rb +42 -0
  109. data/lib/skylight/vendor/cli/thor/rake_compat.rb +1 -1
  110. data/lib/skylight/version.rb +5 -1
  111. data/lib/skylight/vm/gc.rb +68 -0
  112. metadata +116 -17
@@ -1,9 +1,52 @@
1
+ require "securerandom"
2
+ require "skylight/util/logging"
3
+
1
4
  module Skylight
2
- class Trace < Core::Trace
3
- attr_reader :component
5
+ class Trace
6
+ GC_CAT = "noise.gc".freeze
7
+
8
+ META_KEYS = %i[mute_children].freeze
9
+
10
+ include Util::Logging
11
+
12
+ attr_reader :instrumenter, :endpoint, :segment, :notifications, :meta, :component
13
+
14
+ def self.new(instrumenter, endpoint, start, cat, title = nil, desc = nil, meta: nil, segment: nil, component: nil)
15
+ uuid = SecureRandom.uuid
16
+ inst = native_new(normalize_time(start), uuid, endpoint, meta)
17
+ inst.uuid = uuid
18
+ inst.send(:initialize, instrumenter, cat, title, desc, meta, component: component)
19
+ inst.endpoint = endpoint
20
+ inst.segment = segment
21
+ inst
22
+ end
23
+
24
+ # TODO: Move this into native
25
+ def self.normalize_time(time)
26
+ # At least one customer has extensions that cause integer division to produce rationals.
27
+ # Since the native code expects an integer, we force it again.
28
+ (time.to_i / 100_000).to_i
29
+ end
30
+
31
+ def initialize(instrumenter, cat, title, desc, meta, component: nil)
32
+ raise ArgumentError, "instrumenter is required" unless instrumenter
33
+
34
+ @instrumenter = instrumenter
35
+ @submitted = false
36
+ @broken = false
37
+
38
+ @notifications = []
39
+
40
+ @spans = []
41
+
42
+ # create the root node
43
+ @root = start(native_get_started_at, cat, title, desc, meta, normalize: false)
44
+
45
+ # Also store meta for later access
46
+ @meta = meta
47
+
48
+ @gc = config.gc.track unless ENV.key?("SKYLIGHT_DISABLE_GC_TRACKING")
4
49
 
5
- def initialize(*, component: nil)
6
- super
7
50
  self.component = component if component
8
51
  @too_many_spans = false
9
52
  native_use_pruning if use_pruning?
@@ -26,29 +69,247 @@ module Skylight
26
69
  !!@too_many_spans
27
70
  end
28
71
 
29
- def maybe_broken(error)
30
- if error.is_a?(Skylight::MaximumTraceSpansError) && config.get(:report_max_spans_exceeded)
72
+ def log_context
73
+ @log_context ||= { trace: uuid }
74
+ end
75
+
76
+ def endpoint=(value)
77
+ if muted?
78
+ maybe_warn(:endpoint_set_muted, "tried to set endpoint name while muted")
79
+ return
80
+ end
81
+ @endpoint = value
82
+ native_set_endpoint(value)
83
+ end
84
+
85
+ def segment=(value)
86
+ if muted?
87
+ maybe_warn(:segment_set_muted, "tried to set segment name while muted")
88
+ return
89
+ end
90
+ @segment = value
91
+ end
92
+
93
+ attr_accessor :compound_response_error_status
94
+
95
+ def config
96
+ @instrumenter.config
97
+ end
98
+
99
+ def muted?
100
+ !!@child_instrumentation_muted_by || @instrumenter.muted?
101
+ end
102
+
103
+ def broken?
104
+ !!@broken
105
+ end
106
+
107
+ def maybe_broken(err)
108
+ if err.is_a?(Skylight::MaximumTraceSpansError) && config.get(:report_max_spans_exceeded)
31
109
  too_many_spans!
32
110
  else
33
- super
111
+ error "failed to instrument span; msg=%s; endpoint=%s", err.message, endpoint
112
+ broken!
34
113
  end
35
114
  end
36
115
 
116
+ def instrument(cat, title = nil, desc = nil, meta = nil)
117
+ return if muted?
118
+ return if broken?
119
+
120
+ t { "instrument: #{cat}, #{title}" }
121
+
122
+ title.freeze if title.is_a?(String)
123
+ desc.freeze if desc.is_a?(String)
124
+
125
+ now = Skylight::Util::Clock.nanos
126
+
127
+ preprocess_meta(meta) if meta
128
+
129
+ start(now - gc_time, cat, title, desc, meta)
130
+ rescue => e
131
+ maybe_broken(e)
132
+ nil
133
+ end
134
+
135
+ def done(span, meta = nil)
136
+ # `span` will be `nil` if we failed to start instrumenting, such as in
137
+ # the case of too many spans in a request.
138
+ return unless span
139
+ return if broken?
140
+
141
+ if meta&.[](:defer)
142
+ deferred_spans[span] ||= (Skylight::Util::Clock.nanos - gc_time)
143
+ return
144
+ end
145
+
146
+ if meta && (meta[:exception_object] || meta[:exception])
147
+ native_span_set_exception(span, meta[:exception_object], meta[:exception])
148
+ end
149
+
150
+ stop(span, Skylight::Util::Clock.nanos - gc_time)
151
+ rescue => e
152
+ error "failed to close span; msg=%s; endpoint=%s", e.message, endpoint
153
+ log_trace "Original Backtrace:\n#{e.backtrace.join("\n")}"
154
+ broken!
155
+ nil
156
+ end
157
+
158
+ def inspect
159
+ to_s
160
+ end
161
+
162
+ def release
163
+ t { "release; is_current=#{@instrumenter.current_trace == self}" }
164
+ return unless @instrumenter.current_trace == self
165
+
166
+ @instrumenter.current_trace = nil
167
+ end
168
+
169
+ def broken!
170
+ debug "trace is broken"
171
+ @broken = true
172
+ end
173
+
37
174
  def traced
38
175
  if too_many_spans?
39
176
  error("[E%04d] The request exceeded the maximum number of spans allowed. It will still " \
40
177
  "be tracked but with reduced information. endpoint=%s", Skylight::MaximumTraceSpansError.code, endpoint)
41
178
  end
42
179
 
43
- super
180
+ gc = gc_time
181
+ now = Skylight::Util::Clock.nanos
182
+ track_gc(gc, now)
183
+ stop(@root, now)
184
+ end
185
+
186
+ def submit
187
+ t { "submitting trace" }
188
+
189
+ # This must always be called to clean up properly
190
+ release
191
+
192
+ if broken?
193
+ t { "broken, not submitting" }
194
+ return
195
+ end
196
+
197
+ if @submitted
198
+ t { "already submitted" }
199
+ return
200
+ end
201
+
202
+ @submitted = true
203
+
204
+ traced
205
+
206
+ @instrumenter.process(self)
207
+ rescue Exception => e
208
+ error e.message
209
+ t { e.backtrace.join("\n") }
44
210
  end
45
211
 
46
212
  private
47
213
 
48
- def track_gc(*)
214
+ def track_gc(time, now)
49
215
  # This attempts to log another span which will fail if we have too many
50
216
  return if too_many_spans?
51
- super
217
+
218
+ if time > 0
219
+ t { fmt "tracking GC time; duration=%d", time }
220
+ stop(start(now - time, GC_CAT, nil, nil, nil), now)
221
+ end
222
+ end
223
+
224
+ def start(time, cat, title, desc, meta, opts = {})
225
+ time = self.class.normalize_time(time) unless opts[:normalize] == false
226
+
227
+ mute_children = meta&.delete(:mute_children)
228
+
229
+ sp = native_start_span(time, cat.to_s)
230
+ native_span_set_title(sp, title.to_s) if title
231
+ native_span_set_description(sp, desc.to_s) if desc
232
+ native_span_set_meta(sp, meta) if meta
233
+ native_span_started(sp)
234
+
235
+ @spans << sp
236
+ t { "started span: #{sp} - #{cat}, #{title}" }
237
+
238
+ if mute_children
239
+ t { "muting child instrumentation for span=#{sp}" }
240
+ mute_child_instrumentation(sp)
241
+ end
242
+
243
+ sp
244
+ end
245
+
246
+ def mute_child_instrumentation(span)
247
+ @child_instrumentation_muted_by = span
248
+ end
249
+
250
+ # Middleware spans that were interrupted by a throw/catch should be cached here.
251
+ # keys: span ids
252
+ # values: nsec timestamp at which the span was cached here.
253
+ def deferred_spans
254
+ @deferred_spans ||= {}
255
+ end
256
+
257
+ def stop(span, time)
258
+ t { "stopping span: #{span}" }
259
+
260
+ # If `stop` is called for a span that is not the last item in the stack,
261
+ # check to see if the last item has been marked as deferred. If so, close
262
+ # that span first, then try to close the original.
263
+ while deferred_spans[expected = @spans.pop]
264
+ normalized_stop(expected, deferred_spans.delete(expected))
265
+ end
266
+
267
+ handle_unexpected_stop(expected, span) unless span == expected
268
+
269
+ normalized_stop(span, time)
270
+ nil
271
+ end
272
+
273
+ def normalized_stop(span, time)
274
+ time = self.class.normalize_time(time)
275
+ native_stop_span(span, time)
276
+
277
+ if @child_instrumentation_muted_by == span
278
+ @child_instrumentation_muted_by = nil # restart instrumenting
279
+ end
280
+ end
281
+
282
+ # Originally extracted from `stop`.
283
+ # If we attempt to close spans out of order, and it appears to be a middleware issue,
284
+ # disable the middleware probe and mark trace as broken.
285
+ def handle_unexpected_stop(expected, span)
286
+ message = "[E0001] Spans were closed out of order. Expected to see '#{native_span_get_title(expected)}', " \
287
+ "but got '#{native_span_get_title(span)}' instead."
288
+
289
+ if native_span_get_category(span) == "rack.middleware" && Skylight::Probes.installed.key?(:middleware)
290
+ if Skylight::Probes::Middleware::Probe.disabled?
291
+ message << "\nWe disabled the Middleware probe but unfortunately, this didn't solve the issue."
292
+ else
293
+ Skylight::Probes::Middleware::Probe.disable!
294
+ message << "\n#{native_span_get_title(span)} may be a Middleware that doesn't fully conform " \
295
+ "to the Rack SPEC. We've disabled the Middleware probe to see if that resolves the issue."
296
+ end
297
+ end
298
+
299
+ message << "\nThis request will not be tracked. Please contact support@skylight.io for more information."
300
+
301
+ error message
302
+
303
+ t { "expected=#{expected}, actual=#{span}" }
304
+
305
+ broken!
306
+ end
307
+
308
+ def gc_time
309
+ return 0 unless @gc
310
+
311
+ @gc.update
312
+ @gc.time
52
313
  end
53
314
 
54
315
  def use_pruning?
@@ -61,9 +322,39 @@ module Skylight
61
322
 
62
323
  def component=(component)
63
324
  resolve_component(component).tap do |c|
325
+ # Would it be better for the component getter to get from native?
64
326
  @component = c
65
327
  native_set_component(c)
66
328
  end
67
329
  end
330
+
331
+ def preprocess_meta(meta)
332
+ validate_meta(meta)
333
+ instrumenter.extensions.trace_preprocess_meta(meta)
334
+ end
335
+
336
+ def validate_meta(meta)
337
+ unknown_keys = meta.keys - allowed_meta_keys
338
+ if unknown_keys.any?
339
+ warn "Unknown meta keys will be ignored; keys=#{unknown_keys.inspect}"
340
+ unknown_keys.each { |key| meta.delete(key) }
341
+ end
342
+ end
343
+
344
+ def allowed_meta_keys
345
+ META_KEYS | instrumenter.extensions.allowed_meta_keys
346
+ end
347
+
348
+ def maybe_warn(context, msg)
349
+ return if warnings_silenced?(context)
350
+
351
+ instrumenter.silence_warnings(context)
352
+
353
+ warn(msg)
354
+ end
355
+
356
+ def warnings_silenced?(context)
357
+ instrumenter.warnings_silenced?(context)
358
+ end
68
359
  end
69
360
  end
@@ -0,0 +1,61 @@
1
+ require "yaml"
2
+ require "skylight/errors"
3
+
4
+ module Skylight
5
+ class UserConfig
6
+ attr_accessor :disable_dev_warning, :disable_env_warning
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ @file_path = nil
11
+ reload
12
+ end
13
+
14
+ def file_path
15
+ return @file_path if @file_path
16
+
17
+ config_path = @config[:user_config_path] || begin
18
+ require "etc"
19
+ home_dir = ENV["HOME"] || Etc.getpwuid.dir || (ENV["USER"] && File.expand_path("~#{ENV['USER']}"))
20
+ if home_dir
21
+ File.join(home_dir, ".skylight")
22
+ else
23
+ raise ConfigError,
24
+ "The Skylight `user_config_path` must be defined since the home directory cannot be inferred"
25
+ end
26
+ end
27
+
28
+ @file_path = File.expand_path(config_path)
29
+ end
30
+
31
+ def disable_dev_warning?
32
+ disable_dev_warning || ENV["SKYLIGHT_DISABLE_DEV_WARNING"] =~ /^true$/i
33
+ end
34
+
35
+ def disable_env_warning?
36
+ disable_env_warning
37
+ end
38
+
39
+ def reload
40
+ config = File.exist?(file_path) ? YAML.load_file(file_path) : false
41
+ return unless config
42
+
43
+ self.disable_dev_warning = !!config["disable_dev_warning"]
44
+ self.disable_env_warning = !!config["disable_env_warning"]
45
+ end
46
+
47
+ def save
48
+ FileUtils.mkdir_p(File.dirname(file_path))
49
+ File.open(file_path, "w") do |f|
50
+ f.puts YAML.dump(to_hash)
51
+ end
52
+ end
53
+
54
+ def to_hash
55
+ {
56
+ "disable_dev_warning" => disable_dev_warning,
57
+ "disable_env_warning" => disable_env_warning
58
+ }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+ module Skylight
2
+ # @api private
3
+ module Util
4
+ # Used from the main lib
5
+ require "skylight/util/allocation_free"
6
+ require "skylight/util/clock"
7
+ require "skylight/util/instrumenter_method"
8
+
9
+ # Used from the CLI
10
+ autoload :Gzip, "skylight/util/gzip"
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ module Skylight
2
+ module Util
3
+ # Helpers to reduce memory allocation
4
+ module AllocationFree
5
+ # Find an item in an array without allocation.
6
+ #
7
+ # @param array [Array] the array to search
8
+ # @yield a block called against each item until a match is found
9
+ # @yieldparam item an item from the array
10
+ # @yieldreturn [Boolean] whether `item` matches the criteria
11
+ # return the found item or nil, if nothing found
12
+ def array_find(array)
13
+ i = 0
14
+
15
+ while i < array.size
16
+ item = array[i]
17
+ return item if yield item
18
+
19
+ i += 1
20
+ end
21
+
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end