honeybadger 5.10.2 → 5.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ module Honeybadger
2
+ class Metric
3
+ attr_reader :name, :attributes, :samples
4
+
5
+ def self.metric_type
6
+ name.split('::').last.downcase
7
+ end
8
+
9
+ def self.signature(metric_type, name, attributes)
10
+ Digest::SHA1.hexdigest("#{metric_type}-#{name}-#{attributes.keys.join('-')}-#{attributes.values.join('-')}").to_sym
11
+ end
12
+
13
+ def self.register(registry, metric_name, attributes)
14
+ registry.get(metric_type, metric_name, attributes) ||
15
+ registry.register(new(metric_name, attributes))
16
+ end
17
+
18
+ def initialize(name, attributes)
19
+ @name = name
20
+ @attributes = attributes || {}
21
+ @samples = 0
22
+ end
23
+
24
+ def metric_type
25
+ self.class.metric_type
26
+ end
27
+
28
+ def signature
29
+ self.class.signature(metric_type, name, attributes)
30
+ end
31
+
32
+ def base_payload
33
+ attributes.merge({
34
+ event_type: "metric.hb",
35
+ metric_name: name,
36
+ metric_type: metric_type,
37
+ samples: samples
38
+ })
39
+ end
40
+
41
+ def event_payloads
42
+ payloads.map do |payload|
43
+ base_payload.merge(payload)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,175 @@
1
+ require 'honeybadger/logging'
2
+
3
+ module Honeybadger
4
+ # A concurrent queue to execute plugin collect blocks and registry.
5
+ # @api private
6
+ class MetricsWorker
7
+ extend Forwardable
8
+
9
+ include Honeybadger::Logging::Helper
10
+
11
+ # Sub-class thread so we have a named thread (useful for debugging in Thread.list).
12
+ class Thread < ::Thread; end
13
+
14
+ # Used to signal the worker to shutdown.
15
+ SHUTDOWN = :__hb_worker_shutdown!
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ @interval_seconds = 1
20
+ @mutex = Mutex.new
21
+ @marker = ConditionVariable.new
22
+ @queue = Queue.new
23
+ @shutdown = false
24
+ @start_at = nil
25
+ @pid = Process.pid
26
+ end
27
+
28
+ def push(msg)
29
+ return false unless config.insights_enabled?
30
+ return false unless start
31
+
32
+ queue.push(msg)
33
+ end
34
+
35
+ def send_now(msg)
36
+ return if msg.tick > 0
37
+
38
+ msg.call
39
+ msg.reset
40
+ end
41
+
42
+ def shutdown(force = false)
43
+ d { 'shutting down worker' }
44
+
45
+ mutex.synchronize do
46
+ @shutdown = true
47
+ end
48
+
49
+ return true if force
50
+ return true unless thread&.alive?
51
+
52
+ queue.push(SHUTDOWN)
53
+ !!thread.join
54
+ ensure
55
+ queue.clear
56
+ kill!
57
+ end
58
+
59
+ # Blocks until queue is processed up to this point in time.
60
+ def flush
61
+ mutex.synchronize do
62
+ if thread && thread.alive?
63
+ queue.push(marker)
64
+ marker.wait(mutex)
65
+ end
66
+ end
67
+ end
68
+
69
+ def start
70
+ return false unless can_start?
71
+
72
+ mutex.synchronize do
73
+ @shutdown = false
74
+ @start_at = nil
75
+
76
+ return true if thread&.alive?
77
+
78
+ @pid = Process.pid
79
+ @thread = Thread.new { run }
80
+ end
81
+
82
+ true
83
+ end
84
+
85
+ private
86
+
87
+ attr_reader :config, :queue, :pid, :mutex, :marker, :thread, :interval_seconds, :start_at
88
+
89
+ def shutdown?
90
+ mutex.synchronize { @shutdown }
91
+ end
92
+
93
+ def suspended?
94
+ mutex.synchronize { start_at && Time.now.to_i < start_at }
95
+ end
96
+
97
+ def can_start?
98
+ return false if shutdown?
99
+ return false if suspended?
100
+ true
101
+ end
102
+
103
+ def kill!
104
+ d { 'killing worker thread' }
105
+
106
+ if thread
107
+ Thread.kill(thread)
108
+ thread.join # Allow ensure blocks to execute.
109
+ end
110
+
111
+ true
112
+ end
113
+
114
+ def suspend(interval)
115
+ mutex.synchronize do
116
+ @start_at = Time.now.to_i + interval
117
+ queue.clear
118
+ end
119
+
120
+ # Must be performed last since this may kill the current thread.
121
+ kill!
122
+ end
123
+
124
+ def run
125
+ begin
126
+ d { 'worker started' }
127
+ loop do
128
+ case msg = queue.pop
129
+ when SHUTDOWN then break
130
+ when ConditionVariable then signal_marker(msg)
131
+ else work(msg)
132
+ end
133
+ end
134
+ ensure
135
+ d { 'stopping worker' }
136
+ end
137
+ rescue Exception => e
138
+ error {
139
+ msg = "Error in worker thread (shutting down) class=%s message=%s\n\t%s"
140
+ sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
141
+ }
142
+ ensure
143
+ release_marker
144
+ end
145
+
146
+ def work(msg)
147
+ send_now(msg)
148
+
149
+ if shutdown?
150
+ kill!
151
+ return
152
+ end
153
+ rescue StandardError => e
154
+ error {
155
+ err = "Error in worker thread class=%s message=%s\n\t%s"
156
+ sprintf(err, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
157
+ }
158
+ ensure
159
+ queue.push(msg) unless shutdown? || suspended?
160
+ sleep(interval_seconds)
161
+ end
162
+
163
+ # Release the marker. Important to perform during cleanup when shutting
164
+ # down, otherwise it could end up waiting indefinitely.
165
+ def release_marker
166
+ signal_marker(marker)
167
+ end
168
+
169
+ def signal_marker(marker)
170
+ mutex.synchronize do
171
+ marker.signal
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,99 @@
1
+ require 'honeybadger/instrumentation_helper'
2
+
3
+ module Honeybadger
4
+ class NotificationSubscriber
5
+ def start(name, id, payload)
6
+ @start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
7
+ end
8
+
9
+ def finish(name, id, payload)
10
+ @finish_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
11
+
12
+ return unless process?(name)
13
+
14
+ payload = {
15
+ instrumenter_id: id,
16
+ duration: ((@finish_time - @start_time) * 1000).round(2)
17
+ }.merge(format_payload(payload).compact)
18
+
19
+ record(name, payload)
20
+ end
21
+
22
+ def record(name, payload)
23
+ Honeybadger.event(name, payload)
24
+ end
25
+
26
+ def process?(event)
27
+ true
28
+ end
29
+
30
+ def format_payload(payload)
31
+ payload
32
+ end
33
+ end
34
+
35
+ class ActionControllerSubscriber < NotificationSubscriber
36
+ def format_payload(payload)
37
+ payload.except(:headers, :request, :response)
38
+ end
39
+ end
40
+
41
+ class ActionControllerCacheSubscriber < NotificationSubscriber
42
+ end
43
+
44
+ class ActiveSupportCacheSubscriber < NotificationSubscriber
45
+ end
46
+
47
+ class ActionViewSubscriber < NotificationSubscriber
48
+ PROJECT_ROOT = defined?(::Rails) ? ::Rails.root.to_s : ''
49
+
50
+ def format_payload(payload)
51
+ {
52
+ view: payload[:identifier].to_s.gsub(PROJECT_ROOT, '[PROJECT_ROOT]'),
53
+ layout: payload[:layout]
54
+ }
55
+ end
56
+ end
57
+
58
+ class ActiveRecordSubscriber < NotificationSubscriber
59
+ def format_payload(payload)
60
+ {
61
+ query: payload[:sql].to_s.gsub(/\s+/, ' ').strip,
62
+ async: payload[:async]
63
+ }
64
+ end
65
+ end
66
+
67
+ class ActiveJobSubscriber < NotificationSubscriber
68
+ def format_payload(payload)
69
+ job = payload[:job]
70
+ payload.except(:job).merge({
71
+ job_class: job.class,
72
+ job_id: job.job_id,
73
+ queue_name: job.queue_name
74
+ })
75
+ end
76
+ end
77
+
78
+ class ActiveJobMetricsSubscriber < NotificationSubscriber
79
+ include Honeybadger::InstrumentationHelper
80
+
81
+ def format_payload(payload)
82
+ {
83
+ job_class: payload[:job].class,
84
+ queue_name: payload[:job].queue_name
85
+ }
86
+ end
87
+
88
+ def record(name, payload)
89
+ metric_source 'active_job'
90
+ histogram name, { bins: [30, 60, 120, 300, 1800, 3600, 21_600] }.merge(payload)
91
+ end
92
+ end
93
+
94
+ class ActionMailerSubscriber < NotificationSubscriber
95
+ end
96
+
97
+ class ActiveStorageSubscriber < NotificationSubscriber
98
+ end
99
+ end
@@ -1,4 +1,5 @@
1
1
  require 'forwardable'
2
+ require 'honeybadger/instrumentation_helper'
2
3
 
3
4
  module Honeybadger
4
5
  # +Honeybadger::Plugin+ defines the API for registering plugins with
@@ -7,6 +8,11 @@ module Honeybadger
7
8
  # optional dependencies and load the plugin for each dependency only if it's
8
9
  # present in the application.
9
10
  #
11
+ # Plugins may also define a collect block that is repeatedly called from
12
+ # within a thread. The MetricsWorker contains a loop that will call all
13
+ # enabled plugins' collect method, and then sleep for 1 second. This block
14
+ # is useful for collecting and/or sending metrics at regular intervals.
15
+ #
10
16
  # See the plugins/ directory for examples of official plugins. If you're
11
17
  # interested in developing a plugin for Honeybadger, see the Integration
12
18
  # Guide: https://docs.honeybadger.io/ruby/gem-reference/integration.html
@@ -37,6 +43,14 @@ module Honeybadger
37
43
  # Honeybadger.notify(exception)
38
44
  # end
39
45
  # end
46
+ #
47
+ # collect do
48
+ # # This block will be periodically called at regular intervals. Here you can
49
+ # # gather metrics or inspect services. See the Honeybadger::InstrumentationHelper
50
+ # # module to see availble methods for metric collection.
51
+ # gauge 'scheduled_jobs', -> { MyFramework.stats.scheduled_jobs.count }
52
+ # gauge 'latency', -> { MyFramework.stats.latency }
53
+ # end
40
54
  # end
41
55
  # end
42
56
  # end
@@ -53,13 +67,15 @@ module Honeybadger
53
67
  @@instances
54
68
  end
55
69
 
56
- # Register a new plugin with Honeybadger. See {#requirement} and {#execution}.
70
+ # Register a new plugin with Honeybadger. See {#requirement}, {#execution}, and
71
+ # {#collect}..
57
72
  #
58
73
  # @example
59
74
  #
60
75
  # Honeybadger::Plugin.register 'my_framework' do
61
76
  # requirement { }
62
77
  # execution { }
78
+ # collect { }
63
79
  # end
64
80
  #
65
81
  # @param [String, Symbol] name The optional name of the plugin. Should use
@@ -111,12 +127,41 @@ module Honeybadger
111
127
  def_delegator :@config, :logger
112
128
  end
113
129
 
130
+ # @api private
131
+ class CollectorExecution < Execution
132
+ include Honeybadger::InstrumentationHelper
133
+
134
+ DEFAULT_COLLECTION_INTERVAL = 60
135
+
136
+ def initialize(name, config, options, &block)
137
+ @name = name
138
+ @config = config
139
+ @options = options
140
+ @block = block
141
+ @interval = config.collection_interval(name) || options.fetch(:interval, DEFAULT_COLLECTION_INTERVAL)
142
+ @end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @interval
143
+ end
144
+
145
+ def tick
146
+ @end_time - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
147
+ end
148
+
149
+ def reset
150
+ @end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @interval
151
+ end
152
+
153
+ def register!
154
+ Honeybadger.collect(self)
155
+ end
156
+ end
157
+
114
158
  # @api private
115
159
  def initialize(name)
116
160
  @name = name
117
161
  @loaded = false
118
162
  @requirements = []
119
163
  @executions = []
164
+ @collectors = []
120
165
  end
121
166
 
122
167
  # Define a requirement. All requirement blocks must return +true+ for the
@@ -165,6 +210,31 @@ module Honeybadger
165
210
  @executions << block
166
211
  end
167
212
 
213
+ # Define an collect block. Collect blocks will be added to an execution
214
+ # queue if requirement blocks return +true+. The block will be called as frequently
215
+ # as once per second, but can be configured to increase it's interval.
216
+ #
217
+ # @example
218
+ #
219
+ # Honeybadger::Plugin.register 'my_framework' do
220
+ # requirement { defined?(MyFramework) }
221
+ #
222
+ # collect do
223
+ # stats = MyFramework.stats
224
+ # gauge 'capacity', -> { stats.capcity }
225
+ # end
226
+ #
227
+ # collect(interval: 10) do
228
+ # stats = MyFramework.more_expensive_stats
229
+ # gauge 'other_stat', -> { stats.expensive_metric }
230
+ # end
231
+ # end
232
+ #
233
+ # @return nil
234
+ def collect(options={}, &block)
235
+ @collectors << [options, block]
236
+ end
237
+
168
238
  # @api private
169
239
  def ok?(config)
170
240
  @requirements.all? {|r| Execution.new(config, &r).call }
@@ -181,6 +251,7 @@ module Honeybadger
181
251
  elsif ok?(config)
182
252
  config.logger.debug(sprintf('load plugin name=%s', name))
183
253
  @executions.each {|e| Execution.new(config, &e).call }
254
+ @collectors.each {|o,b| CollectorExecution.new(name, config, o, &b).register! }
184
255
  @loaded = true
185
256
  else
186
257
  config.logger.debug(sprintf('skip plugin name=%s reason=requirement', name))
@@ -193,6 +264,11 @@ module Honeybadger
193
264
  false
194
265
  end
195
266
 
267
+ # @api private
268
+ def collectors
269
+ @collectors
270
+ end
271
+
196
272
  # @api private
197
273
  def loaded?
198
274
  @loaded
@@ -1,3 +1,5 @@
1
+ require 'honeybadger/notification_subscriber'
2
+
1
3
  module Honeybadger
2
4
  module Plugins
3
5
  module ActiveJob
@@ -33,17 +35,27 @@ module Honeybadger
33
35
  end
34
36
  end
35
37
 
36
- Plugin.register do
38
+ Plugin.register :active_job do
37
39
  requirement do
38
40
  defined?(::Rails.application) &&
39
41
  ::Rails.application.config.respond_to?(:active_job) &&
40
- (queue_adapter = ::Rails.application.config.active_job[:queue_adapter]) &&
41
- !EXCLUDED_ADAPTERS.include?(queue_adapter.to_sym) &&
42
- !(defined?(::GoodJob) && ::GoodJob.on_thread_error.nil? && queue_adapter.to_sym == :good_job) # Don't report errors if GoodJob is reporting them
42
+ !EXCLUDED_ADAPTERS.include?(::Rails.application.config.active_job[:queue_adapter].to_sym)
43
+ end
44
+
45
+ # Don't report errors if GoodJob is reporting them
46
+ requirement do
47
+ ::Rails.application.config.active_job[:queue_adapter].to_sym != :good_job ||
48
+ !::Rails.application.config.respond_to?(:good_job) ||
49
+ ::Rails.application.config.good_job.on_thread_error.nil?
43
50
  end
44
51
 
45
52
  execution do
46
53
  ::ActiveJob::Base.set_callback(:perform, :around, &ActiveJob.method(:perform_around))
54
+
55
+ if config.load_plugin_insights?(:active_job)
56
+ ::ActiveSupport::Notifications.subscribe(/(enqueue_at|enqueue|enqueue_retry|enqueue_all|perform|retry_stopped|discard)\.active_job/, Honeybadger::ActiveJobSubscriber.new)
57
+ ::ActiveSupport::Notifications.subscribe('perform.active_job', Honeybadger::ActiveJobMetricsSubscriber.new)
58
+ end
47
59
  end
48
60
  end
49
61
  end
@@ -0,0 +1,30 @@
1
+ require 'honeybadger/instrumentation_helper'
2
+ require 'honeybadger/plugin'
3
+
4
+ module Honeybadger
5
+ module Plugins
6
+ module Autotuner
7
+ Plugin.register :autotuner do
8
+ requirement { config.load_plugin_insights?(:autotuner) && defined?(::Autotuner) }
9
+
10
+ execution do
11
+ singleton_class.include(Honeybadger::InstrumentationHelper)
12
+
13
+ ::Autotuner.enabled = true
14
+
15
+ ::Autotuner.reporter = proc do |report|
16
+ Honeybadger.event("report.autotuner", report: report.to_s)
17
+ end
18
+
19
+ metric_source 'autotuner'
20
+
21
+ ::Autotuner.metrics_reporter = proc do |metrics|
22
+ metrics.each do |key, val|
23
+ gauge key, ->{ val }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,14 +1,29 @@
1
1
  require 'honeybadger/plugin'
2
- require 'honeybadger/ruby'
3
2
 
4
3
  module Honeybadger
5
4
  module Plugins
6
- Plugin.register do
5
+ Plugin.register :karafka do
7
6
  requirement { defined?(::Karafka) }
8
7
 
9
8
  execution do
10
9
  ::Karafka.monitor.subscribe('error.occurred') do |event|
11
10
  Honeybadger.notify(event[:error])
11
+ Honeybadger.event('error.occurred', error: event[:error]) if config.load_plugin_insights?(:karafka)
12
+ end
13
+
14
+ if config.load_plugin_insights?(:karafka)
15
+ ::Karafka.monitor.subscribe("consumer.consumed") do |event|
16
+ context = {
17
+ duration: event.payload[:time],
18
+ consumer: event.payload[:caller].class.to_s,
19
+ id: event.payload[:caller].id,
20
+ topic: event.payload[:caller].messages.metadata.topic,
21
+ messages_count: event.payload[:caller].messages.metadata.size,
22
+ partition: event.payload[:caller].messages.metadata.partition
23
+ }
24
+
25
+ Honeybadger.event('consumer.consumed.karafka', context)
26
+ end
12
27
  end
13
28
  end
14
29
  end
@@ -0,0 +1,52 @@
1
+ require 'net/http'
2
+ require 'honeybadger/plugin'
3
+ require 'honeybadger/instrumentation'
4
+ require 'resolv'
5
+
6
+ module Honeybadger
7
+ module Plugins
8
+ module Net
9
+ module HTTP
10
+ def request(request_data, body = nil, &block)
11
+ return super unless started?
12
+ return super if hb?
13
+
14
+ Honeybadger.instrumentation.monotonic_timer { super }.tap do |duration, response_data|
15
+ context = {
16
+ duration: duration,
17
+ method: request_data.method,
18
+ status: response_data.code.to_i
19
+ }.merge(parsed_uri_data(request_data))
20
+
21
+ Honeybadger.event('request.net_http', context)
22
+ end[1] # return the response data only
23
+ end
24
+
25
+ def hb?
26
+ address.to_s[/#{Honeybadger.config[:'connection.host'].to_s}/]
27
+ end
28
+
29
+ def parsed_uri_data(request_data)
30
+ uri = request_data.uri || build_uri(request_data)
31
+ {}.tap do |uri_data|
32
+ uri_data[:host] = uri.host
33
+ uri_data[:url] = uri.to_s if Honeybadger.config[:'net_http.insights.full_url']
34
+ end
35
+ end
36
+
37
+ def build_uri(request_data)
38
+ hostname = (address[/#{Resolv::IPv6::Regex}/]) ? "[#{address}]" : address
39
+ URI.parse("#{use_ssl? ? 'https' : 'http'}://#{hostname}#{request_data.path}")
40
+ end
41
+
42
+ Plugin.register :net_http do
43
+ requirement { config.load_plugin_insights?(:net_http) }
44
+
45
+ execution do
46
+ ::Net::HTTP.send(:prepend, Honeybadger::Plugins::Net::HTTP)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,4 +1,5 @@
1
1
  require 'honeybadger/plugin'
2
+ require 'honeybadger/notification_subscriber'
2
3
 
3
4
  module Honeybadger
4
5
  module Plugins
@@ -68,6 +69,20 @@ module Honeybadger
68
69
  end
69
70
  end
70
71
  end
72
+
73
+ Plugin.register :rails do
74
+ requirement { config.load_plugin_insights?(:rails_metrics) && defined?(::Rails.application) && ::Rails.application }
75
+
76
+ execution do
77
+ ::ActiveSupport::Notifications.subscribe(/(process_action|send_file|redirect_to|halted_callback|unpermitted_parameters)\.action_controller/, Honeybadger::ActionControllerSubscriber.new)
78
+ ::ActiveSupport::Notifications.subscribe(/(write_fragment|read_fragment|expire_fragment|exist_fragment\?)\.action_controller/, Honeybadger::ActionControllerCacheSubscriber.new)
79
+ ::ActiveSupport::Notifications.subscribe(/cache_(read|read_multi|generate|fetch_hit|write|write_multi|increment|decrement|delete|delete_multi|cleanup|prune|exist\?)\.active_support/, Honeybadger::ActiveSupportCacheSubscriber.new)
80
+ ::ActiveSupport::Notifications.subscribe(/^render_(template|partial|collection)\.action_view/, Honeybadger::ActionViewSubscriber.new)
81
+ ::ActiveSupport::Notifications.subscribe("sql.active_record", Honeybadger::ActiveRecordSubscriber.new)
82
+ ::ActiveSupport::Notifications.subscribe("process.action_mailer", Honeybadger::ActionMailerSubscriber.new)
83
+ ::ActiveSupport::Notifications.subscribe(/(service_upload|service_download)\.active_storage/, Honeybadger::ActiveStorageSubscriber.new)
84
+ end
85
+ end
71
86
  end
72
87
  end
73
88
  end