Skip to content
Open
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
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,73 @@
### Feature

- Propagated sampling rates as specified in [Traces](https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value) docs ([#2671](https://github.com/getsentry/sentry-ruby/pull/2671))
- Support for Rails ActiveSupport log subscribers ([#2690](https://github.com/getsentry/sentry-ruby/pull/2690))
- Support for defining custom Rails log subscribers that work with Sentry Structured Logging ([#2689](https://github.com/getsentry/sentry-ruby/pull/2689))

Rails applications can now define custom log subscribers that integrate with Sentry's structured logging system. The feature includes built-in subscribers for ActionController, ActiveRecord, ActiveJob, and ActionMailer events, with automatic parameter filtering that respects Rails' `config.filter_parameters` configuration.

To enable structured logging with Rails log subscribers:

```ruby
Sentry.init do |config|
# ... your setup ...

# Make sure structured logging is enabled
config.enable_logs = true

# Enable default Rails log subscribers (ActionController and ActiveRecord)
config.rails.structured_logging.enabled = true
end
```

To configure all subscribers:

```ruby
Sentry.init do |config|
# ... your setup ...

# Make sure structured logging is enabled
config.enable_logs = true

# Enable Rails log subscribers
config.rails.structured_logging.enabled = true

# Add ActionMailer and ActiveJob subscribers
config.rails.structured_logging.subscribers.update(
action_mailer: Sentry::Rails::LogSubscribers::ActionMailerSubscriber,
active_job: Sentry::Rails::LogSubscribers::ActiveJobSubscriber
)
end
```

You can also define custom log subscribers by extending the base class:

```ruby
class MyCustomSubscriber < Sentry::Rails::LogSubscriber
attach_to :my_component

def my_event(event)
log_structured_event(
message: "Custom event occurred",
level: :info,
attributes: { duration_ms: event.duration }
)
end
end

Sentry.init do |config|
# ... your setup ...

# Make sure structured logging is enabled
config.enable_logs = true

# Enable Rails log subscribers
config.rails.structured_logging.enabled = true

# Add custom subscriber
config.rails.structured_logging.subscribers[:my_component] = MyCustomSubscriber
end
```

### Internal

Expand Down
1 change: 1 addition & 0 deletions sentry-rails/lib/sentry/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require "sentry/integrable"
require "sentry/rails/tracing"
require "sentry/rails/configuration"
require "sentry/rails/structured_logging"
require "sentry/rails/engine"
require "sentry/rails/railtie"

Expand Down
28 changes: 28 additions & 0 deletions sentry-rails/lib/sentry/rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
require "sentry/rails/tracing/active_storage_subscriber"
require "sentry/rails/tracing/active_support_subscriber"

require "sentry/rails/log_subscribers/active_record_subscriber"
require "sentry/rails/log_subscribers/action_controller_subscriber"

module Sentry
class Configuration
attr_reader :rails
Expand Down Expand Up @@ -159,6 +162,10 @@ class Configuration
# Set this option to true if you want Sentry to capture each retry failure
attr_accessor :active_job_report_on_retry_error

# Configuration for structured logging feature
# @return [StructuredLoggingConfiguration]
attr_reader :structured_logging

def initialize
@register_error_subscriber = false
@report_rescued_exceptions = true
Expand All @@ -176,6 +183,27 @@ def initialize
@db_query_source_threshold_ms = 100
@active_support_logger_subscription_items = Sentry::Rails::ACTIVE_SUPPORT_LOGGER_SUBSCRIPTION_ITEMS_DEFAULT.dup
@active_job_report_on_retry_error = false
@structured_logging = StructuredLoggingConfiguration.new
end
end

class StructuredLoggingConfiguration
# Enable or disable structured logging
# @return [Boolean]
attr_accessor :enabled

# Hash of components to subscriber classes for structured logging
# @return [Hash<Symbol, Class>]
attr_accessor :subscribers

DEFAULT_SUBSCRIBERS = {
active_record: Sentry::Rails::LogSubscribers::ActiveRecordSubscriber,
action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber
}.freeze

def initialize
@enabled = false
@subscribers = DEFAULT_SUBSCRIBERS.dup
end
end
end
Expand Down
70 changes: 70 additions & 0 deletions sentry-rails/lib/sentry/rails/log_subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

require "active_support/log_subscriber"

module Sentry
module Rails
# Base class for Sentry log subscribers that extends ActiveSupport::LogSubscriber
# to provide structured logging capabilities for Rails components.
#
# This class follows Rails' LogSubscriber pattern and provides common functionality
# for capturing Rails instrumentation events and logging them through Sentry's
# structured logging system.
#
# @example Creating a custom log subscriber
# class MySubscriber < Sentry::Rails::LogSubscriber
# attach_to :my_component
#
# def my_event(event)
# log_structured_event(
# message: "My event occurred",
# level: :info,
# attributes: {
# duration_ms: event.duration,
# custom_data: event.payload[:custom_data]
# }
# )
# end
# end
class LogSubscriber < ActiveSupport::LogSubscriber
class << self
if ::Rails.version.to_f < 6.0
# Rails 5.x does not provide detach_from
def detach_from(namespace, notifications = ActiveSupport::Notifications)
listeners = public_instance_methods(false)
.flat_map { |key|
notifications.notifier.listeners_for("#{key}.#{namespace}")
}
.select { |listener| listener.instance_variable_get(:@delegate).is_a?(self) }

listeners.map do |listener|
notifications.notifier.unsubscribe(listener)
end
end
end
end

protected

# Log a structured event using Sentry's structured logger
#
# @param message [String] The log message
# @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal)
# @param attributes [Hash] Additional structured attributes to include
def log_structured_event(message:, level: :info, attributes: {})
Sentry.logger.public_send(level, message, **attributes)
rescue => e
# Silently handle any errors in logging to avoid breaking the application
Sentry.configuration.sdk_logger.debug("Failed to log structured event: #{e.message}")
end

# Calculate duration in milliseconds from an event
#
# @param event [ActiveSupport::Notifications::Event] The event
# @return [Float] Duration in milliseconds
def duration_ms(event)
event.duration.round(2)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# frozen_string_literal: true

require "sentry/rails/log_subscriber"
require "sentry/rails/log_subscribers/parameter_filter"

module Sentry
module Rails
module LogSubscribers
# LogSubscriber for ActionController events that captures HTTP request processing
# and logs them using Sentry's structured logging system.
#
# This subscriber captures process_action.action_controller events and formats them
# with relevant request information including controller, action, HTTP status,
# request parameters, and performance metrics.
#
# @example Usage
# # Enable structured logging for ActionController
# Sentry.init do |config|
# config.enable_logs = true
# config.rails.structured_logging = true
# config.rails.structured_logging.subscribers = { action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber }
# end
class ActionControllerSubscriber < Sentry::Rails::LogSubscriber
include ParameterFilter

# Handle process_action.action_controller events
#
# @param event [ActiveSupport::Notifications::Event] The controller action event
def process_action(event)
payload = event.payload

controller = payload[:controller]
action = payload[:action]

status = extract_status(payload)

attributes = {
controller: controller,
action: action,
status: status,
duration_ms: duration_ms(event),
method: payload[:method],
path: payload[:path],
format: payload[:format]
}

if payload[:view_runtime]
attributes[:view_runtime_ms] = payload[:view_runtime].round(2)
end

if payload[:db_runtime]
attributes[:db_runtime_ms] = payload[:db_runtime].round(2)
end

if Sentry.configuration.send_default_pii && payload[:params]
filtered_params = filter_sensitive_params(payload[:params])
attributes[:params] = filtered_params unless filtered_params.empty?
end

level = level_for_request(payload)
message = "#{controller}##{action}"

log_structured_event(
message: message,
level: level,
attributes: attributes
)
end

private

def extract_status(payload)
if payload[:status]
payload[:status]
elsif payload[:exception]
case payload[:exception].first
when "ActionController::RoutingError"
404
when "ActionController::BadRequest"
400
else
500
end
end
end

def level_for_request(payload)
status = payload[:status]

# In Rails < 6.0 status is not set when an action raised an exception
if status.nil? && payload[:exception]
case payload[:exception].first
when "ActionController::RoutingError"
:warn
when "ActionController::BadRequest"
:warn
else
:error
end
elsif status >= 200 && status < 400
:info
elsif status >= 400 && status < 500
:warn
elsif status >= 500
:error
else
:info
end
end
end
end
end
end
Loading
Loading