From 2dda6ec28357fe853dbd037f97b062402958cdba Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Thu, 7 Dec 2023 17:16:00 +0100 Subject: [PATCH 01/13] Implements Honeybadger.event by sync log call --- lib/honeybadger/agent.rb | 9 +++++++++ lib/honeybadger/singleton.rb | 1 + spec/unit/honeybadger/agent_spec.rb | 17 +++++++++++++++++ spec/unit/honeybadger_spec.rb | 6 ++++++ 4 files changed, 33 insertions(+) diff --git a/lib/honeybadger/agent.rb b/lib/honeybadger/agent.rb index fd2e08cf..4638053c 100644 --- a/lib/honeybadger/agent.rb +++ b/lib/honeybadger/agent.rb @@ -365,6 +365,15 @@ def stop(force = false) true end + # Sends event to events backend + # + # @param event_name [String] a string describing the event + # @param payload [Hash] Additional data to be sent with the event as keyword arguments + def event(event_name, **payload) + log_string = {event: event_name, payload: payload}.to_json + logger.debug(log_string) + end + # @api private attr_reader :config diff --git a/lib/honeybadger/singleton.rb b/lib/honeybadger/singleton.rb index dc4f0161..53016ad3 100644 --- a/lib/honeybadger/singleton.rb +++ b/lib/honeybadger/singleton.rb @@ -38,6 +38,7 @@ module Honeybadger def_delegator :'Honeybadger::Agent.instance', :breadcrumbs def_delegator :'Honeybadger::Agent.instance', :clear! def_delegator :'Honeybadger::Agent.instance', :track_deployment + def_delegator :'Honeybadger::Agent.instance', :event # @!macro [attach] def_delegator # @!method $2(...) diff --git a/spec/unit/honeybadger/agent_spec.rb b/spec/unit/honeybadger/agent_spec.rb index 6f78f29c..a6698dba 100644 --- a/spec/unit/honeybadger/agent_spec.rb +++ b/spec/unit/honeybadger/agent_spec.rb @@ -283,6 +283,23 @@ end end + context "#event" do + let(:logger) { double(NULL_LOGGER) } + let(:config) { Honeybadger::Config.new(api_key:'fake api key', logger: logger, debug: true) } + let(:instance) { Honeybadger::Agent.new(config) } + + subject { instance } + + it "logs an event" do + expected_msg = { event: "test_event", payload: { some_data: "is here" } }.to_json + expect(logger).to receive(:add) do |level, msg| + expect(level).to eq(Logger::Severity::INFO) + expect(msg).to match(Regexp.escape(expected_msg)) + end + subject.event("test_event", some_data: "is here") + end + end + context do let!(:instance) { described_class.new(config) } let(:config) { Honeybadger::Config.new(logger: NULL_LOGGER, debug: true) } diff --git a/spec/unit/honeybadger_spec.rb b/spec/unit/honeybadger_spec.rb index 996103f6..d85e189d 100644 --- a/spec/unit/honeybadger_spec.rb +++ b/spec/unit/honeybadger_spec.rb @@ -9,6 +9,7 @@ describe Honeybadger do it { should be_a Module } it { should respond_to :notify } + it { should respond_to :event } it { should respond_to :start } it { should respond_to :track_deployment } @@ -38,6 +39,11 @@ it "delegates ::flush to agent instance" do expect(Honeybadger::Agent.instance).to receive(:flush) Honeybadger.flush + end + + it "delegates ::event to agent instance" do + expect(Honeybadger::Agent.instance).to receive(:event) + Honeybadger.event("just an event") end describe "#context" do From 6bd416f51371f5301557a6109ca0f1ed4f72fc8f Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Fri, 8 Dec 2023 16:58:52 +0100 Subject: [PATCH 02/13] API changes, add timestamp --- lib/honeybadger/agent.rb | 9 +++++++-- spec/unit/honeybadger/agent_spec.rb | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/honeybadger/agent.rb b/lib/honeybadger/agent.rb index 4638053c..d4d51c41 100644 --- a/lib/honeybadger/agent.rb +++ b/lib/honeybadger/agent.rb @@ -369,8 +369,13 @@ def stop(force = false) # # @param event_name [String] a string describing the event # @param payload [Hash] Additional data to be sent with the event as keyword arguments - def event(event_name, **payload) - log_string = {event: event_name, payload: payload}.to_json + def event(event_type, payload={}) + ts = DateTime.now.new_offset(0).rfc3339 + unless payload.kind_of?(Hash) + logger.error("Event has non-hash payload") + return + end + log_string = payload.merge({event_type: event_type, ts: ts}).to_json logger.debug(log_string) end diff --git a/spec/unit/honeybadger/agent_spec.rb b/spec/unit/honeybadger/agent_spec.rb index a6698dba..6ad47138 100644 --- a/spec/unit/honeybadger/agent_spec.rb +++ b/spec/unit/honeybadger/agent_spec.rb @@ -291,10 +291,11 @@ subject { instance } it "logs an event" do - expected_msg = { event: "test_event", payload: { some_data: "is here" } }.to_json expect(logger).to receive(:add) do |level, msg| expect(level).to eq(Logger::Severity::INFO) - expect(msg).to match(Regexp.escape(expected_msg)) + expect(msg).to match(/"some_data":"is here"/) + expect(msg).to match(/"event_type":"test_event"/) + expect(msg).to match(/"ts":/) end subject.event("test_event", some_data: "is here") end From 2097dbfd42f2ac92694fa9049d4ebf7f3ad428fc Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Fri, 5 Jan 2024 12:23:03 +0100 Subject: [PATCH 03/13] fix #event: Use Hash() and reverse merge order. --- lib/honeybadger/agent.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/honeybadger/agent.rb b/lib/honeybadger/agent.rb index d4d51c41..5dd3078f 100644 --- a/lib/honeybadger/agent.rb +++ b/lib/honeybadger/agent.rb @@ -371,12 +371,8 @@ def stop(force = false) # @param payload [Hash] Additional data to be sent with the event as keyword arguments def event(event_type, payload={}) ts = DateTime.now.new_offset(0).rfc3339 - unless payload.kind_of?(Hash) - logger.error("Event has non-hash payload") - return - end - log_string = payload.merge({event_type: event_type, ts: ts}).to_json - logger.debug(log_string) + merged = {event_type: event_type, ts: ts}.merge(Hash(payload)) + logger.debug(merged.to_json) end # @api private From 086995197f7a3d9644c654f7ac50d80c546c04d1 Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Thu, 25 Jan 2024 11:25:51 +0100 Subject: [PATCH 04/13] feat: implement simple debug backend endpoint for events (#513) * Implement simple debug backend endpoint for events This currently is missing a queue and calls the backend directly from the agent. Should I implement an events_worker within this PR or in the PR that adds the server backend? * Refactor signature of events backend to take only one argument * WIP: Add worker * WIP start of worker spec * Worker spec successfully duplicated * Implement timeout mechanism using separate thread Given that the worker relies on the Queue as the main scheduling mechanism I saw no other way than to start a second thread that occasionally throws a message into the queue to check if the timeout is reached. This seems to work in testing. * Remove one timeout check, namespace config * Remove unused code * Add events worker to agent stop/flush commands * Fix debug message in events worker --------- Co-authored-by: Joshua Wood --- lib/honeybadger/agent.rb | 12 +- lib/honeybadger/backend/base.rb | 10 + lib/honeybadger/backend/debug.rb | 6 + lib/honeybadger/backend/null.rb | 4 + lib/honeybadger/config/defaults.rb | 10 + lib/honeybadger/events_worker.rb | 322 ++++++++++++++++ spec/unit/honeybadger/agent_spec.rb | 18 +- spec/unit/honeybadger/backend/base_spec.rb | 1 + spec/unit/honeybadger/backend/debug_spec.rb | 12 + spec/unit/honeybadger/events_worker_spec.rb | 389 ++++++++++++++++++++ 10 files changed, 773 insertions(+), 11 deletions(-) create mode 100644 lib/honeybadger/events_worker.rb create mode 100644 spec/unit/honeybadger/events_worker_spec.rb diff --git a/lib/honeybadger/agent.rb b/lib/honeybadger/agent.rb index 5dd3078f..bff3666d 100644 --- a/lib/honeybadger/agent.rb +++ b/lib/honeybadger/agent.rb @@ -7,6 +7,7 @@ require 'honeybadger/plugin' require 'honeybadger/logging' require 'honeybadger/worker' +require 'honeybadger/events_worker' require 'honeybadger/breadcrumbs' module Honeybadger @@ -74,7 +75,7 @@ def initialize(opts = {}) @breadcrumbs = nil end - init_worker + init_workers end # Sends an exception to Honeybadger. Does not report ignored exceptions by @@ -354,6 +355,7 @@ def flush yield ensure worker.flush + events_worker.flush end # Stops the Honeybadger service. @@ -362,6 +364,7 @@ def flush # Honeybadger.stop # => nil def stop(force = false) worker.shutdown(force) + events_worker.shutdown(force) true end @@ -372,7 +375,7 @@ def stop(force = false) def event(event_type, payload={}) ts = DateTime.now.new_offset(0).rfc3339 merged = {event_type: event_type, ts: ts}.merge(Hash(payload)) - logger.debug(merged.to_json) + events_worker.push(merged) end # @api private @@ -447,7 +450,7 @@ def with_rack_env(rack_env, &block) end # @api private - attr_reader :worker + attr_reader :worker, :events_worker # @api private # @!method init!(...) @@ -484,8 +487,9 @@ def send_now(object) true end - def init_worker + def init_workers @worker = Worker.new(config) + @events_worker = EventsWorker.new(config) end def with_error_handling diff --git a/lib/honeybadger/backend/base.rb b/lib/honeybadger/backend/base.rb index 34465c00..a2ce5022 100644 --- a/lib/honeybadger/backend/base.rb +++ b/lib/honeybadger/backend/base.rb @@ -109,6 +109,16 @@ def track_deployment(payload) notify(:deploys, payload) end + # Send event + # @example + # backend.event([{event_type: "email_received", ts: "2023-03-04T12:12:00+1:00", subject: 'Re: Aquisition' }}) + # + # @param [Array] payload array of event hashes to send + # @raise NotImplementedError + def event(payload) + raise NotImplementedError, "must define #event on subclass" + end + private attr_reader :config diff --git a/lib/honeybadger/backend/debug.rb b/lib/honeybadger/backend/debug.rb index 7a401dd8..4d557b40 100644 --- a/lib/honeybadger/backend/debug.rb +++ b/lib/honeybadger/backend/debug.rb @@ -17,6 +17,12 @@ def check_in(id) return Response.new(ENV['DEBUG_BACKEND_STATUS'].to_i, nil) if ENV['DEBUG_BACKEND_STATUS'] super end + + def event(payload) + logger.unknown("sending event to debug backend with event=#{payload.to_json}") + return Response.new(ENV['DEBUG_BACKEND_STATUS'].to_i, nil) if ENV['DEBUG_BACKEND_STATUS'] + super + end end end end diff --git a/lib/honeybadger/backend/null.rb b/lib/honeybadger/backend/null.rb index 17c259c7..3f2af2d8 100644 --- a/lib/honeybadger/backend/null.rb +++ b/lib/honeybadger/backend/null.rb @@ -24,6 +24,10 @@ def notify(feature, payload) def check_in(id) StubbedResponse.new end + + def event(payload) + StubbedResponse.new + end end end end diff --git a/lib/honeybadger/config/defaults.rb b/lib/honeybadger/config/defaults.rb index d884f127..2d944997 100644 --- a/lib/honeybadger/config/defaults.rb +++ b/lib/honeybadger/config/defaults.rb @@ -91,6 +91,16 @@ class Boolean; end default: 100, type: Integer }, + :'events.batch_size' => { + description: 'Send events batch if n events have accumulated', + default: 100, + type: Integer + }, + :'events.timeout' => { + description: 'Timeout after which the events batch will be sent regardless (in milliseconds)', + default: 30_000, + type: Integer + }, plugins: { description: 'An optional list of plugins to load. Default is to load all plugins.', default: nil, diff --git a/lib/honeybadger/events_worker.rb b/lib/honeybadger/events_worker.rb new file mode 100644 index 00000000..e0b9ff93 --- /dev/null +++ b/lib/honeybadger/events_worker.rb @@ -0,0 +1,322 @@ +require 'forwardable' +require 'net/http' + +require 'honeybadger/logging' + +module Honeybadger + # A concurrent queue to notify the backend. + # @api private + class EventsWorker + extend Forwardable + + include Honeybadger::Logging::Helper + + # Sub-class thread so we have a named thread (useful for debugging in Thread.list). + class Thread < ::Thread; end + + # Used to signal the worker to shutdown. + SHUTDOWN = :__hb_worker_shutdown! + FLUSH = :__hb_worker_flush! + CHECK_TIMEOUT = :__hb_worker_check_timeout! + + # The base number for the exponential backoff formula when calculating the + # throttle interval. `1.05 ** throttle` will reach an interval of 2 minutes + # after around 100 429 responses from the server. + BASE_THROTTLE = 1.05 + + # TODO: These could be configurable? + + def initialize(config) + @config = config + @throttle = 0 + @throttle_interval = 0 + @mutex = Mutex.new + @marker = ConditionVariable.new + @queue = Queue.new + @send_queue = Queue.new + @shutdown = false + @start_at = nil + @pid = Process.pid + @send_queue = [] + @last_sent = nil + + @max_events = config.get(:'events.batch_size') + @send_timeout = config.get(:'events.timeout') + end + + def push(msg) + return false unless start + + if queue.size >= config.max_queue_size + warn { sprintf('Unable to send event; reached max queue size of %s.', queue.size) } + return false + end + + queue.push(msg) + end + + def send_now(msg) + handle_response(send_to_backend(msg)) + end + + def shutdown(force = false) + d { 'shutting down events worker' } + + mutex.synchronize do + @shutdown = true + end + + return true if force + return true unless thread&.alive? + + if throttled? + warn { sprintf('Unable to send %s event(s) to Honeybadger (currently throttled)', queue.size) } unless queue.empty? + return true + end + + info { sprintf('Waiting to send %s events(s) to Honeybadger', queue.size) } unless queue.empty? + queue.push(FLUSH) + queue.push(SHUTDOWN) + !!thread.join + ensure + queue.clear + kill! + end + + # Blocks until queue is processed up to this point in time. + def flush + mutex.synchronize do + if thread && thread.alive? + queue.push(FLUSH) + queue.push(marker) + marker.wait(mutex) + end + end + end + + def start + return false unless can_start? + + mutex.synchronize do + @shutdown = false + @start_at = nil + + return true if thread&.alive? + + @pid = Process.pid + @thread = Thread.new { run } + @timeout_thread = Thread.new { schedule_timeout_check } + end + + true + end + + private + + attr_reader :config, :queue, :pid, :mutex, :marker, :thread, :timeout_thread, :throttle, + :throttle_interval, :start_at, :send_queue, :last_sent, :max_events, :send_timeout + + def_delegator :config, :backend + + def shutdown? + mutex.synchronize { @shutdown } + end + + def suspended? + mutex.synchronize { start_at && Time.now.to_i < start_at } + end + + def can_start? + return false if shutdown? + return false if suspended? + true + end + + def throttled? + mutex.synchronize { throttle > 0 } + end + + def kill! + d { 'killing worker thread' } + + if thread + Thread.kill(thread) + Thread.kill(timeout_thread) + thread.join # Allow ensure blocks to execute. + end + + true + end + + def suspend(interval) + mutex.synchronize do + @start_at = Time.now.to_i + interval + queue.clear + end + + # Must be performed last since this may kill the current thread. + kill! + end + + def schedule_timeout_check + loop do + sleep(send_timeout / 1000.0) + queue.push(CHECK_TIMEOUT) + end + end + + def run + begin + d { 'worker started' } + mutex.synchronize do + @last_sent = Time.now + end + loop do + case msg = queue.pop + when SHUTDOWN then break + when CHECK_TIMEOUT then check_timeout + when FLUSH then flush_send_queue + when ConditionVariable then signal_marker(msg) + else work(msg) + end + end + ensure + d { 'stopping worker' } + end + rescue Exception => e + error { + msg = "Error in worker thread (shutting down) class=%s message=%s\n\t%s" + sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t")) + } + ensure + release_marker + end + + def check_timeout + return if mutex.synchronize { send_queue.empty? } + ms_since = (Time.now.to_f - last_sent.to_f) * 1000.0 + if ms_since >= send_timeout + send_batch + end + end + + def enqueue_msg(msg) + mutex.synchronize do + @send_queue << msg + end + end + + def send_batch + send_now(mutex.synchronize { send_queue }) + mutex.synchronize do + @last_sent = Time.now + send_queue.clear + end + end + + def check_and_send + return if mutex.synchronize { send_queue.empty? } + if mutex.synchronize { send_queue.length } >= max_events + send_batch + end + end + + def flush_send_queue + return if mutex.synchronize { send_queue.empty? } + send_batch + rescue StandardError => e + error { + msg = "Error in worker thread class=%s message=%s\n\t%s" + sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t")) + } + end + + def work(msg) + enqueue_msg(msg) + check_and_send + + if shutdown? && throttled? + warn { sprintf('Unable to send %s events(s) to Honeybadger (currently throttled)', queue.size) } if queue.size > 1 + kill! + return + end + + sleep(throttle_interval) + rescue StandardError => e + error { + msg = "Error in worker thread class=%s message=%s\n\t%s" + sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t")) + } + end + + + def send_to_backend(msg) + d { 'events_worker sending to backend' } + response = backend.event(msg) + response + end + + def calc_throttle_interval + ((BASE_THROTTLE ** throttle) - 1).round(3) + end + + def inc_throttle + mutex.synchronize do + @throttle += 1 + @throttle_interval = calc_throttle_interval + throttle + end + end + + def dec_throttle + mutex.synchronize do + return nil if throttle == 0 + @throttle -= 1 + @throttle_interval = calc_throttle_interval + throttle + end + end + + def handle_response(response) + d { sprintf('events_worker response code=%s message=%s', response.code, response.message.to_s.dump) } + + case response.code + when 429, 503 + throttle = inc_throttle + warn { sprintf('Event send failed: project is sending too many events. code=%s throttle=%s interval=%s', response.code, throttle, throttle_interval) } + when 402 + warn { sprintf('Event send failed: payment is required. code=%s', response.code) } + suspend(3600) + when 403 + warn { sprintf('Event send failed: API key is invalid. code=%s', response.code) } + suspend(3600) + when 413 + warn { sprintf('Event send failed: Payload is too large. code=%s', response.code) } + when 201 + if throttle = dec_throttle + info { sprintf('Success ⚡ Event sent code=%s throttle=%s interval=%s', response.code, throttle, throttle_interval) } + else + info { sprintf('Success ⚡ Event sent code=%s', response.code) } + end + when :stubbed + info { sprintf('Success ⚡ Development mode is enabled; This event will be sent after app is deployed.') } + when :error + warn { sprintf('Event send failed: an unknown error occurred. code=%s error=%s', response.code, response.message.to_s.dump) } + else + warn { sprintf('Event send failed: unknown response from server. code=%s', response.code) } + end + end + + # Release the marker. Important to perform during cleanup when shutting + # down, otherwise it could end up waiting indefinitely. + def release_marker + signal_marker(marker) + end + + def signal_marker(marker) + mutex.synchronize do + marker.signal + end + end + end +end diff --git a/spec/unit/honeybadger/agent_spec.rb b/spec/unit/honeybadger/agent_spec.rb index 6ad47138..264bc8ed 100644 --- a/spec/unit/honeybadger/agent_spec.rb +++ b/spec/unit/honeybadger/agent_spec.rb @@ -1,4 +1,5 @@ require 'honeybadger/agent' +require 'honeybadger/events_worker' require 'timecop' describe Honeybadger::Agent do @@ -284,18 +285,21 @@ end context "#event" do - let(:logger) { double(NULL_LOGGER) } - let(:config) { Honeybadger::Config.new(api_key:'fake api key', logger: logger, debug: true) } + let(:config) { Honeybadger::Config.new(api_key:'fake api key', logger: NULL_LOGGER, backend: :debug) } + let(:events_worker) { double(Honeybadger::EventsWorker.new(config)) } let(:instance) { Honeybadger::Agent.new(config) } subject { instance } + before do + allow(instance).to receive(:events_worker).and_return(events_worker) + end + it "logs an event" do - expect(logger).to receive(:add) do |level, msg| - expect(level).to eq(Logger::Severity::INFO) - expect(msg).to match(/"some_data":"is here"/) - expect(msg).to match(/"event_type":"test_event"/) - expect(msg).to match(/"ts":/) + expect(events_worker).to receive(:push) do |msg| + expect(msg[:event_type]).to eq("test_event") + expect(msg[:some_data]).to eq("is here") + expect(msg[:ts]).not_to be_nil end subject.event("test_event", some_data: "is here") end diff --git a/spec/unit/honeybadger/backend/base_spec.rb b/spec/unit/honeybadger/backend/base_spec.rb index f1d671c8..582bd9ef 100644 --- a/spec/unit/honeybadger/backend/base_spec.rb +++ b/spec/unit/honeybadger/backend/base_spec.rb @@ -43,6 +43,7 @@ subject { described_class.new(config) } it { should respond_to :notify } + it { should respond_to :event } describe "#notify" do it "raises NotImplementedError" do diff --git a/spec/unit/honeybadger/backend/debug_spec.rb b/spec/unit/honeybadger/backend/debug_spec.rb index 949b4e5a..67564930 100644 --- a/spec/unit/honeybadger/backend/debug_spec.rb +++ b/spec/unit/honeybadger/backend/debug_spec.rb @@ -36,4 +36,16 @@ instance.check_in(10) end end + + describe "#event" do + it "logs the event" do + expect(logger).to receive(:unknown) do |msg| + expect(msg).to match(/"some_data":"is here"/) + expect(msg).to match(/"event_type":"test_event"/) + expect(msg).to match(/"ts":"test_timestamp"/) + end + + instance.event({event_type: "test_event", ts: "test_timestamp", some_data: "is here"}) + end + end end diff --git a/spec/unit/honeybadger/events_worker_spec.rb b/spec/unit/honeybadger/events_worker_spec.rb new file mode 100644 index 00000000..08e0c42a --- /dev/null +++ b/spec/unit/honeybadger/events_worker_spec.rb @@ -0,0 +1,389 @@ +require 'timecop' +require 'thread' + +require 'honeybadger/events_worker' +require 'honeybadger/config' +require 'honeybadger/backend' + +describe Honeybadger::EventsWorker do + let!(:instance) { described_class.new(config) } + let(:config) { + Honeybadger::Config.new( + logger: NULL_LOGGER, debug: true, backend: 'null', + :'events.batch_size' => 5, + :'events.timeout' => 10_000 + ) + } + let(:event) { {event_type: "test", ts: "not-important"} } + + subject { instance } + + after do + Thread.list.each do |thread| + next unless thread.kind_of?(Honeybadger::EventsWorker::Thread) + Thread.kill(thread) + end + end + + context "when an exception happens in the worker loop" do + before do + allow(instance.send(:queue)).to receive(:pop).and_raise('fail') + end + + it "does not raise when shutting down" do + instance.push(event) + + expect { instance.shutdown }.not_to raise_error + end + + it "exits the loop" do + instance.push(event) + instance.flush + + sleep(0.2) + expect(instance.send(:thread)).not_to be_alive + end + + it "logs the error" do + allow(config.logger).to receive(:error) + expect(config.logger).to receive(:error).with(/error/i) + + instance.push(event) + instance.flush + end + end + + context "when an exception happens during processing" do + before do + allow(instance).to receive(:sleep) + allow(instance).to receive(:handle_response).and_raise('fail') + end + + def flush + instance.push(event) + instance.flush + end + + it "does not raise when shutting down" do + flush + expect { instance.shutdown }.not_to raise_error + end + + it "does not exit the loop" do + flush + expect(instance.send(:thread)).to be_alive + end + end + + describe "#initialize" do + describe "#queue" do + subject { instance.send(:queue) } + + it { should be_a Queue } + end + + describe "#backend" do + subject { instance.send(:backend) } + + before do + allow(Honeybadger::Backend::Null).to receive(:new).with(config).and_return(config.backend) + end + + it { should be_a Honeybadger::Backend::Base } + + it "is initialized from config" do + should eq config.backend + end + end + end + + describe "#push" do + it "flushes payload to backend" do + expect(instance.send(:backend)).to receive(:event).with([event]).and_call_original + expect(instance.push(event)).not_to eq false + instance.flush + end + + context "when not started" do + before do + allow(instance).to receive(:start).and_return false + end + + it "rejects push" do + expect(instance.send(:queue)).not_to receive(:push) + expect(instance.push(event)).to eq false + end + end + + context "when queue is full" do + before do + allow(config).to receive(:max_queue_size).and_return(5) + allow(instance).to receive(:queue).and_return(double(size: 5)) + end + + it "rejects the push" do + expect(instance.send(:queue)).not_to receive(:push) + expect(instance.push(event)).to eq false + end + + it "warns the logger" do + allow(config.logger).to receive(:warn) + expect(config.logger).to receive(:warn).with(/reached max/i) + instance.push(event) + end + end + end + + describe "#start" do + it "starts the thread" do + expect { subject.start }.to change(subject, :thread).to(kind_of(Thread)) + end + + it "changes the pid to the current pid" do + allow(Process).to receive(:pid).and_return(:expected) + expect { subject.start }.to change(subject, :pid).to(:expected) + end + + context "when shutdown" do + before do + subject.shutdown + end + + it "doesn't start" do + expect { subject.start }.not_to change(subject, :thread) + end + end + + context "when suspended" do + before do + subject.send(:suspend, 300) + end + + context "and restart is in the future" do + it "doesn't start" do + expect { subject.start }.not_to change(subject, :thread) + end + end + + context "and restart is in the past" do + it "starts the thread" do + Timecop.travel(Time.now + 301) do + expect { subject.start }.to change(subject, :thread).to(kind_of(Thread)) + end + end + end + end + end + + describe "#shutdown" do + before { subject.start } + + it "blocks until queue is processed" do + expect(subject.send(:backend)).to receive(:event).with([event]).and_call_original + subject.push(event) + subject.shutdown + end + + it "stops the thread" do + subject.shutdown + + sleep(0.1) + expect(subject.send(:thread)).not_to be_alive + end + + context "when previously throttled" do + before do + 100.times { subject.send(:inc_throttle) } + subject.push(event) + sleep(0.01) # Pause to allow throttle to activate + end + + it "shuts down immediately" do + expect(subject.send(:backend)).not_to receive(:event) + subject.push(event) + subject.shutdown + end + + it "does not warn the logger when the queue is empty" do + allow(config.logger).to receive(:warn) + expect(config.logger).not_to receive(:warn) + subject.shutdown + end + + it "warns the logger when queue has items" do + subject.push(event) + allow(config.logger).to receive(:warn) + expect(config.logger).to receive(:warn).with(/throttled/i) + subject.shutdown + end + end + + context "when throttled during shutdown" do + before do + allow(subject.send(:backend)).to receive(:event).with(anything).and_return(Honeybadger::Backend::Response.new(429) ) + end + + it "shuts down immediately" do + expect(subject.send(:backend)).to receive(:event).exactly(1).times + 5.times { subject.push(event) } + subject.shutdown + end + + it "does not warn the logger when the queue is empty" do + allow(config.logger).to receive(:warn) + expect(config.logger).not_to receive(:warn).with(/throttled/) + + subject.push(event) + subject.shutdown + end + + it "warns the logger when the queue has additional items" do + allow(config.logger).to receive(:warn) + expect(config.logger).to receive(:warn).with(/throttled/i) + 100.times { subject.send(:inc_throttle) } + 10.times do + subject.push(event) + end + + subject.shutdown + end + end + end + + describe "#flush" do + it "blocks until queue is flushed" do + expect(subject.send(:backend)).to receive(:event).with([event]).and_call_original + subject.push(event) + subject.flush + end + end + + describe "#handle_response" do + def handle_response + instance.send(:handle_response, response) + end + + before do + allow(instance).to receive(:suspend).and_return true + end + + context "when 429" do + let(:response) { Honeybadger::Backend::Response.new(429) } + + it "adds throttle" do + expect { handle_response }.to change(instance, :throttle_interval).by(0.05) + end + end + + context "when 402" do + let(:response) { Honeybadger::Backend::Response.new(402) } + + it "shuts down the worker" do + expect(instance).to receive(:suspend) + handle_response + end + + it "warns the logger" do + expect(config.logger).to receive(:warn).with(/payment/) + handle_response + end + end + + context "when 403" do + let(:response) { Honeybadger::Backend::Response.new(403, %({"error":"unauthorized"})) } + + it "shuts down the worker" do + expect(instance).to receive(:suspend) + handle_response + end + + it "warns the logger" do + expect(config.logger).to receive(:warn).with(/invalid/) + handle_response + end + end + + context "when 413" do + let(:response) { Honeybadger::Backend::Response.new(413, %({"error":"Payload exceeds maximum size"})) } + + it "warns the logger" do + expect(config.logger).to receive(:warn).with(/too large/) + handle_response + end + end + + context "when 201" do + let(:response) { Honeybadger::Backend::Response.new(201) } + + context "and there is no throttle" do + it "doesn't change throttle" do + expect { handle_response }.not_to change(instance, :throttle_interval) + end + end + + context "and a throttle is set" do + before { instance.send(:inc_throttle) } + + it "removes throttle" do + expect { handle_response }.to change(instance, :throttle_interval).by(-0.05) + end + end + + it "doesn't warn" do + expect(config.logger).not_to receive(:warn) + handle_response + end + end + + context "when unknown" do + let(:response) { Honeybadger::Backend::Response.new(418) } + + it "warns the logger" do + expect(config.logger).to receive(:warn).with(/failed/) + handle_response + end + end + + context "when error" do + let(:response) { Honeybadger::Backend::Response.new(:error, nil, 'test error message') } + + it "warns the logger" do + expect(config.logger).to receive(:warn).with(/test error message/) + handle_response + end + end + end + + describe "batching" do + it "should send after batch size is reached" do + expect(subject.send(:backend)).to receive(:event).with([event] * 5).and_return(Honeybadger::Backend::Null::StubbedResponse.new) + 5.times do + subject.push(event) + end + sleep(0.2) + end + context "timeout" do + let(:config) { + Honeybadger::Config.new( + logger: NULL_LOGGER, debug: true, backend: 'null', + :'events.batch_size' => 5, + :'events.timeout' => 100 + ) + } + + it "should send after timeout when sending another" do + expect(subject.send(:backend)).to receive(:event).with([event]).twice().and_return(Honeybadger::Backend::Null::StubbedResponse.new) + subject.push(event) + sleep(0.2) + subject.push(event) + sleep(0.2) + end + + it "should send after timeout without new message" do + expect(subject.send(:backend)).to receive(:event).with([event]).and_return(Honeybadger::Backend::Null::StubbedResponse.new) + subject.push(event) + sleep(0.2) + end + end + end +end From 22b29b7d35f8d2a666443d2af4f950758f5a094c Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Thu, 25 Jan 2024 11:53:39 +0100 Subject: [PATCH 05/13] Slightly bump sleep values in test to fix jruby tests There seems to be a slight difference in how sleep works in jruby so the timeouts in the tests did not hit predictably. --- spec/unit/honeybadger/events_worker_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/honeybadger/events_worker_spec.rb b/spec/unit/honeybadger/events_worker_spec.rb index 08e0c42a..0b67059f 100644 --- a/spec/unit/honeybadger/events_worker_spec.rb +++ b/spec/unit/honeybadger/events_worker_spec.rb @@ -374,15 +374,15 @@ def handle_response it "should send after timeout when sending another" do expect(subject.send(:backend)).to receive(:event).with([event]).twice().and_return(Honeybadger::Backend::Null::StubbedResponse.new) subject.push(event) - sleep(0.2) + sleep(0.25) subject.push(event) - sleep(0.2) + sleep(0.25) end it "should send after timeout without new message" do expect(subject.send(:backend)).to receive(:event).with([event]).and_return(Honeybadger::Backend::Null::StubbedResponse.new) subject.push(event) - sleep(0.2) + sleep(0.25) end end end From 0871fabff8b93df7a4aaa24ece9ca41ebca6794f Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Thu, 25 Jan 2024 12:03:11 +0100 Subject: [PATCH 06/13] install sqlite dev package for rails tests --- .github/workflows/ruby.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 256fd6f2..7aa98dbe 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -51,6 +51,10 @@ jobs: BUNDLE_WITHOUT: "development" steps: - uses: actions/checkout@v4 + - name: setup sqlite3 + run: | + apt update && apt upgrade + apt install libsqlite-dev - name: Set up Ruby uses: ruby/setup-ruby@v1 From 5e9603c73c740d9287baf1e42215c1e63aea4c89 Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Thu, 25 Jan 2024 12:08:50 +0100 Subject: [PATCH 07/13] use sudo --- .github/workflows/ruby.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 7aa98dbe..24eed451 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -53,8 +53,8 @@ jobs: - uses: actions/checkout@v4 - name: setup sqlite3 run: | - apt update && apt upgrade - apt install libsqlite-dev + sudo apt-get update && sudo apt-get upgrade -y + sudo apt-get install -y libsqlite3-dev - name: Set up Ruby uses: ruby/setup-ruby@v1 From e92ed5e60159ebfe983894c2b0d11d0e2f14e5bf Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Thu, 25 Jan 2024 12:21:05 +0100 Subject: [PATCH 08/13] Okay, sqlite problem seems to be based on rubygems issue https://github.com/sparklemotion/sqlite3-ruby/issues/411 --- .github/workflows/ruby.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 24eed451..b645d3bf 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -51,10 +51,10 @@ jobs: BUNDLE_WITHOUT: "development" steps: - uses: actions/checkout@v4 - - name: setup sqlite3 + + - name: Upgrade rubygems run: | - sudo apt-get update && sudo apt-get upgrade -y - sudo apt-get install -y libsqlite3-dev + gem update --system - name: Set up Ruby uses: ruby/setup-ruby@v1 From 6e9372049987678776c083be8fc18cebc37668f1 Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Thu, 25 Jan 2024 12:23:29 +0100 Subject: [PATCH 09/13] I have no idea what I'm doing --- .github/workflows/ruby.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index b645d3bf..646ea342 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -52,15 +52,12 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Upgrade rubygems - run: | - gem update --system - - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true + rubygems: latest - name: Build and test regular ruby run: | From 637e192c83c94c6535f6aa195cba45553ebdaf1c Mon Sep 17 00:00:00 2001 From: Jan Krutisch Date: Wed, 7 Feb 2024 22:02:30 +0100 Subject: [PATCH 10/13] feat: http(s) backend implementation for events (#520) * Implement simple debug backend endpoint for events This currently is missing a queue and calls the backend directly from the agent. Should I implement an events_worker within this PR or in the PR that adds the server backend? * Refactor signature of events backend to take only one argument * WIP: Add worker * WIP start of worker spec * Worker spec successfully duplicated * Implement timeout mechanism using separate thread Given that the worker relies on the Queue as the main scheduling mechanism I saw no other way than to start a second thread that occasionally throws a message into the queue to check if the timeout is reached. This seems to work in testing. * Remove one timeout check, namespace config * Remove unused code * Add server back end functionality for events This adds a minimal set of tests to ensure API conformance I've tested the code manually against "the real thing(tm)" * Add events worker to agent stop/flush commands * Fix debug message in events worker --------- Co-authored-by: Joshua Wood --- lib/honeybadger/backend/server.rb | 17 +++++-- lib/honeybadger/util/http.rb | 6 +++ spec/unit/honeybadger/backend/server_spec.rb | 48 ++++++++++++++++++++ spec/unit/honeybadger/util/http_spec.rb | 26 +++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/lib/honeybadger/backend/server.rb b/lib/honeybadger/backend/server.rb index b042bd4b..5100a235 100644 --- a/lib/honeybadger/backend/server.rb +++ b/lib/honeybadger/backend/server.rb @@ -11,11 +11,10 @@ module Backend class Server < Base ENDPOINTS = { notices: '/v1/notices'.freeze, - deploys: '/v1/deploys'.freeze + deploys: '/v1/deploys'.freeze, }.freeze - CHECK_IN_ENDPOINT = '/v1/check_in'.freeze - + EVENTS_ENDPOINT = '/v1/events'.freeze HTTP_ERRORS = Util::HTTP::ERRORS @@ -48,6 +47,18 @@ def check_in(id) Response.new(:error, nil, "HTTP Error: #{e.class}") end + # Send event + # @example + # backend.event([{event_type: "email_received", ts: "2023-03-04T12:12:00+1:00", subject: 'Re: Aquisition' }}) + # + # @param [Array] payload array of event hashes to send + # @return [Response] + def event(payload) + Response.new(@http.post_newline_delimited(EVENTS_ENDPOINT, payload)) + rescue *HTTP_ERRORS => e + Response.new(:error, nil, "HTTP Error: #{e.class}") + end + private def payload_headers(payload) diff --git a/lib/honeybadger/util/http.rb b/lib/honeybadger/util/http.rb index 844729e7..d2030415 100644 --- a/lib/honeybadger/util/http.rb +++ b/lib/honeybadger/util/http.rb @@ -49,6 +49,12 @@ def post(endpoint, payload, headers = nil) response end + def post_newline_delimited(endpoint, payload, headers = nil) + response = http_connection.post(endpoint, compress(payload.map(&:to_json).join("\n")), http_headers(headers)) + debug { sprintf("http method=POST path=%s code=%d", endpoint.dump, response.code) } + response + end + private attr_reader :config diff --git a/spec/unit/honeybadger/backend/server_spec.rb b/spec/unit/honeybadger/backend/server_spec.rb index f643fe47..b71c5805 100644 --- a/spec/unit/honeybadger/backend/server_spec.rb +++ b/spec/unit/honeybadger/backend/server_spec.rb @@ -11,6 +11,7 @@ it { should respond_to :notify } it { should respond_to :check_in } + it { should respond_to :event } describe "#check_in" do it "returns a response" do @@ -79,6 +80,53 @@ def notify_backend subject.notify(:notices, payload) end + end + + describe "#event" do + it "returns the response" do + stub_http + expect(send_event).to be_a Honeybadger::Backend::Response + end + + it "adds auth headers" do + http = stub_http + expect(http).to receive(:post).with(anything, anything, hash_including({ 'X-API-Key' => 'abc123'})) + send_event + end + + it "serialises json and compresses" do + http = stub_http + expect(http).to receive(:post) do |path, body, headers| + cleartext_body = Zlib::Inflate.inflate(body) + json = JSON.parse(cleartext_body) + expect(json["ts"]).to_not be_nil + expect(json["event_type"]).to eq("checkout") + expect(json["increment"]).to eq(0) + end + send_event + end + + it "serialises json newline delimited and compresses" do + http = stub_http + expect(http).to receive(:post) do |path, body, headers| + cleartext_body = Zlib::Inflate.inflate(body) + + the_jsons = cleartext_body.split("\n").map { |t| JSON.parse(t) } + expect(the_jsons.length).to eq(2) + expect(the_jsons[0]["ts"]).to_not be_nil + expect(the_jsons[0]["event_type"]).to eq("checkout") + expect(the_jsons[0]["sum"]).to eq("123.23") + expect(the_jsons[0]["increment"]).to eq(0) + expect(the_jsons[1]["increment"]).to eq(1) + end + send_event(2) + end + + def send_event(count=1) + payload = [] + count.times {|i| payload << {ts: DateTime.now.new_offset(0).rfc3339, event_type: "checkout", sum: "123.23", increment: i} } + subject.event(payload) + end end end diff --git a/spec/unit/honeybadger/util/http_spec.rb b/spec/unit/honeybadger/util/http_spec.rb index 9e958401..93da9c7d 100644 --- a/spec/unit/honeybadger/util/http_spec.rb +++ b/spec/unit/honeybadger/util/http_spec.rb @@ -10,6 +10,7 @@ subject { described_class.new(config) } it { should respond_to :post } + it { should respond_to :post_newline_delimited } it { should respond_to :get } it "sends a user agent with version number" do @@ -57,6 +58,11 @@ expect(http_post).to be_a Net::HTTPResponse end + it "returns the response for post_newline_delimited" do + stub_http + expect(http_post_newline_delimited).to be_a Net::HTTPResponse + end + it "returns the response for #get" do stub_http expect(http_get).to be_a Net::HTTPResponse @@ -240,10 +246,30 @@ end end + describe "#post_newline_delimited" do + it "should properly serialize NDJSON and compress" do + http = stub_http + expect(http).to receive(:post) do |path, body, headers| + expect(path).to eq("/v1/foo") + decompressed = Zlib::Inflate.inflate(body) + parts = decompressed.split("\n").map { |part| JSON.parse(part) } + expect(parts.length).to be(2) + + Net::HTTPSuccess.new('1.2', '200', 'OK') + end + http_post_newline_delimited + end + end + def http_post subject.post('/v1/foo', double('Notice', to_json: '{}')) end + def http_post_newline_delimited + ts = DateTime.now.new_offset(0).rfc3339 + subject.post_newline_delimited('/v1/foo', [{ts: ts, event_type: "test"}, {ts: ts, event_type: "test2"}]) + end + def http_get subject.get('/v1/foo') end From 7ae6c225d386668a0176bdcb97c8e0111e61d705 Mon Sep 17 00:00:00 2001 From: Joshua Wood Date: Wed, 7 Feb 2024 15:04:14 -0800 Subject: [PATCH 11/13] Support Hash as first argument to Honeybadger#event (#521) This enables both signatures: # With event type as first argument (recommended): Honeybadger.event("user_signed_up", user_id: 123) # With just a payload: Honeybadger.event(event_type: "user_signed_up", user_id: 123) --- lib/honeybadger/agent.rb | 25 ++++++++++++++++++++++--- spec/unit/honeybadger/agent_spec.rb | 25 +++++++++++++++++++------ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/honeybadger/agent.rb b/lib/honeybadger/agent.rb index bff3666d..7fc4ba74 100644 --- a/lib/honeybadger/agent.rb +++ b/lib/honeybadger/agent.rb @@ -370,11 +370,30 @@ def stop(force = false) # Sends event to events backend # - # @param event_name [String] a string describing the event + # @example + # # With event type as first argument (recommended): + # Honeybadger.event("user_signed_up", user_id: 123) + # + # # With just a payload: + # Honeybadger.event(event_type: "user_signed_up", user_id: 123) + # + # @param event_name [String, Hash] a String describing the event or a Hash + # when the second argument is omitted. # @param payload [Hash] Additional data to be sent with the event as keyword arguments - def event(event_type, payload={}) + # + # @return [void] + def event(event_type, payload = {}) ts = DateTime.now.new_offset(0).rfc3339 - merged = {event_type: event_type, ts: ts}.merge(Hash(payload)) + merged = {ts: ts} + + if event_type.is_a?(String) + merged.merge!(event_type: event_type) + else + merged.merge!(Hash(event_type)) + end + + merged.merge!(Hash(payload)) + events_worker.push(merged) end diff --git a/spec/unit/honeybadger/agent_spec.rb b/spec/unit/honeybadger/agent_spec.rb index 264bc8ed..cd2bd3a5 100644 --- a/spec/unit/honeybadger/agent_spec.rb +++ b/spec/unit/honeybadger/agent_spec.rb @@ -295,13 +295,26 @@ allow(instance).to receive(:events_worker).and_return(events_worker) end - it "logs an event" do - expect(events_worker).to receive(:push) do |msg| - expect(msg[:event_type]).to eq("test_event") - expect(msg[:some_data]).to eq("is here") - expect(msg[:ts]).not_to be_nil + context "with event type as first argument" do + it "logs an event" do + expect(events_worker).to receive(:push) do |msg| + expect(msg[:event_type]).to eq("test_event") + expect(msg[:some_data]).to eq("is here") + expect(msg[:ts]).not_to be_nil + end + subject.event("test_event", some_data: "is here") + end + end + + context "with payload as first argument" do + it "logs an event" do + expect(events_worker).to receive(:push) do |msg| + expect(msg[:event_type]).to eq("test_event") + expect(msg[:some_data]).to eq("is here") + expect(msg[:ts]).not_to be_nil + end + subject.event(event_type: "test_event", some_data: "is here") end - subject.event("test_event", some_data: "is here") end end From 5bab240ca39a40ab31985ce5e2ece55f8b258712 Mon Sep 17 00:00:00 2001 From: Joshua Wood Date: Fri, 9 Feb 2024 10:06:16 -0800 Subject: [PATCH 12/13] Don't memoize events config The config is initialized after the agent is created (when the app loads). --- lib/honeybadger/config.rb | 8 ++++++++ lib/honeybadger/events_worker.rb | 11 ++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/honeybadger/config.rb b/lib/honeybadger/config.rb index 9e9b6f74..9106db53 100644 --- a/lib/honeybadger/config.rb +++ b/lib/honeybadger/config.rb @@ -224,6 +224,14 @@ def max_queue_size self[:max_queue_size] end + def events_batch_size + self[:'events.batch_size'] + end + + def events_timeout + self[:'events.timeout'] + end + def params_filters Array(self[:'request.filter_keys']) end diff --git a/lib/honeybadger/events_worker.rb b/lib/honeybadger/events_worker.rb index e0b9ff93..ccba4e57 100644 --- a/lib/honeybadger/events_worker.rb +++ b/lib/honeybadger/events_worker.rb @@ -39,9 +39,6 @@ def initialize(config) @pid = Process.pid @send_queue = [] @last_sent = nil - - @max_events = config.get(:'events.batch_size') - @send_timeout = config.get(:'events.timeout') end def push(msg) @@ -114,7 +111,7 @@ def start private attr_reader :config, :queue, :pid, :mutex, :marker, :thread, :timeout_thread, :throttle, - :throttle_interval, :start_at, :send_queue, :last_sent, :max_events, :send_timeout + :throttle_interval, :start_at, :send_queue, :last_sent def_delegator :config, :backend @@ -160,7 +157,7 @@ def suspend(interval) def schedule_timeout_check loop do - sleep(send_timeout / 1000.0) + sleep(config.events_timeout / 1000.0) queue.push(CHECK_TIMEOUT) end end @@ -195,7 +192,7 @@ def run def check_timeout return if mutex.synchronize { send_queue.empty? } ms_since = (Time.now.to_f - last_sent.to_f) * 1000.0 - if ms_since >= send_timeout + if ms_since >= config.events_timeout send_batch end end @@ -216,7 +213,7 @@ def send_batch def check_and_send return if mutex.synchronize { send_queue.empty? } - if mutex.synchronize { send_queue.length } >= max_events + if mutex.synchronize { send_queue.length } >= config.events_batch_size send_batch end end From c5133d33107f78aaef42171b784111beb1bde42a Mon Sep 17 00:00:00 2001 From: Joshua Wood Date: Mon, 12 Feb 2024 09:24:50 -0800 Subject: [PATCH 13/13] Lazy initialize events worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This results in less change for current users—if you aren't using insights, the extra threads don't need to run. We could change this back in the future. --- lib/honeybadger/agent.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/honeybadger/agent.rb b/lib/honeybadger/agent.rb index 7fc4ba74..86c21251 100644 --- a/lib/honeybadger/agent.rb +++ b/lib/honeybadger/agent.rb @@ -75,7 +75,7 @@ def initialize(opts = {}) @breadcrumbs = nil end - init_workers + init_worker end # Sends an exception to Honeybadger. Does not report ignored exceptions by @@ -355,7 +355,7 @@ def flush yield ensure worker.flush - events_worker.flush + events_worker&.flush end # Stops the Honeybadger service. @@ -364,7 +364,7 @@ def flush # Honeybadger.stop # => nil def stop(force = false) worker.shutdown(force) - events_worker.shutdown(force) + events_worker&.shutdown(force) true end @@ -383,6 +383,8 @@ def stop(force = false) # # @return [void] def event(event_type, payload = {}) + init_events_worker + ts = DateTime.now.new_offset(0).rfc3339 merged = {ts: ts} @@ -506,8 +508,13 @@ def send_now(object) true end - def init_workers + def init_worker + return if @worker @worker = Worker.new(config) + end + + def init_events_worker + return if @events_worker @events_worker = EventsWorker.new(config) end