skylight 4.2.3 → 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +420 -331
  3. data/CLA.md +1 -1
  4. data/CONTRIBUTING.md +2 -8
  5. data/ERRORS.md +3 -0
  6. data/LICENSE.md +7 -17
  7. data/README.md +1 -1
  8. data/ext/extconf.rb +61 -56
  9. data/ext/libskylight.yml +8 -6
  10. data/ext/skylight_native.c +26 -100
  11. data/lib/skylight/api.rb +32 -21
  12. data/lib/skylight/cli/doctor.rb +64 -65
  13. data/lib/skylight/cli/helpers.rb +19 -19
  14. data/lib/skylight/cli/merger.rb +142 -138
  15. data/lib/skylight/cli.rb +48 -46
  16. data/lib/skylight/config.rb +640 -201
  17. data/lib/skylight/data/cacert.pem +730 -1023
  18. data/lib/skylight/deprecation.rb +17 -0
  19. data/lib/skylight/errors.rb +26 -9
  20. data/lib/skylight/extensions/source_location.rb +291 -0
  21. data/lib/skylight/extensions.rb +95 -0
  22. data/lib/skylight/formatters/http.rb +18 -0
  23. data/lib/skylight/gc.rb +99 -0
  24. data/lib/skylight/helpers.rb +81 -36
  25. data/lib/skylight/instrumenter.rb +336 -18
  26. data/lib/skylight/middleware.rb +147 -1
  27. data/lib/skylight/native.rb +60 -12
  28. data/lib/skylight/native_ext_fetcher.rb +13 -14
  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 +87 -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 +20 -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 +41 -0
  72. data/lib/skylight/normalizers.rb +157 -0
  73. data/lib/skylight/probes/action_controller.rb +52 -0
  74. data/lib/skylight/probes/action_dispatch/request_id.rb +33 -0
  75. data/lib/skylight/probes/action_dispatch/routing/route_set.rb +30 -0
  76. data/lib/skylight/probes/action_dispatch.rb +2 -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/active_record_async.rb +96 -0
  82. data/lib/skylight/probes/delayed_job.rb +144 -0
  83. data/lib/skylight/probes/elasticsearch.rb +36 -0
  84. data/lib/skylight/probes/excon/middleware.rb +65 -0
  85. data/lib/skylight/probes/excon.rb +25 -0
  86. data/lib/skylight/probes/faraday.rb +23 -0
  87. data/lib/skylight/probes/graphql.rb +38 -0
  88. data/lib/skylight/probes/httpclient.rb +44 -0
  89. data/lib/skylight/probes/middleware.rb +135 -0
  90. data/lib/skylight/probes/mongo.rb +156 -0
  91. data/lib/skylight/probes/mongoid.rb +13 -0
  92. data/lib/skylight/probes/net_http.rb +54 -0
  93. data/lib/skylight/probes/rack_builder.rb +37 -0
  94. data/lib/skylight/probes/redis.rb +51 -0
  95. data/lib/skylight/probes/sequel.rb +29 -0
  96. data/lib/skylight/probes/sinatra.rb +66 -0
  97. data/lib/skylight/probes/sinatra_add_middleware.rb +10 -10
  98. data/lib/skylight/probes/tilt.rb +25 -0
  99. data/lib/skylight/probes.rb +173 -0
  100. data/lib/skylight/railtie.rb +166 -28
  101. data/lib/skylight/sidekiq.rb +47 -0
  102. data/lib/skylight/sinatra.rb +1 -1
  103. data/lib/skylight/subscriber.rb +130 -0
  104. data/lib/skylight/test.rb +147 -0
  105. data/lib/skylight/trace.rb +325 -22
  106. data/lib/skylight/user_config.rb +58 -0
  107. data/lib/skylight/util/allocation_free.rb +26 -0
  108. data/lib/skylight/util/clock.rb +57 -0
  109. data/lib/skylight/util/component.rb +22 -22
  110. data/lib/skylight/util/deploy.rb +19 -24
  111. data/lib/skylight/util/gzip.rb +20 -0
  112. data/lib/skylight/util/http.rb +106 -113
  113. data/lib/skylight/util/instrumenter_method.rb +26 -0
  114. data/lib/skylight/util/logging.rb +136 -0
  115. data/lib/skylight/util/lru_cache.rb +36 -0
  116. data/lib/skylight/util/platform.rb +3 -7
  117. data/lib/skylight/util/ssl.rb +1 -25
  118. data/lib/skylight/util.rb +12 -0
  119. data/lib/skylight/vendor/cli/thor/rake_compat.rb +1 -1
  120. data/lib/skylight/version.rb +5 -1
  121. data/lib/skylight/vm/gc.rb +60 -0
  122. data/lib/skylight.rb +201 -14
  123. metadata +134 -18
@@ -14,7 +14,7 @@ module Skylight
14
14
 
15
15
  if (opts = @__sk_instrument_next_method)
16
16
  @__sk_instrument_next_method = nil
17
- instrument_method(name, opts)
17
+ instrument_method(name, **opts)
18
18
  end
19
19
  end
20
20
 
@@ -24,7 +24,7 @@ module Skylight
24
24
 
25
25
  if (opts = @__sk_instrument_next_method)
26
26
  @__sk_instrument_next_method = nil
27
- instrument_class_method(name, opts)
27
+ instrument_class_method(name, **opts)
28
28
  end
29
29
  end
30
30
 
@@ -77,14 +77,12 @@ module Skylight
77
77
  # do_expensive_stuff
78
78
  # end
79
79
  # end
80
- def instrument_method(*args)
81
- opts = args.pop if args.last.is_a?(Hash)
82
-
80
+ def instrument_method(*args, **opts)
83
81
  if (name = args.pop)
84
82
  title = "#{self}##{name}"
85
- __sk_instrument_method_on(self, name, title, opts || {})
83
+ __sk_instrument_method_on(self, name, title, **opts)
86
84
  else
87
- @__sk_instrument_next_method = opts || {}
85
+ @__sk_instrument_next_method = opts
88
86
  end
89
87
  end
90
88
 
@@ -123,47 +121,94 @@ module Skylight
123
121
  #
124
122
  # instrument_class_method :my_method, title: 'Expensive work'
125
123
  # end
126
- def instrument_class_method(name, opts = {})
124
+ def instrument_class_method(name, **opts)
125
+ # NOTE: If the class is defined anonymously and then assigned to a variable this code
126
+ # will not be aware of the updated name.
127
127
  title = "#{self}.#{name}"
128
- __sk_instrument_method_on(__sk_singleton_class, name, title, opts || {})
128
+ __sk_instrument_method_on(__sk_singleton_class, name, title, **opts)
129
129
  end
130
130
 
131
131
  private
132
132
 
133
- def __sk_instrument_method_on(klass, name, title, opts)
134
- category = (opts[:category] || "app.method").to_s
135
- title = (opts[:title] || title).to_s
136
- desc = opts[:description].to_s if opts[:description]
133
+ HAS_ARGUMENT_FORWARDING = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0")
134
+
135
+ def __sk_instrument_method_on(klass, name, title, **opts)
136
+ category = (opts[:category] || "app.method").to_s
137
+ title = (opts[:title] || title).to_s
138
+ desc = opts[:description].to_s if opts[:description]
139
+
140
+ # NOTE: The source location logic happens before we have have a config so we can'
141
+ # check if source locations are enabled. However, it only happens once so the potential impact
142
+ # should be minimal. This would more appropriately belong to Extensions::SourceLocation,
143
+ # but as that is a runtime concern, and this happens at compile time, there isn't currently
144
+ # a clean way to turn this on and off. The absence of the extension will cause the
145
+ # source_file and source_line to be removed from the trace span before it is submitted.
146
+ source_file, source_line = klass.instance_method(name).source_location
137
147
 
138
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
139
- alias_method :"before_instrument_#{name}", :"#{name}"
148
+ # We should strongly prefer using the new argument-forwarding syntax (...) where available.
149
+ # In Ruby 2.7, the following are known to be syntax errors:
150
+ #
151
+ # - mixing positional arguments with argument forwarding (e.g., send(:method_name, ...))
152
+ # - calling a setter method with multiple arguments, unless dispatched via send or public_send.
153
+ #
154
+ # So it is possible, though not recommended, to define setter methods that take multiple arguments,
155
+ # keywords, and/or blocks. Unfortunately, this means that for setters, we still need to explicitly
156
+ # forward the different argument types.
157
+ is_setter_method = name.to_s.end_with?("=")
158
+
159
+ arg_string =
160
+ if HAS_ARGUMENT_FORWARDING
161
+ is_setter_method ? "*args, **kwargs, &blk" : "..."
162
+ else
163
+ "*args, &blk"
164
+ end
140
165
 
141
- def #{name}(*args, &blk)
142
- span = Skylight.instrument(
143
- category: :"#{category}",
144
- title: #{title.inspect},
145
- description: #{desc.inspect})
166
+ original_method_dispatch =
167
+ if is_setter_method
168
+ "self.send(:before_instrument_#{name}, #{arg_string})"
169
+ else
170
+ "before_instrument_#{name}(#{arg_string})"
171
+ end
146
172
 
147
- meta = {}
148
- begin
149
- send(:before_instrument_#{name}, *args, &blk)
150
- rescue Exception => e
151
- meta[:exception_object] = e
152
- raise e
153
- ensure
154
- Skylight.done(span, meta) if span
155
- end
156
- end
173
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
174
+ alias_method :"before_instrument_#{name}", :"#{name}" # alias_method :"before_instrument_process", :"process"
175
+ def #{name}(#{arg_string}) # def process(*args, **kwargs, &blk)
176
+ span = Skylight.instrument( # span = Skylight.instrument(
177
+ category: :"#{category}", # category: :"app.method",
178
+ title: #{title.inspect}, # title: "process",
179
+ description: #{desc.inspect}, # description: "Process data",
180
+ source_file: #{source_file.inspect}, # source_file: "myapp/lib/processor.rb",
181
+ source_line: #{source_line.inspect}) # source_line: 123)
182
+ #
183
+ meta = {} # meta = {}
184
+ #
185
+ begin # begin
186
+ #{original_method_dispatch} # self.before_instrument_process(...)
187
+ rescue Exception => e # rescue Exception => e
188
+ meta[:exception_object] = e # meta[:exception_object] = e
189
+ raise e # raise e
190
+ ensure # ensure
191
+ Skylight.done(span, meta) if span # Skylight.done(span, meta) if span
192
+ end # end
193
+ end # end
194
+ #
195
+ if protected_method_defined?(:"before_instrument_#{name}") # if protected_method_defined?(:"before_instrument_process")
196
+ protected :"#{name}" # protected :"process"
197
+ elsif private_method_defined?(:"before_instrument_#{name}") # elsif private_method_defined?(:"before_instrument_process")
198
+ private :"#{name}" # private :"process"
199
+ end # end
157
200
  RUBY
158
- end
201
+ end
159
202
 
160
- if respond_to?(:singleton_class)
161
- alias __sk_singleton_class singleton_class
162
- else
163
- def __sk_singleton_class
164
- class << self; self; end
203
+ if respond_to?(:singleton_class)
204
+ alias __sk_singleton_class singleton_class
205
+ else
206
+ def __sk_singleton_class
207
+ class << self
208
+ self
165
209
  end
166
210
  end
211
+ end
167
212
  end
168
213
 
169
214
  # @api private
@@ -1,36 +1,354 @@
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, :subscriber
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
- if defined?(Skylight.check_install_errors)
11
- Skylight.check_install_errors(config)
12
- end
109
+ Skylight.check_install_errors(config) if defined?(Skylight.check_install_errors)
13
110
 
14
111
  if !Skylight.native? && defined?(Skylight.warn_skylight_native_missing)
15
112
  Skylight.warn_skylight_native_missing(config)
16
- return
113
+ return false
17
114
  end
115
+
116
+ true
117
+ end
118
+
119
+ def muted=(val)
120
+ @trace_info.muted = val
121
+ end
122
+
123
+ def muted?
124
+ @trace_info.muted?
18
125
  end
19
126
 
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}"
127
+ def mute
128
+ old_muted = muted?
129
+ self.muted = true
130
+ yield if block_given?
131
+ ensure
132
+ self.muted = old_muted
133
+ end
134
+
135
+ def unmute
136
+ old_muted = muted?
137
+ self.muted = false
138
+ yield if block_given?
139
+ ensure
140
+ self.muted = old_muted
141
+ end
142
+
143
+ def silence_warnings(context)
144
+ @warnings_silenced || @mutex.synchronize { @warnings_silenced ||= {} }
145
+
146
+ @warnings_silenced[context] = true
147
+ end
148
+
149
+ def warnings_silenced?(context)
150
+ @warnings_silenced && @warnings_silenced[context]
151
+ end
152
+
153
+ alias disable mute
154
+ alias disabled? muted?
155
+
156
+ def start!
157
+ # We do this here since we can't report these issues via Gem install without stopping install entirely.
158
+ return unless validate_installation
159
+
160
+ t { "starting instrumenter" }
161
+
162
+ unless config.validate_with_server
163
+ log_error "invalid config"
164
+ return
165
+ end
166
+
167
+ t { "starting native instrumenter" }
168
+ unless native_start
169
+ warn "failed to start instrumenter"
170
+ return
27
171
  end
172
+
173
+ enable_extension!(:source_location) if @config.enable_source_locations?
174
+ config.gc.enable
175
+ @subscriber.register!
176
+
177
+ ActiveSupport::Notifications.instrument("started_instrumenter.skylight", instrumenter: self)
178
+
179
+ self
180
+ rescue Exception => e
181
+ log_error "failed to start instrumenter; msg=%s; config=%s", e.message, config.inspect
182
+ t { e.backtrace.join("\n") }
28
183
  nil
29
184
  end
30
185
 
31
- def handle_instrumenter_error(trace, e)
32
- poison! if e.is_a?(Skylight::InstrumenterUnrecoverableError)
33
- super
186
+ def shutdown
187
+ @subscriber.unregister!
188
+ native_stop
189
+ end
190
+
191
+ def trace(endpoint, cat, title = nil, desc = nil, meta: nil, segment: nil, component: nil)
192
+ # If a trace is already in progress, continue with that one
193
+ if (trace = @trace_info.current)
194
+ return yield(trace) if block_given?
195
+
196
+ return trace
197
+ end
198
+
199
+ begin
200
+ meta ||= {}
201
+ extensions.process_trace_meta(meta)
202
+ trace =
203
+ Trace.new(
204
+ self,
205
+ endpoint,
206
+ Skylight::Util::Clock.nanos,
207
+ cat,
208
+ title,
209
+ desc,
210
+ meta: meta,
211
+ segment: segment,
212
+ component: component
213
+ )
214
+ rescue Exception => e
215
+ log_error e.message
216
+ t { e.backtrace.join("\n") }
217
+ return
218
+ end
219
+
220
+ @trace_info.current = trace
221
+ return trace unless block_given?
222
+
223
+ begin
224
+ yield trace
225
+ ensure
226
+ @trace_info.current = nil
227
+ t { "instrumenter submitting trace; trace=#{trace.uuid}" }
228
+ trace.submit
229
+ end
230
+ end
231
+
232
+ def instrument(cat, title = nil, desc = nil, meta = nil)
233
+ raise ArgumentError, "cat is required" unless cat
234
+
235
+ if muted?
236
+ return yield if block_given?
237
+
238
+ return
239
+ end
240
+
241
+ unless (trace = @trace_info.current)
242
+ return yield if block_given?
243
+
244
+ return
245
+ end
246
+
247
+ cat = cat.to_s
248
+
249
+ unless Skylight::CATEGORY_REGEX.match?(cat)
250
+ warn "invalid skylight instrumentation category; trace=%s; value=%s", trace.uuid, cat
251
+ return yield if block_given?
252
+
253
+ return
254
+ end
255
+
256
+ cat = "other.#{cat}" unless Skylight::TIER_REGEX.match?(cat)
257
+
258
+ unless (sp = trace.instrument(cat, title, desc, meta))
259
+ return yield if block_given?
260
+
261
+ return
262
+ end
263
+
264
+ return sp unless block_given?
265
+
266
+ begin
267
+ yield sp
268
+ rescue Exception => e
269
+ meta ||= {}
270
+ meta[:exception] = [e.class.name, e.message]
271
+ meta[:exception_object] = e
272
+ raise e
273
+ ensure
274
+ trace.done(sp, meta)
275
+ end
276
+ end
277
+
278
+ def broken!
279
+ return unless (trace = @trace_info.current)
280
+
281
+ trace.broken!
282
+ end
283
+
284
+ def poison!
285
+ @poisoned = true
286
+ end
287
+
288
+ def poisoned?
289
+ @poisoned
290
+ end
291
+
292
+ def done(span, meta = nil)
293
+ return unless (trace = @trace_info.current)
294
+
295
+ trace.done(span, meta)
296
+ end
297
+
298
+ def process(trace)
299
+ t { fmt "processing trace=#{trace.uuid}" }
300
+
301
+ if ignore?(trace)
302
+ t { fmt "ignoring trace=#{trace.uuid}" }
303
+ return false
304
+ end
305
+
306
+ begin
307
+ finalize_endpoint_segment(trace)
308
+ native_submit_trace(trace)
309
+ true
310
+ rescue StandardError => e
311
+ handle_instrumenter_error(trace, e)
312
+ end
313
+ end
314
+
315
+ def handle_instrumenter_error(trace, err)
316
+ poison! if err.is_a?(Skylight::InstrumenterUnrecoverableError)
317
+
318
+ warn "failed to submit trace to worker; trace=%s, err=%s", trace.uuid, err
319
+ t { "BACKTRACE:\n#{err.backtrace.join("\n")}" }
320
+
321
+ false
322
+ end
323
+
324
+ def ignore?(trace)
325
+ config.ignored_endpoints.include?(trace.endpoint)
326
+ end
327
+
328
+ # Because GraphQL can return multiple results, each of which
329
+ # may have their own success/error states, we need to set the
330
+ # skylight segment as follows:
331
+ #
332
+ # - when all queries have errors: "error"
333
+ # - when some queries have errors: "<rendered format>+error"
334
+ # - when no queries have errors: "<rendered format>"
335
+ #
336
+ # <rendered format> will be determined by the Rails controller as usual.
337
+ # See Instrumenter#finalize_endpoint_segment for the actual segment/error assignment.
338
+ def finalize_endpoint_segment(trace)
339
+ return unless (segment = trace.segment)
340
+
341
+ segment =
342
+ case trace.compound_response_error_status
343
+ when :all
344
+ "error"
345
+ when :partial
346
+ "#{segment}+error"
347
+ else
348
+ segment
349
+ end
350
+
351
+ trace.endpoint += "<sk-segment>#{segment}</sk-segment>"
34
352
  end
35
353
  end
36
354
  end
@@ -1,4 +1,150 @@
1
+ require "securerandom"
2
+
1
3
  module Skylight
2
- class Middleware < Core::Middleware
4
+ # @api private
5
+ class Middleware
6
+ SKYLIGHT_REQUEST_ID = "skylight.request_id".freeze
7
+
8
+ class BodyProxy
9
+ def initialize(body, &block)
10
+ @body = body
11
+ @block = block
12
+ @closed = false
13
+ end
14
+
15
+ def respond_to_missing?(name, include_all = false)
16
+ return false if name.to_s !~ /^to_ary$/
17
+
18
+ @body.respond_to?(name, include_all)
19
+ end
20
+
21
+ def close
22
+ return if @closed
23
+
24
+ @closed = true
25
+ begin
26
+ @body.close if @body.respond_to? :close
27
+ ensure
28
+ @block.call
29
+ end
30
+ end
31
+
32
+ def closed?
33
+ @closed
34
+ end
35
+
36
+ # N.B. This method is a special case to address the bug described by
37
+ # https://github.com/rack/rack/issues/434.
38
+ # We are applying this special case for #each only. Future bugs of this
39
+ # class will be handled by requesting users to patch their ruby
40
+ # implementation, to save adding too many methods in this class.
41
+ def each(*args, &block)
42
+ @body.each(*args, &block)
43
+ end
44
+
45
+ def method_missing(*args, &block)
46
+ super if args.first.to_s =~ /^to_ary$/
47
+ @body.__send__(*args, &block)
48
+ end
49
+ end
50
+
51
+ def self.with_after_close(resp, debug_identifier: "unknown", &block)
52
+ unless resp.respond_to?(:to_ary)
53
+ if resp.respond_to?(:to_a)
54
+ Skylight.warn(
55
+ "Rack response from \"#{debug_identifier}\" cannot be implicitly converted to an array. " \
56
+ "This is in violation of the Rack SPEC and will raise an error in future versions."
57
+ )
58
+ resp = resp.to_a
59
+ else
60
+ Skylight.error(
61
+ "Rack response from \"#{debug_identifier}\" cannot be converted to an array. This is in " \
62
+ "violation of the Rack SPEC and may cause problems with Skylight operation."
63
+ )
64
+ return resp
65
+ end
66
+ end
67
+
68
+ status, headers, body = resp
69
+ [status, headers, BodyProxy.new(body, &block)]
70
+ end
71
+
72
+ include Skylight::Util::Logging
73
+
74
+ # For Util::Logging
75
+ attr_reader :config
76
+
77
+ def initialize(app, opts = {})
78
+ @app = app
79
+ @config = opts[:config]
80
+ end
81
+
82
+ def call(env)
83
+ set_request_id(env)
84
+
85
+ if Skylight.tracing?
86
+ debug "Already instrumenting. Make sure the Skylight Rack Middleware hasn't been added more than once."
87
+ return @app.call(env)
88
+ end
89
+
90
+ if env["REQUEST_METHOD"] == "HEAD"
91
+ t { "middleware skipping HEAD" }
92
+ @app.call(env)
93
+ else
94
+ begin
95
+ t { "middleware beginning trace" }
96
+ trace = Skylight.trace(endpoint_name(env), "app.rack.request", nil, meta: endpoint_meta(env), component: :web)
97
+ t { "middleware began trace=#{trace ? trace.uuid : nil}" }
98
+
99
+ resp = @app.call(env)
100
+
101
+ trace ? Middleware.with_after_close(resp, debug_identifier: "Rack App: #{@app.class}") { trace.submit } : resp
102
+ rescue Exception => e
103
+ t { "middleware exception: #{e}\n#{e.backtrace.join("\n")}" }
104
+ trace&.submit
105
+ raise
106
+ end
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def log_context
113
+ # Don't cache this, it will change
114
+ { request_id: current_request_id, inst: Skylight.instrumenter&.uuid }
115
+ end
116
+
117
+ # Allow for overwriting
118
+ def endpoint_name(_env)
119
+ "Rack"
120
+ end
121
+
122
+ def endpoint_meta(_env)
123
+ { source_location: Trace::SYNTHETIC }
124
+ end
125
+
126
+ # Request ID code based on ActionDispatch::RequestId
127
+ def set_request_id(env)
128
+ return if env[SKYLIGHT_REQUEST_ID]
129
+
130
+ existing_request_id = env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
131
+ self.current_request_id = env[SKYLIGHT_REQUEST_ID] = make_request_id(existing_request_id)
132
+ end
133
+
134
+ def make_request_id(request_id)
135
+ request_id && !request_id.empty? ? request_id.gsub(/[^\w\-]/, "".freeze)[0...255] : internal_request_id
136
+ end
137
+
138
+ def internal_request_id
139
+ SecureRandom.uuid
140
+ end
141
+
142
+ def current_request_id
143
+ Thread.current[SKYLIGHT_REQUEST_ID]
144
+ end
145
+
146
+ def current_request_id=(request_id)
147
+ Thread.current[SKYLIGHT_REQUEST_ID] = request_id
148
+ end
3
149
  end
4
150
  end