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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.9.6]

### Added
- Enhanced Sentry integration

### Deprecated
- `Eventboss::ErrorHandlers::Sentry` is now deprecated in favor of `Eventboss::Sentry::ErrorHandler`

## [1.9.2] - 2025-01-21

- Fix typo in instance var during shut down
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@ Eventboss.configure do |config|
end
```

### Sentry Integration

Eventboss provides built-in integration with [Sentry](https://sentry.io/) for error monitoring and performance tracking. The simplest way to enable Sentry integration is to require the configuration module:

```ruby
require 'eventboss/sentry/configure'
```

For more advanced configuration options, you can manually configure the integration. Please inspect [lib/eventboss/sentry/configure.rb](lib/eventboss/sentry/configure.rb) to see options.
```

### Middlewares

Server middlewares intercept the execution of your `Listeners`. You can use to extract and run common functions on every message received.
Expand Down
8 changes: 8 additions & 0 deletions lib/eventboss/error_handlers/sentry.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
module Eventboss
module ErrorHandlers
class Sentry
def initialize
warn "[DEPRECATED] Eventboss::ErrorHandlers::Sentry is deprecated. " \
"Use Eventboss::Sentry::ErrorHandler instead. " \
"For automatic configuration, require 'eventboss/sentry/configure'. " \
"This class will be removed in a future version."
super
end

def call(exception, context = {})
eventboss_context = { component: 'eventboss' }
eventboss_context[:action] = context[:processor].class.to_s if context[:processor]
Expand Down
4 changes: 3 additions & 1 deletion lib/eventboss/long_poller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def fetch_messages
@client.receive_message(
queue_url: queue.url,
max_number_of_messages: 10,
wait_time_seconds: TIME_WAIT
wait_time_seconds: TIME_WAIT,
attribute_names: ['SentTimestamp', 'ApproximateReceiveCount'],
message_attribute_names: ['sentry-trace', 'baggage', 'sentry_user']
).messages
end
end
Expand Down
41 changes: 35 additions & 6 deletions lib/eventboss/publisher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,48 @@ def initialize(event_name, sns_client, configuration, opts = {})
end

def publish(payload)
topic_arn = Topic.build_arn(event_name: event_name, source_app: source)
sns_client.publish(
topic_arn: topic_arn,
message: json_payload(payload)
)
with_sentry_span do
sns_client.publish(**build_sns_params(payload))
end
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We would need to implement client middleware to support it cleanly. But that was never needed and never requested. That can be refactored then to support the current and old needs. Right now i don't want to crystal ball a future use-case


private

attr_reader :event_name, :sns_client, :configuration, :source

def sentry_enabled?
defined?(::Eventboss::Sentry::Integration) && ::Sentry.initialized?
end

def build_sns_params(payload)
{
topic_arn: Topic.build_arn(event_name: event_name, source_app: source),
message: json_payload(payload),
message_attributes: sentry_enabled? ? build_sns_message_attributes : {}
}
end

def with_sentry_span
return yield unless sentry_enabled?

queue_name = Queue.build_name(destination: source, event_name: event_name, env: Eventboss.env, source_app: source)

::Sentry.with_child_span(op: 'queue.publish', description: "Eventboss push #{source}/#{event_name}") do |span|
span.set_data(::Sentry::Span::DataConventions::MESSAGING_DESTINATION_NAME, ::Eventboss::Sentry::Context.queue_name_for_sentry(queue_name))

message = yield

span.set_data(::Sentry::Span::DataConventions::MESSAGING_MESSAGE_ID, message.message_id)
message
end
end

def json_payload(payload)
payload.is_a?(String) ? payload : payload.to_json
end

def build_sns_message_attributes
::Eventboss::Sentry::Context.build_sns_message_attributes
end
end
end
end
11 changes: 11 additions & 0 deletions lib/eventboss/sentry/configure.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

require_relative 'integration'

# Auto configure eventboss to use sentry

Eventboss.configure do |config|
config.server_middleware.add Eventboss::Sentry::ServerMiddleware
config.error_handlers << Eventboss::Sentry::ErrorHandler.new
end

35 changes: 35 additions & 0 deletions lib/eventboss/sentry/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module Eventboss
module Sentry
class Context
# since sentry has env selector, we can remove it from queue names
QUEUES_WITHOUT_ENV = Hash.new do |hash, key|
hash[key] = key
.gsub(/-#{Eventboss.env}-deadletter$/, '-ENV-deadletter')
.gsub(/-#{Eventboss.env}$/, '-ENV')
end

def self.queue_name_for_sentry(queue_name)
QUEUES_WITHOUT_ENV[queue_name]
end

# Constructs SNS message attributes for Sentry trace propagation.
def self.build_sns_message_attributes
attributes = ::Sentry.get_trace_propagation_headers
.slice('sentry-trace', 'baggage')
.transform_values do |header_value|
{ string_value: header_value, data_type: 'String' }
end

user = ::Sentry.get_current_scope&.user
if user && !user.empty?
attributes['sentry_user'] = {
string_value: user.to_json,
data_type: 'String'
}
end

attributes
end
end
end
end
15 changes: 15 additions & 0 deletions lib/eventboss/sentry/error_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Eventboss
module Sentry
class ErrorHandler
def call(exception, _context = {})
return unless ::Sentry.initialized?

Eventboss::Sentry::Integration.capture_exception(
exception,
contexts: { eventboss: { } },
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

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

[nitpick] The eventboss context is empty. Consider adding relevant context information such as queue name, message ID, or listener class to provide more debugging information.

Suggested change
contexts: { eventboss: { } },
contexts: { eventboss: _context },

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

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

The empty hash in the eventboss context provides no useful information. Consider adding relevant context data like queue name, message ID, or listener information to help with debugging.

Suggested change
contexts: { eventboss: { } },
eventboss_context = {}
eventboss_context[:queue_name] = _context[:queue_name] if _context.key?(:queue_name)
eventboss_context[:message_id] = _context[:message_id] if _context.key?(:message_id)
eventboss_context[:listener] = _context[:listener] if _context.key?(:listener)
Eventboss::Sentry::Integration.capture_exception(
exception,
contexts: { eventboss: eventboss_context },

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We already have that in the middleware

hint: { background: false }
)
end
end
end
end
15 changes: 15 additions & 0 deletions lib/eventboss/sentry/integration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'sentry-ruby'
require 'sentry/integrable'
require_relative 'error_handler'
require_relative 'context'
require_relative 'server_middleware'

module Eventboss
module Sentry
class Integration
extend ::Sentry::Integrable

register_integration name: "eventboss", version: Eventboss::VERSION
end
end
end
95 changes: 95 additions & 0 deletions lib/eventboss/sentry/server_middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
module Eventboss
module Sentry
class ServerMiddleware < Eventboss::Middleware::Base
OP_NAME = 'queue.process'
SPAN_ORIGIN = 'auto.queue.eventboss'

def call(work)
return yield unless ::Sentry.initialized?

::Sentry.clone_hub_to_current_thread
scope = ::Sentry.get_current_scope
scope.clear
if (user = extract_sentry_user(work))
scope.set_user(user)
end
scope.set_tags(queue: extract_queue_name(work), message_id: work.message.message_id)
scope.set_transaction_name(extract_transaction_name(work), source: :task)
transaction = start_transaction(scope, work)

if transaction
scope.set_span(transaction)
transaction.set_data(::Sentry::Span::DataConventions::MESSAGING_MESSAGE_ID, work.message.message_id)
transaction.set_data(::Sentry::Span::DataConventions::MESSAGING_DESTINATION_NAME, extract_queue_name(work))

if (latency = extract_latency(work.message))
transaction.set_data(::Sentry::Span::DataConventions::MESSAGING_MESSAGE_RECEIVE_LATENCY, latency)
end

if (retry_count = extract_receive_count(work.message))
transaction.set_data(::Sentry::Span::DataConventions::MESSAGING_MESSAGE_RETRY_COUNT, retry_count)
end
end

begin
yield
rescue StandardError
finish_transaction(transaction, 500)
raise
end

finish_transaction(transaction, 200)
end

def start_transaction(scope, work)
options = {
name: scope.transaction_name,
source: scope.transaction_source,
op: OP_NAME,
origin: SPAN_ORIGIN
}

env = {
'sentry-trace' => work.message.message_attributes['sentry-trace']&.string_value,
'baggage' => work.message.message_attributes['baggage']&.string_value
}

transaction = ::Sentry.continue_trace(env, **options)
::Sentry.start_transaction(transaction: transaction, **options)
end

def finish_transaction(transaction, status)
return unless transaction

transaction.set_http_status(status)
transaction.finish
end

def extract_sentry_user(work)
if (value = work.message.message_attributes["sentry_user"]&.string_value)
JSON.parse(value)
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

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

JSON parsing can raise JSON::ParserError for malformed data. Consider wrapping this in a rescue block to handle invalid JSON gracefully and prevent crashes during message processing.

Suggested change
JSON.parse(value)
begin
JSON.parse(value)
rescue JSON::ParserError
nil
end

Copilot uses AI. Check for mistakes.
end
end

def extract_transaction_name(work)
"Eventboss/#{work.listener.to_s}"
end

def extract_queue_name(work)
::Eventboss::Sentry::Context.queue_name_for_sentry(work.queue.name)
end

def extract_latency(message)
if sent_timestamp = message.attributes.fetch('SentTimestamp', nil)
Time.now - Time.at(sent_timestamp.to_i / 1000.0)
end
end

def extract_receive_count(message)
if receive_count = message.attributes.fetch('ApproximateReceiveCount', nil)
receive_count.to_i - 1
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/eventboss/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Eventboss
VERSION = "1.9.5"
VERSION = "1.9.6"
end
4 changes: 3 additions & 1 deletion spec/eventboss/long_poller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@

it 'calls client with proper attributes' do
expect(client).to receive(:receive_message)
.with(queue_url: 'url', max_number_of_messages: 10, wait_time_seconds: 10)
.with(queue_url: 'url', max_number_of_messages: 10, wait_time_seconds: 10,
attribute_names: ["SentTimestamp", "ApproximateReceiveCount"],
message_attribute_names: ["sentry-trace", "baggage", "sentry_user"])

subject.fetch_and_dispatch
end
Expand Down
6 changes: 4 additions & 2 deletions spec/eventboss/publisher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
it 'publishes to sns with source app name by default' do
expect(sns_client).to receive(:publish).with(
topic_arn: "arn:aws:sns:::eventboss-app_name1-event_name-ping",
message: "{}"
message: "{}",
message_attributes: {}
)
expect(subject).to eq(sns_response)
end
Expand All @@ -53,7 +54,8 @@
it 'publishes to sns without app name' do
expect(sns_client).to receive(:publish).with(
topic_arn: "arn:aws:sns:::eventboss-event_name-ping",
message: "{}"
message: "{}",
message_attributes: {}
)
subject
end
Expand Down