Skip to content

Commit

Permalink
feat: implements honeybadger.event by synchronous log call (#512)
Browse files Browse the repository at this point in the history
* Implements Honeybadger.event by sync log call

* API changes, add timestamp

* fix #event: Use Hash() and reverse merge order.

* 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 <josh@joshuawood.net>

* 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.

* install sqlite dev package for rails tests

* use sudo

* Okay, sqlite problem seems to be based on rubygems issue

sparklemotion/sqlite3-ruby#411

* I have no idea what I'm doing

* 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 <josh@joshuawood.net>

* 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)

* Don't memoize events config

The config is initialized after the agent is created (when the app loads).

* Lazy initialize events worker

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.

---------

Co-authored-by: Joshua Wood <josh@joshuawood.net>
  • Loading branch information
halfbyte and joshuap committed Feb 12, 2024
1 parent 597653d commit dbe7e3d
Show file tree
Hide file tree
Showing 18 changed files with 937 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ruby.yml
Expand Up @@ -57,6 +57,7 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
rubygems: latest

- name: Build and test regular ruby
run: |
Expand Down
42 changes: 41 additions & 1 deletion lib/honeybadger/agent.rb
Expand Up @@ -7,6 +7,7 @@
require 'honeybadger/plugin'
require 'honeybadger/logging'
require 'honeybadger/worker'
require 'honeybadger/events_worker'
require 'honeybadger/breadcrumbs'

module Honeybadger
Expand Down Expand Up @@ -354,6 +355,7 @@ def flush
yield
ensure
worker.flush
events_worker&.flush
end

# Stops the Honeybadger service.
Expand All @@ -362,9 +364,41 @@ def flush
# Honeybadger.stop # => nil
def stop(force = false)
worker.shutdown(force)
events_worker&.shutdown(force)
true
end

# Sends event to events backend
#
# @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
#
# @return [void]
def event(event_type, payload = {})
init_events_worker

ts = DateTime.now.new_offset(0).rfc3339
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

# @api private
attr_reader :config

Expand Down Expand Up @@ -437,7 +471,7 @@ def with_rack_env(rack_env, &block)
end

# @api private
attr_reader :worker
attr_reader :worker, :events_worker

# @api private
# @!method init!(...)
Expand Down Expand Up @@ -475,9 +509,15 @@ def send_now(object)
end

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

def with_error_handling
yield
rescue => ex
Expand Down
10 changes: 10 additions & 0 deletions lib/honeybadger/backend/base.rb
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/honeybadger/backend/debug.rb
Expand Up @@ -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
4 changes: 4 additions & 0 deletions lib/honeybadger/backend/null.rb
Expand Up @@ -24,6 +24,10 @@ def notify(feature, payload)
def check_in(id)
StubbedResponse.new
end

def event(payload)
StubbedResponse.new
end
end
end
end
17 changes: 14 additions & 3 deletions lib/honeybadger/backend/server.rb
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions lib/honeybadger/config.rb
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions lib/honeybadger/config/defaults.rb
Expand Up @@ -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,
Expand Down

0 comments on commit dbe7e3d

Please sign in to comment.