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,19 @@
1
+ module Skylight
2
+ module Formatters
3
+ module HTTP
4
+ # Build instrumentation options for HTTP queries
5
+ #
6
+ # @param [String] method HTTP method, e.g. get, post
7
+ # @param [String] scheme HTTP scheme, e.g. http, https
8
+ # @param [String] host Request host, e.g. example.com
9
+ # @param [String, Integer] port Request port
10
+ # @param [String] path Request path
11
+ # @param [String] query Request query string
12
+ # @return [Hash] a hash containing `:category`, `:title`, and `:annotations`
13
+ def self.build_opts(method, _scheme, host, _port, _path, _query)
14
+ { category: "api.http.#{method.downcase}",
15
+ title: "#{method.upcase} #{host}" }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,109 @@
1
+ require "skylight/util/logging"
2
+
3
+ module Skylight
4
+ # @api private
5
+ class GC
6
+ METHODS = %i[enable total_time].freeze
7
+ TH_KEY = :SK_GC_CURR_WINDOW
8
+ MAX_COUNT = 1000
9
+ MAX_TIME = 30_000_000
10
+
11
+ include Util::Logging
12
+
13
+ attr_reader :config
14
+
15
+ def initialize(config, profiler)
16
+ @listeners = []
17
+ @config = config
18
+ @lock = Mutex.new
19
+ @time = 0
20
+
21
+ if METHODS.all? { |m| profiler.respond_to?(m) }
22
+ @profiler = profiler
23
+ @time = @profiler.total_time
24
+ else
25
+ debug "disabling GC profiling"
26
+ end
27
+ end
28
+
29
+ def enable
30
+ @profiler&.enable
31
+ end
32
+
33
+ # Total time in microseconds for GC over entire process lifetime
34
+ def total_time
35
+ @profiler ? @profiler.total_time : nil
36
+ end
37
+
38
+ def track
39
+ if @profiler
40
+ win = Window.new(self)
41
+
42
+ @lock.synchronize do
43
+ __update
44
+ @listeners << win
45
+
46
+ # Cleanup any listeners that might have leaked
47
+ @listeners.shift until @listeners[0].time < MAX_TIME
48
+
49
+ if @listeners.length > MAX_COUNT
50
+ @listeners.shift
51
+ end
52
+ end
53
+
54
+ win
55
+ else
56
+ Window.new(nil)
57
+ end
58
+ end
59
+
60
+ def release(win)
61
+ @lock.synchronize do
62
+ @listeners.delete(win)
63
+ end
64
+ end
65
+
66
+ def update
67
+ @lock.synchronize do
68
+ __update
69
+ end
70
+
71
+ nil
72
+ end
73
+
74
+ private
75
+
76
+ def __update
77
+ time = @profiler.total_time
78
+ diff = time - @time
79
+ @time = time
80
+
81
+ if diff > 0
82
+ @listeners.each do |l|
83
+ l.add(diff)
84
+ end
85
+ end
86
+ end
87
+
88
+ class Window
89
+ attr_reader :time
90
+
91
+ def initialize(global)
92
+ @global = global
93
+ @time = 0
94
+ end
95
+
96
+ def update
97
+ @global&.update
98
+ end
99
+
100
+ def add(time)
101
+ @time += time
102
+ end
103
+
104
+ def release
105
+ @global&.release(self)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -135,14 +135,24 @@ module Skylight
135
135
  title = (opts[:title] || title).to_s
136
136
  desc = opts[:description].to_s if opts[:description]
137
137
 
138
+ # NOTE: The source location logic happens before we have have a config so we can'
139
+ # check if source locations are enabled. However, it only happens once so the potential impact
140
+ # should be minimal. This would more appropriately belong to Extensions::SourceLocation,
141
+ # but as that is a runtime concern, and this happens at compile time, there isn't currently
142
+ # a clean way to turn this on and off. The absence of the extension will cause the
143
+ # source_file and source_line to be removed from the trace span before it is submitted.
144
+ source_file, source_line = klass.instance_method(name).source_location
145
+
138
146
  klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
139
147
  alias_method :"before_instrument_#{name}", :"#{name}"
140
148
 
141
149
  def #{name}(*args, &blk)
142
150
  span = Skylight.instrument(
143
- category: :"#{category}",
151
+ category: :"#{category}",
144
152
  title: #{title.inspect},
145
- description: #{desc.inspect})
153
+ description: #{desc.inspect},
154
+ source_file: #{source_file.inspect},
155
+ source_line: #{source_line.inspect})
146
156
 
147
157
  meta = {}
148
158
  begin
@@ -154,6 +164,12 @@ module Skylight
154
164
  Skylight.done(span, meta) if span
155
165
  end
156
166
  end
167
+
168
+ if protected_method_defined?(:"before_instrument_#{name}")
169
+ protected :"#{name}"
170
+ elsif private_method_defined?(:"before_instrument_#{name}")
171
+ private :"#{name}"
172
+ end
157
173
  RUBY
158
174
  end
159
175
 
@@ -1,10 +1,109 @@
1
+ require "strscan"
2
+ require "securerandom"
3
+ require "skylight/util/logging"
4
+ require "skylight/extensions"
5
+
1
6
  module Skylight
2
- class Instrumenter < Core::Instrumenter
3
- def self.trace_class
4
- Trace
7
+ # @api private
8
+ class Instrumenter
9
+ KEY = :__skylight_current_trace
10
+
11
+ include Util::Logging
12
+
13
+ class TraceInfo
14
+ def initialize(key = KEY)
15
+ @key = key
16
+ @muted_key = "#{key}_muted"
17
+ end
18
+
19
+ def current
20
+ Thread.current[@key]
21
+ end
22
+
23
+ def current=(trace)
24
+ Thread.current[@key] = trace
25
+ end
26
+
27
+ # NOTE: This should only be set by the instrumenter, and only
28
+ # in the context of a `mute` block. Do not try to turn this
29
+ # flag on and off directly.
30
+ def muted=(val)
31
+ Thread.current[@muted_key] = val
32
+ end
33
+
34
+ def muted?
35
+ !!Thread.current[@muted_key]
36
+ end
37
+ end
38
+
39
+ attr_reader :uuid, :config, :gc, :extensions
40
+
41
+ def self.native_new(_uuid, _config_env)
42
+ raise "not implemented"
43
+ end
44
+
45
+ def self.new(config)
46
+ config.validate!
47
+
48
+ uuid = SecureRandom.uuid
49
+ inst = native_new(uuid, config.to_native_env)
50
+ inst.send(:initialize, uuid, config)
51
+ inst
52
+ end
53
+
54
+ def initialize(uuid, config)
55
+ @uuid = uuid
56
+ @gc = config.gc
57
+ @config = config
58
+ @subscriber = Skylight::Subscriber.new(config, self)
59
+
60
+ @trace_info = @config[:trace_info] || TraceInfo.new(KEY)
61
+ @mutex = Mutex.new
62
+ @extensions = Skylight::Extensions::Collection.new(@config)
63
+ end
64
+
65
+ def enable_extension!(name)
66
+ @mutex.synchronize { extensions.enable!(name) }
67
+ end
68
+
69
+ def disable_extension!(name)
70
+ @mutex.synchronize { extensions.disable!(name) }
71
+ end
72
+
73
+ def extension_enabled?(name)
74
+ extensions.enabled?(name)
75
+ end
76
+
77
+ def log_context
78
+ @log_context ||= { inst: uuid }
79
+ end
80
+
81
+ def native_start
82
+ raise "not implemented"
5
83
  end
6
84
 
7
- def check_install!
85
+ def native_stop
86
+ raise "not implemented"
87
+ end
88
+
89
+ def native_track_desc(_endpoint, _description)
90
+ raise "not implemented"
91
+ end
92
+
93
+ def native_submit_trace(_trace)
94
+ raise "not implemented"
95
+ end
96
+
97
+ def current_trace
98
+ @trace_info.current
99
+ end
100
+
101
+ def current_trace=(trace)
102
+ t { "setting current_trace=#{trace ? trace.uuid : 'nil'}; thread=#{Thread.current.object_id}" }
103
+ @trace_info.current = trace
104
+ end
105
+
106
+ def validate_installation
8
107
  # Warn if there was an error installing Skylight.
9
108
 
10
109
  if defined?(Skylight.check_install_errors)
@@ -13,24 +112,236 @@ module Skylight
13
112
 
14
113
  if !Skylight.native? && defined?(Skylight.warn_skylight_native_missing)
15
114
  Skylight.warn_skylight_native_missing(config)
16
- return
115
+ return false
17
116
  end
117
+
118
+ true
119
+ end
120
+
121
+ def muted=(val)
122
+ @trace_info.muted = val
123
+ end
124
+
125
+ def muted?
126
+ @trace_info.muted?
18
127
  end
19
128
 
20
- def process_sql(sql)
21
- Skylight.lex_sql(sql)
22
- rescue SqlLexError => e
23
- if config[:log_sql_parse_errors]
24
- config.logger.error "[#{e.formatted_code}] Failed to extract binds from SQL query. " \
25
- "It's likely that this query uses more advanced syntax than we currently support. " \
26
- "sql=#{sql.inspect}"
129
+ def mute
130
+ old_muted = muted?
131
+ self.muted = true
132
+ yield if block_given?
133
+ ensure
134
+ self.muted = old_muted
135
+ end
136
+
137
+ def unmute
138
+ old_muted = muted?
139
+ self.muted = false
140
+ yield if block_given?
141
+ ensure
142
+ self.muted = old_muted
143
+ end
144
+
145
+ def silence_warnings(context)
146
+ @warnings_silenced || @mutex.synchronize do
147
+ @warnings_silenced ||= {}
148
+ end
149
+
150
+ @warnings_silenced[context] = true
151
+ end
152
+
153
+ def warnings_silenced?(context)
154
+ @warnings_silenced && @warnings_silenced[context]
155
+ end
156
+
157
+ alias disable mute
158
+ alias disabled? muted?
159
+
160
+ def start!
161
+ # We do this here since we can't report these issues via Gem install without stopping install entirely.
162
+ return unless validate_installation
163
+
164
+ t { "starting instrumenter" }
165
+
166
+ unless config.validate_with_server
167
+ log_error "invalid config"
168
+ return
169
+ end
170
+
171
+ t { "starting native instrumenter" }
172
+ unless native_start
173
+ warn "failed to start instrumenter"
174
+ return
27
175
  end
176
+
177
+ enable_extension!(:source_location) if @config.enable_source_locations?
178
+ config.gc.enable
179
+ @subscriber.register!
180
+
181
+ ActiveSupport::Notifications.instrument("started_instrumenter.skylight", instrumenter: self)
182
+
183
+ self
184
+ rescue Exception => e
185
+ log_error "failed to start instrumenter; msg=%s; config=%s", e.message, config.inspect
186
+ t { e.backtrace.join("\n") }
28
187
  nil
29
188
  end
30
189
 
31
- def handle_instrumenter_error(trace, e)
32
- poison! if e.is_a?(Skylight::InstrumenterUnrecoverableError)
33
- super
190
+ def shutdown
191
+ @subscriber.unregister!
192
+ native_stop
193
+ end
194
+
195
+ def trace(endpoint, cat, title = nil, desc = nil, meta: nil, segment: nil, component: nil)
196
+ # If a trace is already in progress, continue with that one
197
+ if (trace = @trace_info.current)
198
+ return yield(trace) if block_given?
199
+
200
+ return trace
201
+ end
202
+
203
+ begin
204
+ meta ||= {}
205
+ extensions.process_trace_meta(meta)
206
+ trace = Trace.new(self, endpoint, Skylight::Util::Clock.nanos, cat, title, desc,
207
+ meta: meta, segment: segment, component: component)
208
+ rescue Exception => e
209
+ log_error e.message
210
+ t { e.backtrace.join("\n") }
211
+ return
212
+ end
213
+
214
+ @trace_info.current = trace
215
+ return trace unless block_given?
216
+
217
+ begin
218
+ yield trace
219
+ ensure
220
+ @trace_info.current = nil
221
+ t { "instrumenter submitting trace; trace=#{trace.uuid}" }
222
+ trace.submit
223
+ end
224
+ end
225
+
226
+ def instrument(cat, title = nil, desc = nil, meta = nil)
227
+ raise ArgumentError, "cat is required" unless cat
228
+
229
+ if muted?
230
+ return yield if block_given?
231
+
232
+ return
233
+ end
234
+
235
+ unless (trace = @trace_info.current)
236
+ return yield if block_given?
237
+
238
+ return
239
+ end
240
+
241
+ cat = cat.to_s
242
+
243
+ unless Skylight::CATEGORY_REGEX.match?(cat)
244
+ warn "invalid skylight instrumentation category; trace=%s; value=%s", trace.uuid, cat
245
+ return yield if block_given?
246
+
247
+ return
248
+ end
249
+
250
+ cat = "other.#{cat}" unless Skylight::TIER_REGEX.match?(cat)
251
+
252
+ unless (sp = trace.instrument(cat, title, desc, meta))
253
+ return yield if block_given?
254
+
255
+ return
256
+ end
257
+
258
+ return sp unless block_given?
259
+
260
+ begin
261
+ yield sp
262
+ rescue Exception => e
263
+ meta ||= {}
264
+ meta[:exception] = [e.class.name, e.message]
265
+ meta[:exception_object] = e
266
+ raise e
267
+ ensure
268
+ trace.done(sp, meta)
269
+ end
270
+ end
271
+
272
+ def broken!
273
+ return unless (trace = @trace_info.current)
274
+
275
+ trace.broken!
276
+ end
277
+
278
+ def poison!
279
+ @poisoned = true
280
+ end
281
+
282
+ def poisoned?
283
+ @poisoned
284
+ end
285
+
286
+ def done(span, meta = nil)
287
+ return unless (trace = @trace_info.current)
288
+
289
+ trace.done(span, meta)
290
+ end
291
+
292
+ def process(trace)
293
+ t { fmt "processing trace=#{trace.uuid}" }
294
+
295
+ if ignore?(trace)
296
+ t { fmt "ignoring trace=#{trace.uuid}" }
297
+ return false
298
+ end
299
+
300
+ begin
301
+ finalize_endpoint_segment(trace)
302
+ native_submit_trace(trace)
303
+ true
304
+ rescue => e
305
+ handle_instrumenter_error(trace, e)
306
+ end
307
+ end
308
+
309
+ def handle_instrumenter_error(trace, err)
310
+ poison! if err.is_a?(Skylight::InstrumenterUnrecoverableError)
311
+
312
+ warn "failed to submit trace to worker; trace=%s, err=%s", trace.uuid, err
313
+ t { "BACKTRACE:\n#{err.backtrace.join("\n")}" }
314
+
315
+ false
316
+ end
317
+
318
+ def ignore?(trace)
319
+ config.ignored_endpoints.include?(trace.endpoint)
320
+ end
321
+
322
+ # Because GraphQL can return multiple results, each of which
323
+ # may have their own success/error states, we need to set the
324
+ # skylight segment as follows:
325
+ #
326
+ # - when all queries have errors: "error"
327
+ # - when some queries have errors: "<rendered format>+error"
328
+ # - when no queries have errors: "<rendered format>"
329
+ #
330
+ # <rendered format> will be determined by the Rails controller as usual.
331
+ # See Instrumenter#finalize_endpoint_segment for the actual segment/error assignment.
332
+ def finalize_endpoint_segment(trace)
333
+ return unless (segment = trace.segment)
334
+
335
+ segment = case trace.compound_response_error_status
336
+ when :all
337
+ "error"
338
+ when :partial
339
+ "#{segment}+error"
340
+ else
341
+ segment
342
+ end
343
+
344
+ trace.endpoint += "<sk-segment>#{segment}</sk-segment>"
34
345
  end
35
346
  end
36
347
  end