Skip to content

Commit d7e83cf

Browse files
committed
WIP - add ActionControllerSubscriber
1 parent 5cf39bf commit d7e83cf

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# frozen_string_literal: true
2+
3+
require "sentry/rails/log_subscriber"
4+
5+
module Sentry
6+
module Rails
7+
module LogSubscribers
8+
# LogSubscriber for ActionController events that captures HTTP request processing
9+
# and logs them using Sentry's structured logging system.
10+
#
11+
# This subscriber captures process_action.action_controller events and formats them
12+
# with relevant request information including controller, action, HTTP status,
13+
# request parameters, and performance metrics.
14+
#
15+
# @example Usage
16+
# # Enable structured logging for ActionController
17+
# Sentry.init do |config|
18+
# config.enable_logs = true
19+
# config.rails.structured_logging = true
20+
# config.rails.structured_logging.attach_to = [:action_controller]
21+
# end
22+
class ActionControllerSubscriber < Sentry::Rails::LogSubscriber
23+
# Handle process_action.action_controller events
24+
#
25+
# @param event [ActiveSupport::Notifications::Event] The controller action event
26+
def process_action(event)
27+
return if excluded_event?(event)
28+
29+
payload = event.payload
30+
controller = payload[:controller]
31+
action = payload[:action]
32+
status = payload[:status]
33+
duration = duration_ms(event)
34+
35+
# Prepare structured attributes
36+
attributes = {
37+
controller: controller,
38+
action: action,
39+
status: status,
40+
duration_ms: duration,
41+
method: payload[:method],
42+
path: payload[:path],
43+
format: payload[:format]
44+
}
45+
46+
# Add view and database timing if available
47+
attributes[:view_runtime_ms] = payload[:view_runtime]&.round(2) if payload[:view_runtime]
48+
attributes[:db_runtime_ms] = payload[:db_runtime]&.round(2) if payload[:db_runtime]
49+
50+
# Add request parameters if configured to send PII
51+
if Sentry.configuration.send_default_pii && payload[:params]
52+
# Filter out sensitive parameters
53+
filtered_params = filter_sensitive_params(payload[:params])
54+
attributes[:params] = filtered_params unless filtered_params.empty?
55+
end
56+
57+
# Determine log level based on status code and duration
58+
level = level_for_request(status, duration)
59+
message = "#{controller}##{action}"
60+
61+
# Log the structured event
62+
log_structured_event(
63+
message: message,
64+
level: level,
65+
attributes: attributes
66+
)
67+
end
68+
69+
private
70+
71+
# Determine log level based on HTTP status and duration
72+
#
73+
# @param status [Integer] HTTP status code
74+
# @param duration_ms [Float] Request duration in milliseconds
75+
# @return [Symbol] Log level
76+
def level_for_request(status, duration_ms)
77+
# Error status codes get warn/error level
78+
return :error if status >= 500
79+
return :warn if status >= 400
80+
81+
# Slow requests get warn level
82+
return :warn if duration_ms > 5000 # 5 seconds
83+
84+
:info
85+
end
86+
87+
# Filter sensitive parameters from request params
88+
#
89+
# @param params [Hash] Request parameters
90+
# @return [Hash] Filtered parameters
91+
def filter_sensitive_params(params)
92+
return {} unless params.is_a?(Hash)
93+
94+
# Common sensitive parameter names to exclude
95+
sensitive_keys = %w[
96+
password password_confirmation
97+
secret token api_key
98+
credit_card ssn social_security_number
99+
authorization auth
100+
]
101+
102+
params.reject do |key, _value|
103+
key_str = key.to_s.downcase
104+
sensitive_keys.any? { |sensitive| key_str.include?(sensitive) }
105+
end
106+
end
107+
end
108+
end
109+
end
110+
end
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
require "sentry/rails/log_subscribers/action_controller_subscriber"
5+
6+
RSpec.describe Sentry::Rails::LogSubscribers::ActionControllerSubscriber, type: :request do
7+
before do
8+
make_basic_app do |config|
9+
config.enable_logs = true
10+
config.rails.structured_logging.enabled = true
11+
config.rails.structured_logging.attach_to = [:action_controller]
12+
end
13+
end
14+
15+
describe "integration with ActiveSupport::Notifications" do
16+
it "logs controller action events when requests are processed" do
17+
sentry_transport.events.clear
18+
sentry_transport.envelopes.clear
19+
20+
get "/world"
21+
22+
Sentry.get_current_client.log_event_buffer.flush
23+
24+
expect(sentry_logs).not_to be_empty
25+
26+
log_event = sentry_logs.find { |log| log[:body] == "HelloController#world" }
27+
expect(log_event).not_to be_nil
28+
expect(log_event[:level]).to eq("info")
29+
expect(log_event[:attributes][:controller][:value]).to eq("HelloController")
30+
expect(log_event[:attributes][:action][:value]).to eq("world")
31+
expect(log_event[:attributes][:status][:value]).to eq(200)
32+
expect(log_event[:attributes][:duration_ms][:value]).to be > 0
33+
expect(log_event[:attributes][:method][:value]).to eq("GET")
34+
expect(log_event[:attributes][:path][:value]).to eq("/world")
35+
expect(log_event[:attributes][:format][:value]).to eq(:html)
36+
end
37+
38+
it "logs different status codes appropriately" do
39+
sentry_transport.events.clear
40+
sentry_transport.envelopes.clear
41+
42+
get "/not_found"
43+
44+
Sentry.get_current_client.log_event_buffer.flush
45+
46+
expect(sentry_logs).not_to be_empty
47+
48+
log_event = sentry_logs.find { |log| log[:body] == "HelloController#not_found" }
49+
expect(log_event).not_to be_nil
50+
expect(log_event[:level]).to eq("warn")
51+
expect(log_event[:attributes][:status][:value]).to eq(400)
52+
end
53+
54+
it "logs error status codes with error level" do
55+
sentry_transport.events.clear
56+
sentry_transport.envelopes.clear
57+
58+
get "/exception"
59+
60+
Sentry.get_current_client.log_event_buffer.flush
61+
62+
expect(sentry_logs).not_to be_empty
63+
64+
log_event = sentry_logs.find { |log| log[:body] == "HelloController#exception" }
65+
expect(log_event).not_to be_nil
66+
expect(log_event[:level]).to eq("error")
67+
expect(log_event[:attributes][:status][:value]).to eq(500)
68+
end
69+
70+
it "includes view runtime when available" do
71+
sentry_transport.events.clear
72+
sentry_transport.envelopes.clear
73+
74+
get "/view"
75+
76+
Sentry.get_current_client.log_event_buffer.flush
77+
78+
expect(sentry_logs).not_to be_empty
79+
80+
log_event = sentry_logs.find { |log| log[:body] == "HelloController#view" }
81+
expect(log_event).not_to be_nil
82+
expect(log_event[:attributes][:view_runtime_ms]).to be_present
83+
expect(log_event[:attributes][:view_runtime_ms][:value]).to be >= 0
84+
end
85+
86+
it "includes database runtime when available" do
87+
sentry_transport.events.clear
88+
sentry_transport.envelopes.clear
89+
90+
Post.create!
91+
get "/posts"
92+
93+
Sentry.get_current_client.log_event_buffer.flush
94+
95+
expect(sentry_logs).not_to be_empty
96+
97+
log_event = sentry_logs.find { |log| log[:body] == "PostsController#index" }
98+
expect(log_event).not_to be_nil
99+
expect(log_event[:attributes][:db_runtime_ms]).to be_present
100+
expect(log_event[:attributes][:db_runtime_ms][:value]).to be >= 0
101+
end
102+
103+
context "when send_default_pii is enabled" do
104+
before do
105+
Sentry.configuration.send_default_pii = true
106+
end
107+
108+
after do
109+
Sentry.configuration.send_default_pii = false
110+
end
111+
112+
it "includes filtered request parameters" do
113+
sentry_transport.events.clear
114+
sentry_transport.envelopes.clear
115+
116+
get "/world", params: { safe_param: "value", password: "secret" }
117+
118+
Sentry.get_current_client.log_event_buffer.flush
119+
120+
expect(sentry_logs).not_to be_empty
121+
122+
log_event = sentry_logs.find { |log| log[:body] == "HelloController#world" }
123+
expect(log_event).not_to be_nil
124+
expect(log_event[:attributes][:params]).to be_present
125+
expect(log_event[:attributes][:params][:value]).to include("safe_param" => "value")
126+
expect(log_event[:attributes][:params][:value]).not_to include("password")
127+
end
128+
129+
it "filters sensitive parameter names" do
130+
sentry_transport.events.clear
131+
sentry_transport.envelopes.clear
132+
133+
get "/world", params: {
134+
normal_param: "value",
135+
password: "secret",
136+
api_key: "key123",
137+
credit_card: "1234567890",
138+
authorization: "Bearer token"
139+
}
140+
141+
Sentry.get_current_client.log_event_buffer.flush
142+
143+
expect(sentry_logs).not_to be_empty
144+
145+
log_event = sentry_logs.find { |log| log[:body] == "HelloController#world" }
146+
expect(log_event).not_to be_nil
147+
148+
params = log_event[:attributes][:params][:value]
149+
expect(params).to include("normal_param" => "value")
150+
expect(params).not_to have_key("password")
151+
expect(params).not_to have_key("api_key")
152+
expect(params).not_to have_key("credit_card")
153+
expect(params).not_to have_key("authorization")
154+
end
155+
end
156+
157+
context "when send_default_pii is disabled" do
158+
it "does not include request parameters" do
159+
sentry_transport.events.clear
160+
sentry_transport.envelopes.clear
161+
162+
get "/world", params: { param: "value" }
163+
164+
Sentry.get_current_client.log_event_buffer.flush
165+
166+
expect(sentry_logs).not_to be_empty
167+
168+
log_event = sentry_logs.find { |log| log[:body] == "HelloController#world" }
169+
expect(log_event).not_to be_nil
170+
expect(log_event[:attributes]).not_to have_key(:params)
171+
end
172+
end
173+
end
174+
175+
describe "when logging is disabled" do
176+
before do
177+
make_basic_app do |config|
178+
config.enable_logs = false
179+
config.rails.structured_logging.enabled = true
180+
config.rails.structured_logging.attach_to = [:action_controller]
181+
end
182+
end
183+
184+
it "does not log events when logging is disabled" do
185+
initial_log_count = sentry_logs.count
186+
187+
get "/world"
188+
189+
if Sentry.get_current_client&.log_event_buffer
190+
Sentry.get_current_client.log_event_buffer.flush
191+
end
192+
193+
expect(sentry_logs.count).to eq(initial_log_count)
194+
end
195+
end
196+
end

0 commit comments

Comments
 (0)