honeybadger 5.10.2 → 5.11.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.
@@ -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