skylight 4.3.2 → 5.0.0.beta

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -5
  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 +22 -99
  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 +110 -11
@@ -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