skylight 4.2.3 → 5.3.0

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 (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