skylight 4.3.2 → 5.0.1

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -3
  3. data/CONTRIBUTING.md +2 -8
  4. data/ext/extconf.rb +6 -5
  5. data/ext/libskylight.yml +7 -6
  6. data/ext/skylight_native.c +22 -99
  7. data/lib/skylight.rb +211 -14
  8. data/lib/skylight/api.rb +10 -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 +597 -127
  13. data/lib/skylight/deprecation.rb +17 -0
  14. data/lib/skylight/errors.rb +21 -6
  15. data/lib/skylight/extensions.rb +107 -0
  16. data/lib/skylight/extensions/source_location.rb +291 -0
  17. data/lib/skylight/formatters/http.rb +20 -0
  18. data/lib/skylight/gc.rb +109 -0
  19. data/lib/skylight/helpers.rb +69 -26
  20. data/lib/skylight/instrumenter.rb +326 -15
  21. data/lib/skylight/middleware.rb +138 -1
  22. data/lib/skylight/native.rb +52 -2
  23. data/lib/skylight/native_ext_fetcher.rb +4 -3
  24. data/lib/skylight/normalizers.rb +153 -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/shrine.rb +34 -0
  67. data/lib/skylight/normalizers/sql.rb +45 -0
  68. data/lib/skylight/probes.rb +181 -0
  69. data/lib/skylight/probes/action_controller.rb +48 -0
  70. data/lib/skylight/probes/action_dispatch.rb +2 -0
  71. data/lib/skylight/probes/action_dispatch/request_id.rb +29 -0
  72. data/lib/skylight/probes/action_dispatch/routing/route_set.rb +28 -0
  73. data/lib/skylight/probes/action_view.rb +43 -0
  74. data/lib/skylight/probes/active_job.rb +27 -0
  75. data/lib/skylight/probes/active_job_enqueue.rb +41 -0
  76. data/lib/skylight/probes/active_model_serializers.rb +50 -0
  77. data/lib/skylight/probes/delayed_job.rb +149 -0
  78. data/lib/skylight/probes/elasticsearch.rb +38 -0
  79. data/lib/skylight/probes/excon.rb +25 -0
  80. data/lib/skylight/probes/excon/middleware.rb +66 -0
  81. data/lib/skylight/probes/faraday.rb +23 -0
  82. data/lib/skylight/probes/graphql.rb +43 -0
  83. data/lib/skylight/probes/httpclient.rb +44 -0
  84. data/lib/skylight/probes/middleware.rb +126 -0
  85. data/lib/skylight/probes/mongo.rb +164 -0
  86. data/lib/skylight/probes/mongoid.rb +13 -0
  87. data/lib/skylight/probes/net_http.rb +54 -0
  88. data/lib/skylight/probes/redis.rb +63 -0
  89. data/lib/skylight/probes/sequel.rb +33 -0
  90. data/lib/skylight/probes/sinatra.rb +63 -0
  91. data/lib/skylight/probes/sinatra_add_middleware.rb +10 -10
  92. data/lib/skylight/probes/tilt.rb +27 -0
  93. data/lib/skylight/railtie.rb +162 -18
  94. data/lib/skylight/sidekiq.rb +48 -0
  95. data/lib/skylight/subscriber.rb +110 -0
  96. data/lib/skylight/test.rb +146 -0
  97. data/lib/skylight/trace.rb +307 -10
  98. data/lib/skylight/user_config.rb +61 -0
  99. data/lib/skylight/util.rb +12 -0
  100. data/lib/skylight/util/allocation_free.rb +26 -0
  101. data/lib/skylight/util/clock.rb +56 -0
  102. data/lib/skylight/util/component.rb +5 -2
  103. data/lib/skylight/util/deploy.rb +7 -10
  104. data/lib/skylight/util/gzip.rb +20 -0
  105. data/lib/skylight/util/http.rb +4 -10
  106. data/lib/skylight/util/instrumenter_method.rb +26 -0
  107. data/lib/skylight/util/logging.rb +138 -0
  108. data/lib/skylight/util/lru_cache.rb +40 -0
  109. data/lib/skylight/util/platform.rb +1 -1
  110. data/lib/skylight/vendor/cli/thor/rake_compat.rb +1 -1
  111. data/lib/skylight/version.rb +5 -1
  112. data/lib/skylight/vm/gc.rb +68 -0
  113. metadata +126 -13
@@ -0,0 +1,20 @@
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
+ internal: true }
17
+ end
18
+ end
19
+ end
20
+ 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
@@ -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,37 +121,82 @@ 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)
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)
134
136
  category = (opts[:category] || "app.method").to_s
135
137
  title = (opts[:title] || title).to_s
136
138
  desc = opts[:description].to_s if opts[:description]
137
139
 
138
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
139
- 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
140
147
 
141
- def #{name}(*args, &blk)
142
- span = Skylight.instrument(
143
- category: :"#{category}",
144
- title: #{title.inspect},
145
- description: #{desc.inspect})
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?("=")
146
158
 
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
159
+ arg_string =
160
+ if HAS_ARGUMENT_FORWARDING
161
+ is_setter_method ? "*args, **kwargs, &blk" : "..."
162
+ else
163
+ "*args, &blk"
156
164
  end
165
+
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
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
157
200
  RUBY
158
201
  end
159
202
 
@@ -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