Skip to content

Commit 8726ed0

Browse files
committed
[rails] introduce abstract log subscriber
1 parent 84dc861 commit 8726ed0

File tree

5 files changed

+556
-0
lines changed

5 files changed

+556
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
require "active_support/log_subscriber"
4+
5+
module Sentry
6+
module Rails
7+
# Base class for Sentry log subscribers that extends ActiveSupport::LogSubscriber
8+
# to provide structured logging capabilities for Rails components.
9+
#
10+
# This class follows Rails' LogSubscriber pattern and provides common functionality
11+
# for capturing Rails instrumentation events and logging them through Sentry's
12+
# structured logging system.
13+
#
14+
# @example Creating a custom log subscriber
15+
# class MySubscriber < Sentry::Rails::LogSubscriber
16+
# attach_to :my_component
17+
#
18+
# def my_event(event)
19+
# log_structured_event(
20+
# message: "My event occurred",
21+
# level: :info,
22+
# attributes: {
23+
# duration_ms: event.duration,
24+
# custom_data: event.payload[:custom_data]
25+
# }
26+
# )
27+
# end
28+
# end
29+
class LogSubscriber < ActiveSupport::LogSubscriber
30+
class << self
31+
if ::Rails.version.to_f < 6.0
32+
# Rails 5.x does not provide detach_from
33+
def detach_from(namespace, notifications = ActiveSupport::Notifications)
34+
listeners = public_instance_methods(false)
35+
.flat_map { |key|
36+
notifications.notifier.listeners_for("#{key}.#{namespace}")
37+
}
38+
.select { |listener| listener.instance_variable_get(:@delegate).is_a?(self) }
39+
40+
listeners.map do |listener|
41+
notifications.notifier.unsubscribe(listener)
42+
end
43+
end
44+
end
45+
end
46+
47+
protected
48+
49+
# Log a structured event using Sentry's structured logger
50+
#
51+
# @param message [String] The log message
52+
# @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal)
53+
# @param attributes [Hash] Additional structured attributes to include
54+
def log_structured_event(message:, level: :info, attributes: {})
55+
Sentry.logger.public_send(level, message, **attributes)
56+
rescue => e
57+
# Silently handle any errors in logging to avoid breaking the application
58+
Sentry.configuration.sdk_logger.debug("Failed to log structured event: #{e.message}")
59+
end
60+
61+
# Calculate duration in milliseconds from an event
62+
#
63+
# @param event [ActiveSupport::Notifications::Event] The event
64+
# @return [Float] Duration in milliseconds
65+
def duration_ms(event)
66+
event.duration.round(2)
67+
end
68+
end
69+
end
70+
end
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
module Sentry
4+
module Rails
5+
module LogSubscribers
6+
# Shared utility module for filtering sensitive parameters in log subscribers.
7+
#
8+
# This module provides consistent parameter filtering across all Sentry Rails
9+
# log subscribers, leveraging Rails' built-in parameter filtering when available.
10+
# It automatically detects the correct Rails parameter filtering API based on
11+
# the Rails version and includes the appropriate implementation module.
12+
#
13+
# @example Usage in a log subscriber
14+
# class MySubscriber < Sentry::Rails::LogSubscriber
15+
# include Sentry::Rails::LogSubscribers::ParameterFilter
16+
#
17+
# def my_event(event)
18+
# if Sentry.configuration.send_default_pii && event.payload[:params]
19+
# filtered_params = filter_sensitive_params(event.payload[:params])
20+
# attributes[:params] = filtered_params unless filtered_params.empty?
21+
# end
22+
# end
23+
# end
24+
module ParameterFilter
25+
EMPTY_HASH = {}.freeze
26+
27+
def self.included(base)
28+
# Determine which parameter filter implementation to use based on Rails version
29+
# Try to require ActiveSupport::ParameterFilter first (Rails 6.0+)
30+
begin
31+
require "active_support/parameter_filter"
32+
base.include(ActiveSupportImplementation)
33+
rescue LoadError
34+
# Fall back to ActionDispatch::Http::ParameterFilter (Rails 5.0-5.2)
35+
base.include(ActionDispatchImplementation)
36+
end
37+
end
38+
39+
# Implementation for Rails 6.0+ using ActiveSupport::ParameterFilter
40+
module ActiveSupportImplementation
41+
# Filter sensitive parameters from a hash, respecting Rails configuration.
42+
#
43+
# @param params [Hash] The parameters to filter
44+
# @return [Hash] Filtered parameters with sensitive data removed
45+
def filter_sensitive_params(params)
46+
return EMPTY_HASH unless params.is_a?(Hash)
47+
48+
filter_parameters = ::Rails.application.config.filter_parameters
49+
parameter_filter = ActiveSupport::ParameterFilter.new(filter_parameters)
50+
51+
parameter_filter.filter(params)
52+
end
53+
end
54+
55+
# Implementation for Rails 5.0-5.2 using ActionDispatch::Http::ParameterFilter
56+
module ActionDispatchImplementation
57+
def self.included(base)
58+
require "action_dispatch"
59+
end
60+
61+
# Filter sensitive parameters from a hash, respecting Rails configuration.
62+
#
63+
# @param params [Hash] The parameters to filter
64+
# @return [Hash] Filtered parameters with sensitive data removed
65+
def filter_sensitive_params(params)
66+
return EMPTY_HASH unless params.is_a?(Hash)
67+
68+
filter_parameters = ::Rails.application.config.filter_parameters
69+
parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_parameters)
70+
71+
parameter_filter.filter(params)
72+
end
73+
end
74+
end
75+
end
76+
end
77+
end

sentry-rails/spec/dummy/test_rails_app/app.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ def self.name
7171

7272
configure_app(app)
7373

74+
# Configure parameter filtering for consistent test behavior
75+
app.config.filter_parameters.concat(
76+
[:custom_secret,
77+
:api_key,
78+
:credit_card,
79+
:authorization,
80+
:password,
81+
:token]
82+
)
83+
app.config.filter_parameters.uniq!
84+
7485
app.routes.append do
7586
get "/exception", to: "hello#exception"
7687
get "/view_exception", to: "hello#view_exception"

0 commit comments

Comments
 (0)