Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .standard.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ruby_version: 3.0
ruby_version: 3.4
fix: true
parallel: true
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ruby 3.5-dev
ruby 3.4.5
57 changes: 40 additions & 17 deletions lib/honeybadger/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def load!(framework: {}, env: ENV)
self.env = Env.new(env).freeze
load_config_from_disk { |yaml| self.yaml = yaml.freeze }
detect_revision!
process_deprecations!
@loaded = true
self
end
Expand Down Expand Up @@ -301,24 +302,10 @@ def collection_interval(name)
self[:"#{name}.insights.collection_interval"]
end

def load_plugin_insights?(name)
def load_plugin_insights?(name, feature: nil)
return false unless insights_enabled?
return true if self[:"#{name}.insights.enabled"].nil?
!!self[:"#{name}.insights.enabled"]
end

def load_plugin_insights_events?(name)
return false unless insights_enabled?
return false unless load_plugin_insights?(name)
return true if self[:"#{name}.insights.events"].nil?
!!self[:"#{name}.insights.events"]
end

def load_plugin_insights_metrics?(name)
return false unless insights_enabled?
return false unless load_plugin_insights?(name)
return true if self[:"#{name}.insights.metrics"].nil?
!!self[:"#{name}.insights.metrics"]
return false unless self[:"#{name}.insights.enabled"]
feature.nil? || self[:"#{name}.insights.#{feature}"]
end

def root_regexp
Expand Down Expand Up @@ -365,6 +352,42 @@ def detect_revision!
set(:revision, Util::Revision.detect(self[:root]))
end

# When an option includes the `deprecated` property, warn the logger with
# the provided message (or a default message if `true`). If the
# `deprecated_by` property is present, automatically rename the option,
# removing the old key from the source.
def process_deprecations!
IVARS.each do |var|
source = instance_variable_get(var)

# We don't need to update the source unless there are deprecated_by options.
updated_source = nil

source.each_pair do |deprecated_key, value|
next unless (deprecated = OPTIONS.dig(deprecated_key, :deprecated))
deprecated_by = OPTIONS.dig(deprecated_key, :deprecated_by)

msg = if deprecated.is_a?(String)
deprecated
elsif deprecated_by
"The `#{deprecated_key}` option is deprecated. Use `#{deprecated_by}` instead."
else
"The `#{deprecated_key}` option is deprecated and has no effect."
end

warn("DEPRECATED: #{msg} config_source=#{var.to_s.delete_prefix("@")}")

if deprecated_by
updated_source ||= source.dup
updated_source[deprecated_by] = value unless updated_source.key?(deprecated_by)
updated_source.delete(deprecated_key)
end
end

instance_variable_set(var, updated_source.freeze) if updated_source
end
end

def log_path
return if log_stdout?
return if !self[:"logging.path"]
Expand Down
27 changes: 27 additions & 0 deletions lib/honeybadger/config/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ class Boolean; end
default: 0,
type: Integer
},
"active_job.insights.enabled": {
description: "Enable automatic data collection for Active Job.",
default: true,
type: Boolean
},
"delayed_job.attempt_threshold": {
description: "The number of attempts before notifications will be sent.",
default: 0,
Expand Down Expand Up @@ -413,6 +418,18 @@ class Boolean; end
"rails.insights.events": {
description: "Enable automatic event capturing for Ruby on Rails.",
default: true,
type: Boolean,
deprecated: true,
deprecated_by: :"rails.insights.active_support_events"
},
"rails.insights.active_support_events": {
description: "Enable automatic ActiveSupport::Notifications event capturing for Ruby on Rails.",
default: true,
type: Boolean
},
"rails.insights.structured_events": {
description: "Enable capturing of custom Rails.event events in Rails 8.1 and later.",
default: true,
type: Boolean
},
"rails.insights.metrics": {
Expand Down Expand Up @@ -503,6 +520,11 @@ class Boolean; end
default: 60,
type: Integer
},
"puma.insights.enabled": {
description: "Enable automatic data collection for Puma.",
default: true,
type: Boolean
},
"puma.insights.events": {
description: "Enable automatic event capturing for Puma stats.",
default: true,
Expand All @@ -518,6 +540,11 @@ class Boolean; end
default: 1,
type: Integer
},
"autotuner.insights.enabled": {
description: "Enable automatic data collection for Autotuner.",
default: true,
type: Boolean
},
"autotuner.insights.events": {
description: "Enable automatic event capturing for Autotuner stats.",
default: true,
Expand Down
20 changes: 10 additions & 10 deletions lib/honeybadger/karafka.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ def initialize
#
# @param event [Karafka::Core::Monitoring::Event]
def on_statistics_emitted(event)
if Honeybadger.config.load_plugin_insights_events?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :events)
Honeybadger.event("statistics_emitted.karafka", event.payload)
end

return unless Honeybadger.config.load_plugin_insights_metrics?(:karafka)
return unless Honeybadger.config.load_plugin_insights?(:karafka, feature: :metrics)

statistics = event[:statistics]
consumer_group_id = event[:consumer_group_id]
Expand Down Expand Up @@ -126,11 +126,11 @@ def on_error_occurred(event)
extra_tags.merge!(consumer_tags(event.payload[:caller]))
end

if Honeybadger.config.load_plugin_insights_events?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :events)
Honeybadger.event("error.occurred.karafka", error: event[:error], **extra_tags)
end

if Honeybadger.config.load_plugin_insights_metrics?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :metrics)
increment_counter("error_occurred", value: 1, **extra_tags)
end
end
Expand All @@ -144,7 +144,7 @@ def on_connection_listener_fetch_loop_received(event)
consumer_group_id = event[:subscription_group].consumer_group.id
extra_tags = {consumer_group: consumer_group_id}

if Honeybadger.config.load_plugin_insights_metrics?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :metrics)
histogram("listener_polling_time_taken", value: time_taken, **extra_tags)
histogram("listener_polling_messages", value: messages_count, **extra_tags)
end
Expand All @@ -160,7 +160,7 @@ def on_consumer_consumed(event)

tags = consumer_tags(consumer)

if Honeybadger.config.load_plugin_insights_events?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :events)
event_context = tags.merge({
consumer: consumer.class.name,
duration: event[:time],
Expand All @@ -171,7 +171,7 @@ def on_consumer_consumed(event)
Honeybadger.event("consumer.consumed.karafka", event_context)
end

if Honeybadger.config.load_plugin_insights_metrics?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :metrics)
increment_counter("consumer_messages", value: messages.count, **tags)
increment_counter("consumer_batches", value: 1, **tags)
gauge("consumer_offset", value: metadata.last_offset, **tags)
Expand All @@ -192,7 +192,7 @@ def on_consumer_consumed(event)
#
# @param event [Karafka::Core::Monitoring::Event]
def on_consumer_#{after}(event)
if Honeybadger.config.load_plugin_insights_metrics?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :metrics)
tags = consumer_tags(event.payload[:caller])
increment_counter('consumer_#{name}', value: 1, **tags)
end
Expand All @@ -205,7 +205,7 @@ def on_consumer_#{after}(event)
def on_worker_process(event)
jq_stats = event[:jobs_queue].statistics

if Honeybadger.config.load_plugin_insights_metrics?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :metrics)
gauge("worker_total_threads", value: ::Karafka::App.config.concurrency)
histogram("worker_processing", value: jq_stats[:busy])
histogram("worker_enqueued_jobs", value: jq_stats[:enqueued])
Expand All @@ -218,7 +218,7 @@ def on_worker_process(event)
def on_worker_processed(event)
jq_stats = event[:jobs_queue].statistics

if Honeybadger.config.load_plugin_insights_metrics?(:karafka)
if Honeybadger.config.load_plugin_insights?(:karafka, feature: :metrics)
histogram("worker_processing", value: jq_stats[:busy])
end
end
Expand Down
12 changes: 10 additions & 2 deletions lib/honeybadger/notification_subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ def finish(name, id, payload)
end

def record(name, payload)
if Honeybadger.config.load_plugin_insights_events?(:rails)
if Honeybadger.config.load_plugin_insights?(:rails, feature: :active_support_events)
Honeybadger.event(name, payload)
end

if Honeybadger.config.load_plugin_insights_metrics?(:rails)
if Honeybadger.config.load_plugin_insights?(:rails, feature: :metrics)
metric_source "rails"
record_metrics(name, payload)
end
Expand Down Expand Up @@ -164,4 +164,12 @@ def format_payload(payload)

class ActiveStorageSubscriber < NotificationSubscriber
end

class RailsEventSubscriber
def emit(event)
return unless Honeybadger.config.load_plugin_insights?(:rails, feature: :structured_events)

Honeybadger.event(event[:name], event.except(:name, :timestamp))
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first argument to Honeybadger.event should be the event type, but event[:name] is being passed. Based on the test expectations (line 18 of the spec expects event_type to equal \"test.rails_event\"), this should use the event name/type from the Rails.event notification. However, Rails.event likely doesn't include a :name key in its payload - it would be the first argument to Rails.event.notify(). Review the Rails.event API to determine the correct key to extract the event type from the event hash.

Suggested change
Honeybadger.event(event[:name], event.except(:name, :timestamp))
Honeybadger.event(event.name, event.payload)

Copilot uses AI. Check for mistakes.
end
end
end
4 changes: 2 additions & 2 deletions lib/honeybadger/plugins/autotuner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ module Autotuner
end

::Autotuner.metrics_reporter = proc do |metrics|
if config.load_plugin_insights_events?(:autotuner)
if config.load_plugin_insights?(:autotuner, feature: :events)
Honeybadger.event("stats.autotuner", metrics)
end

if config.load_plugin_insights_metrics?(:autotuner)
if config.load_plugin_insights?(:autotuner, feature: :metrics)
metric_source "autotuner"
metrics.each do |key, val|
gauge key, -> { val }
Expand Down
4 changes: 2 additions & 2 deletions lib/honeybadger/plugins/net_http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ def request(request_data, body = nil, &block)
status: response_data.code.to_i
}.merge(parsed_uri_data(request_data))

if @@hb_config.load_plugin_insights_events?(:net_http)
if @@hb_config.load_plugin_insights?(:net_http, feature: :events)
Honeybadger.event("request.net_http", context)
end

if @@hb_config.load_plugin_insights_metrics?(:net_http)
if @@hb_config.load_plugin_insights?(:net_http, feature: :metrics)
context.delete(:url)
Honeybadger.gauge("duration.request", context.merge(metric_source: "net_http"))
end
Expand Down
5 changes: 5 additions & 0 deletions lib/honeybadger/plugins/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ def self.source_ignored?(source)
::ActiveSupport::Notifications.subscribe("sql.active_record", Honeybadger::ActiveRecordSubscriber.new)
::ActiveSupport::Notifications.subscribe("process.action_mailer", Honeybadger::ActionMailerSubscriber.new)
::ActiveSupport::Notifications.subscribe(/(service_upload|service_download)\.active_storage/, Honeybadger::ActiveStorageSubscriber.new)

# Subscribe to Rails.event for structured event logging (Rails 8.1+)
if defined?(::Rails.event) && config.load_plugin_insights?(:rails, feature: :structured_events)
::Rails.event.subscribe(Honeybadger::RailsEventSubscriber.new)
end
end
end
end
Expand Down
10 changes: 5 additions & 5 deletions lib/honeybadger/plugins/sidekiq.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ def call(worker, msg, queue, &block)
ensure
context[:duration] = duration
context[:status] = status
if Honeybadger.config.load_plugin_insights_events?(:sidekiq)
if Honeybadger.config.load_plugin_insights?(:sidekiq, feature: :events)
Honeybadger.event("perform.sidekiq", context)
end

if Honeybadger.config.load_plugin_insights_metrics?(:sidekiq)
if Honeybadger.config.load_plugin_insights?(:sidekiq, feature: :metrics)
metric_source "sidekiq"
gauge "perform", context.slice(:worker, :queue, :duration)
end
Expand All @@ -60,7 +60,7 @@ def call(worker, msg, queue, _redis)
queue: queue
}

if Honeybadger.config.load_plugin_insights_events?(:sidekiq)
if Honeybadger.config.load_plugin_insights?(:sidekiq, feature: :events)
Honeybadger.event("enqueue.sidekiq", context)
end

Expand Down Expand Up @@ -225,11 +225,11 @@ def collect?
if config.cluster_collection?(:sidekiq) && (leader_checker.nil? || leader_checker.collect?)
stats = collect_sidekiq_stats.call

if Honeybadger.config.load_plugin_insights_events?(:sidekiq)
if Honeybadger.config.load_plugin_insights?(:sidekiq, feature: :events)
Honeybadger.event("stats.sidekiq", stats.except(:stats).merge(stats[:stats]))
end

if Honeybadger.config.load_plugin_insights_metrics?(:sidekiq)
if Honeybadger.config.load_plugin_insights?(:sidekiq, feature: :metrics)
metric_source "sidekiq"

stats[:stats].each do |name, value|
Expand Down
4 changes: 2 additions & 2 deletions lib/honeybadger/plugins/solid_queue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ module SolidQueue
if config.cluster_collection?(:solid_queue)
stats = collect_solid_queue_stats.call

if Honeybadger.config.load_plugin_insights_events?(:solid_queue)
if Honeybadger.config.load_plugin_insights?(:solid_queue, feature: :events)
Honeybadger.event("stats.solid_queue", stats.except(:stats).merge(stats[:stats]))
end

if Honeybadger.config.load_plugin_insights_metrics?(:solid_queue)
if Honeybadger.config.load_plugin_insights?(:solid_queue, feature: :metrics)
metric_source "solid_queue"
stats[:stats].each do |stat_name, value|
gauge stat_name, value: value
Expand Down
4 changes: 2 additions & 2 deletions lib/puma/plugin/honeybadger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ def record
end

def record_puma_stats(stats, context = {})
if Honeybadger.config.load_plugin_insights_events?(:puma)
if Honeybadger.config.load_plugin_insights?(:puma, feature: :events)
Honeybadger.event("stats.puma", context.merge(stats))
end

if Honeybadger.config.load_plugin_insights_metrics?(:puma)
if Honeybadger.config.load_plugin_insights?(:puma, feature: :metrics)
STATS_KEYS.each do |stat|
gauge stat, context, -> { stats[stat] } if stats[stat]
end
Expand Down
41 changes: 41 additions & 0 deletions spec/integration/rails/event_subscriber_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require_relative "../rails_helper"

describe "Rails Insights Event Subscriber", if: RAILS_PRESENT, type: :request do
load_rails_hooks(self)

before do
Honeybadger.config[:"insights.enabled"] = true
Honeybadger.config[:"events.batch_size"] = 0

Honeybadger::Backend::Test.events.clear
end

it "subscribes to Rails.event when available", if: defined?(Rails.event) do
Rails.event.notify("test.rails_event", {rails_key: "rails_value"})

sleep(0.1)

rails_events = Honeybadger::Backend::Test.events.select { |e| e[:event_type] == "test.rails_event" }
expect(rails_events).not_to be_empty
expect(rails_events.first[:payload][:rails_key]).to eq("rails_value")
expect(rails_events.first[:name]).to be_blank
end

it "does not capture Rails.event events when structured_events is disabled", if: defined?(Rails.event) do
Honeybadger.config[:"rails.insights.structured_events"] = false

# Reload the plugin to apply the new config
Honeybadger::Plugin.instances[:rails].load!(Honeybadger.config)

Rails.event.notify("test.disabled_event", {rails_key: "rails_value"})

sleep(0.1)

rails_events = Honeybadger::Backend::Test.events.select { |e| e[:event_type] == "test.disabled_event" }
expect(rails_events).to be_empty
end

it "gracefully handles Rails.event when not available", unless: defined?(Rails.event) do
expect { Honeybadger::Plugin.instances[:rails].load!(Honeybadger.config) }.not_to raise_error
end
end
Loading
Loading