Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 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 defining custom Rails log subscribers that work with Sentry Structured Logging ([#2689](https://github.com/getsentry/sentry-ruby/pull/2689))

### Internal

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
52 changes: 52 additions & 0 deletions sentry-rails/lib/sentry/rails/log_subscribers/parameter_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Sentry
module Rails
module LogSubscribers
# Shared utility module for filtering sensitive parameters in log subscribers.
#
# This module provides consistent parameter filtering across all Sentry Rails
# log subscribers, leveraging Rails' built-in parameter filtering when available.
# It automatically detects the correct Rails parameter filtering API based on
# the Rails version and includes the appropriate implementation module.
#
# @example Usage in a log subscriber
# class MySubscriber < Sentry::Rails::LogSubscriber
# include Sentry::Rails::LogSubscribers::ParameterFilter
#
# def my_event(event)
# if Sentry.configuration.send_default_pii && event.payload[:params]
# filtered_params = filter_sensitive_params(event.payload[:params])
# attributes[:params] = filtered_params unless filtered_params.empty?
# end
# end
# end
module ParameterFilter
EMPTY_HASH = {}.freeze

if ::Rails.version.to_f >= 6.0
def self.backend
ActiveSupport::ParameterFilter
end
else
def self.backend
ActionDispatch::Http::ParameterFilter
end
end

# Filter sensitive parameters from a hash, respecting Rails configuration.
#
# @param params [Hash] The parameters to filter
# @return [Hash] Filtered parameters with sensitive data removed
def filter_sensitive_params(params)
return EMPTY_HASH unless params.is_a?(Hash)

filter_parameters = ::Rails.application.config.filter_parameters
parameter_filter = ParameterFilter.backend.new(filter_parameters)

parameter_filter.filter(params)
end
end
end
end
end
11 changes: 11 additions & 0 deletions sentry-rails/spec/dummy/test_rails_app/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ def self.name

configure_app(app)

# Configure parameter filtering for consistent test behavior
app.config.filter_parameters.concat(
[:custom_secret,
:api_key,
:credit_card,
:authorization,
:password,
:token]
)
app.config.filter_parameters.uniq!

app.routes.append do
get "/exception", to: "hello#exception"
get "/view_exception", to: "hello#view_exception"
Expand Down
Loading
Loading