skylight 3.1.4 → 5.3.4

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +465 -294
  3. data/CLA.md +1 -1
  4. data/CONTRIBUTING.md +11 -3
  5. data/ERRORS.md +3 -0
  6. data/LICENSE.md +8 -18
  7. data/README.md +1 -2
  8. data/bin/skylight +1 -1
  9. data/ext/extconf.rb +118 -122
  10. data/ext/libskylight.yml +8 -6
  11. data/ext/skylight_native.c +56 -100
  12. data/lib/skylight/api.rb +41 -27
  13. data/lib/skylight/cli/doctor.rb +68 -70
  14. data/lib/skylight/cli/helpers.rb +3 -5
  15. data/lib/skylight/cli/merger.rb +99 -92
  16. data/lib/skylight/cli.rb +40 -43
  17. data/lib/skylight/config.rb +656 -201
  18. data/lib/skylight/data/cacert.pem +730 -1023
  19. data/lib/skylight/deprecation.rb +17 -0
  20. data/lib/skylight/errors.rb +34 -16
  21. data/lib/skylight/extensions/source_location.rb +291 -0
  22. data/lib/skylight/extensions.rb +95 -0
  23. data/lib/skylight/formatters/http.rb +18 -0
  24. data/lib/skylight/gc.rb +99 -0
  25. data/lib/skylight/helpers.rb +82 -39
  26. data/lib/skylight/instrumenter.rb +339 -9
  27. data/lib/skylight/middleware.rb +147 -1
  28. data/lib/skylight/native.rb +71 -23
  29. data/lib/skylight/native_ext_fetcher.rb +39 -47
  30. data/lib/skylight/normalizers/action_controller/process_action.rb +68 -0
  31. data/lib/skylight/normalizers/action_controller/send_file.rb +51 -0
  32. data/lib/skylight/normalizers/action_dispatch/process_middleware.rb +22 -0
  33. data/lib/skylight/normalizers/action_dispatch/route_set.rb +27 -0
  34. data/lib/skylight/normalizers/action_view/render_collection.rb +24 -0
  35. data/lib/skylight/normalizers/action_view/render_layout.rb +25 -0
  36. data/lib/skylight/normalizers/action_view/render_partial.rb +23 -0
  37. data/lib/skylight/normalizers/action_view/render_template.rb +23 -0
  38. data/lib/skylight/normalizers/active_job/perform.rb +87 -0
  39. data/lib/skylight/normalizers/active_model_serializers/render.rb +32 -0
  40. data/lib/skylight/normalizers/active_record/instantiation.rb +16 -0
  41. data/lib/skylight/normalizers/active_record/sql.rb +20 -0
  42. data/lib/skylight/normalizers/active_storage.rb +28 -0
  43. data/lib/skylight/normalizers/active_support/cache.rb +11 -0
  44. data/lib/skylight/normalizers/active_support/cache_clear.rb +16 -0
  45. data/lib/skylight/normalizers/active_support/cache_decrement.rb +16 -0
  46. data/lib/skylight/normalizers/active_support/cache_delete.rb +16 -0
  47. data/lib/skylight/normalizers/active_support/cache_exist.rb +16 -0
  48. data/lib/skylight/normalizers/active_support/cache_fetch_hit.rb +16 -0
  49. data/lib/skylight/normalizers/active_support/cache_generate.rb +16 -0
  50. data/lib/skylight/normalizers/active_support/cache_increment.rb +16 -0
  51. data/lib/skylight/normalizers/active_support/cache_read.rb +16 -0
  52. data/lib/skylight/normalizers/active_support/cache_read_multi.rb +16 -0
  53. data/lib/skylight/normalizers/active_support/cache_write.rb +16 -0
  54. data/lib/skylight/normalizers/coach/handler_finish.rb +44 -0
  55. data/lib/skylight/normalizers/coach/middleware_finish.rb +33 -0
  56. data/lib/skylight/normalizers/couch_potato/query.rb +20 -0
  57. data/lib/skylight/normalizers/data_mapper/sql.rb +12 -0
  58. data/lib/skylight/normalizers/default.rb +24 -0
  59. data/lib/skylight/normalizers/elasticsearch/request.rb +20 -0
  60. data/lib/skylight/normalizers/faraday/request.rb +38 -0
  61. data/lib/skylight/normalizers/grape/endpoint.rb +28 -0
  62. data/lib/skylight/normalizers/grape/endpoint_render.rb +25 -0
  63. data/lib/skylight/normalizers/grape/endpoint_run.rb +39 -0
  64. data/lib/skylight/normalizers/grape/endpoint_run_filters.rb +20 -0
  65. data/lib/skylight/normalizers/grape/format_response.rb +20 -0
  66. data/lib/skylight/normalizers/graphiti/render.rb +22 -0
  67. data/lib/skylight/normalizers/graphiti/resolve.rb +31 -0
  68. data/lib/skylight/normalizers/graphql/base.rb +127 -0
  69. data/lib/skylight/normalizers/render.rb +79 -0
  70. data/lib/skylight/normalizers/sequel/sql.rb +12 -0
  71. data/lib/skylight/normalizers/shrine.rb +32 -0
  72. data/lib/skylight/normalizers/sql.rb +41 -0
  73. data/lib/skylight/normalizers.rb +157 -0
  74. data/lib/skylight/probes/action_controller.rb +52 -0
  75. data/lib/skylight/probes/action_dispatch/request_id.rb +33 -0
  76. data/lib/skylight/probes/action_dispatch/routing/route_set.rb +30 -0
  77. data/lib/skylight/probes/action_dispatch.rb +2 -0
  78. data/lib/skylight/probes/action_view.rb +42 -0
  79. data/lib/skylight/probes/active_job.rb +27 -0
  80. data/lib/skylight/probes/active_job_enqueue.rb +35 -0
  81. data/lib/skylight/probes/active_model_serializers.rb +50 -0
  82. data/lib/skylight/probes/active_record_async.rb +96 -0
  83. data/lib/skylight/probes/delayed_job.rb +144 -0
  84. data/lib/skylight/probes/elasticsearch.rb +45 -0
  85. data/lib/skylight/probes/excon/middleware.rb +65 -0
  86. data/lib/skylight/probes/excon.rb +25 -0
  87. data/lib/skylight/probes/faraday.rb +23 -0
  88. data/lib/skylight/probes/graphql.rb +38 -0
  89. data/lib/skylight/probes/httpclient.rb +44 -0
  90. data/lib/skylight/probes/middleware.rb +135 -0
  91. data/lib/skylight/probes/mongo.rb +169 -0
  92. data/lib/skylight/probes/mongoid.rb +6 -0
  93. data/lib/skylight/probes/net_http.rb +54 -0
  94. data/lib/skylight/probes/rack_builder.rb +37 -0
  95. data/lib/skylight/probes/redis.rb +68 -0
  96. data/lib/skylight/probes/sequel.rb +29 -0
  97. data/lib/skylight/probes/sinatra.rb +66 -0
  98. data/lib/skylight/probes/sinatra_add_middleware.rb +10 -10
  99. data/lib/skylight/probes/tilt.rb +25 -0
  100. data/lib/skylight/probes.rb +172 -0
  101. data/lib/skylight/railtie.rb +172 -15
  102. data/lib/skylight/sidekiq.rb +47 -0
  103. data/lib/skylight/sinatra.rb +2 -2
  104. data/lib/skylight/subscriber.rb +130 -0
  105. data/lib/skylight/test.rb +147 -0
  106. data/lib/skylight/trace.rb +331 -15
  107. data/lib/skylight/user_config.rb +60 -0
  108. data/lib/skylight/util/allocation_free.rb +26 -0
  109. data/lib/skylight/util/clock.rb +57 -0
  110. data/lib/skylight/util/component.rb +47 -9
  111. data/lib/skylight/util/deploy.rb +24 -40
  112. data/lib/skylight/util/gzip.rb +20 -0
  113. data/lib/skylight/util/hostname.rb +4 -4
  114. data/lib/skylight/util/http.rb +62 -71
  115. data/lib/skylight/util/instrumenter_method.rb +26 -0
  116. data/lib/skylight/util/logging.rb +136 -0
  117. data/lib/skylight/util/lru_cache.rb +36 -0
  118. data/lib/skylight/util/platform.rb +74 -0
  119. data/lib/skylight/util/proxy.rb +13 -0
  120. data/lib/skylight/util/ssl.rb +4 -28
  121. data/lib/skylight/util.rb +12 -0
  122. data/lib/skylight/vendor/cli/thor/rake_compat.rb +1 -1
  123. data/lib/skylight/version.rb +5 -1
  124. data/lib/skylight/vm/gc.rb +60 -0
  125. data/lib/skylight.rb +213 -24
  126. metadata +171 -53
@@ -6,16 +6,15 @@ module Skylight
6
6
  # into the class that you will be instrumenting. Then, annotate each method that
7
7
  # you wish to instrument with {Skylight::Helpers::ClassMethods#instrument_method instrument_method}.
8
8
  module Helpers
9
-
10
9
  # @see Skylight::Helpers
11
10
  module ClassMethods
12
11
  # @api private
13
12
  def method_added(name)
14
13
  super
15
14
 
16
- if opts = @__sk_instrument_next_method
15
+ if (opts = @__sk_instrument_next_method)
17
16
  @__sk_instrument_next_method = nil
18
- instrument_method(name, opts)
17
+ instrument_method(name, **opts)
19
18
  end
20
19
  end
21
20
 
@@ -23,9 +22,9 @@ module Skylight
23
22
  def singleton_method_added(name)
24
23
  super
25
24
 
26
- if opts = @__sk_instrument_next_method
25
+ if (opts = @__sk_instrument_next_method)
27
26
  @__sk_instrument_next_method = nil
28
- instrument_class_method(name, opts)
27
+ instrument_class_method(name, **opts)
29
28
  end
30
29
  end
31
30
 
@@ -78,14 +77,12 @@ module Skylight
78
77
  # do_expensive_stuff
79
78
  # end
80
79
  # end
81
- def instrument_method(*args)
82
- opts = args.pop if Hash === args.last
83
-
84
- if name = args.pop
85
- title = "#{to_s}##{name}"
86
- __sk_instrument_method_on(self, name, title, opts || {})
80
+ def instrument_method(*args, **opts)
81
+ if (name = args.pop)
82
+ title = "#{self}##{name}"
83
+ __sk_instrument_method_on(self, name, title, **opts)
87
84
  else
88
- @__sk_instrument_next_method = opts || {}
85
+ @__sk_instrument_next_method = opts
89
86
  end
90
87
  end
91
88
 
@@ -124,45 +121,92 @@ module Skylight
124
121
  #
125
122
  # instrument_class_method :my_method, title: 'Expensive work'
126
123
  # end
127
- def instrument_class_method(name, opts = {})
128
- title = "#{to_s}.#{name}"
129
- __sk_instrument_method_on(__sk_singleton_class, name, title, 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
+ title = "#{self}.#{name}"
128
+ __sk_instrument_method_on(__sk_singleton_class, name, title, **opts)
130
129
  end
131
130
 
132
- private
131
+ private
132
+
133
+ HAS_ARGUMENT_FORWARDING = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0")
133
134
 
134
- def __sk_instrument_method_on(klass, name, title, opts)
135
+ def __sk_instrument_method_on(klass, name, title, **opts)
135
136
  category = (opts[:category] || "app.method").to_s
136
- title = (opts[:title] || title).to_s
137
- desc = opts[:description].to_s if opts[:description]
137
+ title = (opts[:title] || title).to_s
138
+ desc = opts[:description].to_s if opts[:description]
138
139
 
139
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
140
- alias_method :"before_instrument_#{name}", :"#{name}"
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
147
+
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?("=")
141
158
 
142
- def #{name}(*args, &blk)
143
- span = Skylight.instrument(
144
- category: :"#{category}",
145
- title: #{title.inspect},
146
- description: #{desc.inspect})
159
+ arg_string =
160
+ if HAS_ARGUMENT_FORWARDING
161
+ is_setter_method ? "*args, **kwargs, &blk" : "..."
162
+ else
163
+ "*args, &blk"
164
+ end
147
165
 
148
- meta = {}
149
- begin
150
- send(:before_instrument_#{name}, *args, &blk)
151
- rescue Exception => e
152
- meta[:exception_object] = e
153
- raise e
154
- ensure
155
- Skylight.done(span, meta) if span
156
- end
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})"
157
171
  end
158
- RUBY
172
+
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
200
+ RUBY
159
201
  end
160
202
 
161
203
  if respond_to?(:singleton_class)
162
- alias :__sk_singleton_class :singleton_class
204
+ alias __sk_singleton_class singleton_class
163
205
  else
164
206
  def __sk_singleton_class
165
- class << self; self; end
207
+ class << self
208
+ self
209
+ end
166
210
  end
167
211
  end
168
212
  end
@@ -174,6 +218,5 @@ module Skylight
174
218
  extend ClassMethods
175
219
  end
176
220
  end
177
-
178
221
  end
179
222
  end
@@ -1,24 +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"
83
+ end
84
+
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
5
104
  end
6
105
 
7
- def check_install!
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)
113
+ return false
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?
125
+ end
126
+
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"
16
164
  return
17
165
  end
166
+
167
+ t { "starting native instrumenter" }
168
+ unless native_start
169
+ warn "failed to start instrumenter"
170
+ return
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") }
183
+ nil
184
+ end
185
+
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
18
230
  end
19
231
 
20
- def process_sql(sql)
21
- Skylight.lex_sql(sql)
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>"
22
352
  end
23
353
  end
24
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