diff --git a/.standard.yml b/.standard.yml index 5ff8447f..98aff45b 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,3 +1,3 @@ -ruby_version: 3.0 +ruby_version: 3.4 fix: true parallel: true diff --git a/.tool-versions b/.tool-versions index 0964b2a6..27a8619d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -ruby 3.5-dev +ruby 3.4.5 diff --git a/lib/honeybadger/config.rb b/lib/honeybadger/config.rb index 0af7e074..2c81228b 100644 --- a/lib/honeybadger/config.rb +++ b/lib/honeybadger/config.rb @@ -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 @@ -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 @@ -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"] diff --git a/lib/honeybadger/config/defaults.rb b/lib/honeybadger/config/defaults.rb index cdd77ed5..f0ebdcad 100644 --- a/lib/honeybadger/config/defaults.rb +++ b/lib/honeybadger/config/defaults.rb @@ -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, @@ -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": { @@ -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, @@ -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, diff --git a/lib/honeybadger/karafka.rb b/lib/honeybadger/karafka.rb index 0014624f..d41de347 100644 --- a/lib/honeybadger/karafka.rb +++ b/lib/honeybadger/karafka.rb @@ -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] @@ -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 @@ -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 @@ -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], @@ -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) @@ -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 @@ -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]) @@ -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 diff --git a/lib/honeybadger/notification_subscriber.rb b/lib/honeybadger/notification_subscriber.rb index 4e6bf11c..1f315e98 100644 --- a/lib/honeybadger/notification_subscriber.rb +++ b/lib/honeybadger/notification_subscriber.rb @@ -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 @@ -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)) + end + end end diff --git a/lib/honeybadger/plugins/autotuner.rb b/lib/honeybadger/plugins/autotuner.rb index b74202f2..adf71bfb 100644 --- a/lib/honeybadger/plugins/autotuner.rb +++ b/lib/honeybadger/plugins/autotuner.rb @@ -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 } diff --git a/lib/honeybadger/plugins/net_http.rb b/lib/honeybadger/plugins/net_http.rb index eec36b54..d78efa4a 100644 --- a/lib/honeybadger/plugins/net_http.rb +++ b/lib/honeybadger/plugins/net_http.rb @@ -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 diff --git a/lib/honeybadger/plugins/rails.rb b/lib/honeybadger/plugins/rails.rb index 3902b365..b787dcb9 100644 --- a/lib/honeybadger/plugins/rails.rb +++ b/lib/honeybadger/plugins/rails.rb @@ -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 diff --git a/lib/honeybadger/plugins/sidekiq.rb b/lib/honeybadger/plugins/sidekiq.rb index 2b1e4ee8..c323fb8f 100644 --- a/lib/honeybadger/plugins/sidekiq.rb +++ b/lib/honeybadger/plugins/sidekiq.rb @@ -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 @@ -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 @@ -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| diff --git a/lib/honeybadger/plugins/solid_queue.rb b/lib/honeybadger/plugins/solid_queue.rb index 52fed766..e454b41c 100644 --- a/lib/honeybadger/plugins/solid_queue.rb +++ b/lib/honeybadger/plugins/solid_queue.rb @@ -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 diff --git a/lib/puma/plugin/honeybadger.rb b/lib/puma/plugin/honeybadger.rb index 1f68dec2..8e543489 100644 --- a/lib/puma/plugin/honeybadger.rb +++ b/lib/puma/plugin/honeybadger.rb @@ -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 diff --git a/spec/integration/rails/event_subscriber_spec.rb b/spec/integration/rails/event_subscriber_spec.rb new file mode 100644 index 00000000..5a1d0b9f --- /dev/null +++ b/spec/integration/rails/event_subscriber_spec.rb @@ -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 diff --git a/spec/unit/honeybadger/config_spec.rb b/spec/unit/honeybadger/config_spec.rb index 077b710d..76288280 100644 --- a/spec/unit/honeybadger/config_spec.rb +++ b/spec/unit/honeybadger/config_spec.rb @@ -96,6 +96,84 @@ def init_instance end end end + + context "when options are deprecated" do + before do + # Unfreeze the constant to allow proxying for method expectations + stub_const("Honeybadger::Config::OPTIONS", Honeybadger::Config::OPTIONS.dup) + allow(Honeybadger::Config::OPTIONS).to receive(:dig).with(anything, :deprecated).and_return(nil) + allow(Honeybadger::Config::OPTIONS).to receive(:dig).with(anything, :deprecated_by).and_call_original + allow(Honeybadger::Config::OPTIONS).to receive(:dig).with(:env, :deprecated).and_return( + deprecated_value + ) + end + + context "with a deprecation message" do + let(:deprecated_value) { "The option `env` is deprecated. Use `environment_name` instead." } + + it "logs a deprecation warning with the message" do + expect(NULL_LOGGER).to receive(:add).with(Logger::Severity::WARN, a_string_including( + "`env`", + "`environment_name`", + "config_source=framework" + ), "honeybadger") + config.init!(api_key: "expected_api_key", env: "expected_env") + expect(config[:api_key]).to eq "expected_api_key" + expect(config[:env]).to eq "expected_env" + end + end + + context "without a deprecation message" do + let(:deprecated_value) { true } + + it "logs a deprecation warning with the default message" do + expect(NULL_LOGGER).to receive(:add).with(Logger::Severity::WARN, a_string_including( + "`env`", + "no effect", + "config_source=framework" + ), "honeybadger") + config.init!(api_key: "expected_api_key", env: "expected_env") + expect(config[:api_key]).to eq "expected_api_key" + expect(config[:env]).to eq "expected_env" + end + end + + context "with deprecated_by option" do + let(:deprecated_value) { true } + + before do + allow(Honeybadger::Config::OPTIONS).to receive(:dig).with(:env, :deprecated_by).and_return(:deprecated_by_env) + end + + context "when the deprecated_by key is not configured" do + it "logs a deprecation warning and configures the deprecated_by key" do + expect(NULL_LOGGER).to receive(:add).with(Logger::Severity::WARN, a_string_including( + "`env`", + "`deprecated_by_env`", + "config_source=framework" + ), "honeybadger") + config.init!(api_key: "expected_api_key", env: "expected_env") + expect(config[:api_key]).to eq "expected_api_key" + expect(config[:env]).to be_nil + expect(config[:deprecated_by_env]).to eq "expected_env" + end + end + + context "when the deprecated_by key is configured" do + it "logs a deprecation warning without overriding the deprecated_by key" do + expect(NULL_LOGGER).to receive(:add).with(Logger::Severity::WARN, a_string_including( + "`env`", + "`deprecated_by_env`", + "config_source=framework" + ), "honeybadger") + config.init!(api_key: "expected_api_key", env: "noop_env", deprecated_by_env: "expected_env") + expect(config[:api_key]).to eq "expected_api_key" + expect(config[:env]).to be_nil + expect(config[:deprecated_by_env]).to eq "expected_env" + end + end + end + end end describe "#get" do