Skip to content

Commit 8b8bad5

Browse files
authored
feat(std_lib_logger): add support for filtering (#2829)
* feat(std_lib_logger): add support for filtering Introduce `config.std_lib_logger_filter` configuration option to allow users to define a custom filter for log messages sent to Sentry via the standard library logger integration. Example usage: ```ruby Sentry.init do |config| config.std_lib_logger_filter = proc do |logger, message, severity| # Only send ERROR and above messages severity == :error || severity == :fatal end config.enabled_patches = [:std_lib_logger] end ``` * Update CHANGELOG.md
1 parent 846c2ba commit 8b8bad5

File tree

5 files changed

+188
-1
lines changed

5 files changed

+188
-1
lines changed

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
Sentry.metrics.count("button.click", 1, attributes: { button_id: "submit" })
1616
Sentry.metrics.distribution("response.time", 120.5, unit: "millisecond")
1717
Sentry.metrics.gauge("cpu.usage", 75.2, unit: "percent")
18-
```
18+
```
1919

2020
- Support for tracing `Sequel` queries ([#2814](https://github.com/getsentry/sentry-ruby/pull/2814))
2121

@@ -33,6 +33,19 @@
3333

3434
- Add support for OpenTelemetry messaging/queue system spans ([#2685](https://github.com/getsentry/sentry-ruby/pull/2685))
3535

36+
- Add support for `config.std_lib_logger_filter` proc ([#2829](https://github.com/getsentry/sentry-ruby/pull/2829))
37+
38+
```ruby
39+
Sentry.init do |config|
40+
config.std_lib_logger_filter = proc do |logger, message, severity|
41+
# Only send ERROR and above messages
42+
severity == :error || severity == :fatal
43+
end
44+
45+
config.enabled_patches = [:std_lib_logger]
46+
end
47+
```
48+
3649
### Bug Fixes
3750

3851
- Handle empty frames case gracefully with local vars ([#2807](https://github.com/getsentry/sentry-ruby/pull/2807))

sentry-ruby/lib/sentry/configuration.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,15 @@ class Configuration
355355
# @return [Proc, nil]
356356
attr_reader :before_send_metric
357357

358+
# Optional Proc, called to filter log messages before sending to Sentry
359+
# @example
360+
# config.std_lib_logger_filter = lambda do |logger, message, level|
361+
# # Only send error and fatal logs to Sentry
362+
# [:error, :fatal].include?(level)
363+
# end
364+
# @return [Proc, nil]
365+
attr_reader :std_lib_logger_filter
366+
358367
# these are not config options
359368
# @!visibility private
360369
attr_reader :errors, :gem_specs
@@ -518,6 +527,7 @@ def initialize
518527
self.before_send_check_in = nil
519528
self.before_send_log = nil
520529
self.before_send_metric = nil
530+
self.std_lib_logger_filter = nil
521531
self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
522532
self.traces_sampler = nil
523533
self.enable_logs = false
@@ -614,6 +624,12 @@ def before_breadcrumb=(value)
614624
@before_breadcrumb = value
615625
end
616626

627+
def std_lib_logger_filter=(value)
628+
check_callable!("std_lib_logger_filter", value)
629+
630+
@std_lib_logger_filter = value
631+
end
632+
617633
def environment=(environment)
618634
@environment = environment.to_s
619635
end

sentry-ruby/lib/sentry/std_lib_logger.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ def add(severity, message = nil, progname = nil, &block)
3737
message = message.to_s.strip
3838

3939
if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity]
40+
if (filter = Sentry.configuration.std_lib_logger_filter) && !filter.call(self, message, method)
41+
return result
42+
end
43+
4044
Sentry.logger.send(method, message, origin: ORIGIN)
4145
end
4246
end

sentry-ruby/spec/isolated/std_lib_logger_spec.rb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,115 @@
131131
expect(log_event[:body]).to eql("Fatal message")
132132
end
133133
end
134+
135+
context "with std_lib_logger_filter" do
136+
let(:null_logger) { ::Logger.new(IO::NULL) }
137+
138+
context "when no filter is configured" do
139+
it "sends all log messages to Sentry" do
140+
logger.info("Test message")
141+
142+
expect(sentry_logs).to_not be_empty
143+
expect(sentry_logs.last[:body]).to eq("Test message")
144+
end
145+
end
146+
147+
context "when filter always returns true" do
148+
before do
149+
Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) { true }
150+
end
151+
152+
it "sends log messages to Sentry" do
153+
logger.info("Test message")
154+
155+
expect(sentry_logs).to_not be_empty
156+
expect(sentry_logs.last[:body]).to eq("Test message")
157+
end
158+
end
159+
160+
context "when filter always returns false" do
161+
before do
162+
Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) { false }
163+
end
164+
165+
it "blocks messages from Sentry but still logs locally" do
166+
expect {
167+
logger.info("Test message")
168+
}.to output(/Test message/).to_stdout
169+
170+
expect(sentry_logs).to be_empty
171+
end
172+
end
173+
174+
context "when filter uses logger instance for decisions" do
175+
before do
176+
Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) do
177+
!logger.instance_variable_get(:@logdev).nil?
178+
end
179+
end
180+
181+
it "allows logs from regular loggers" do
182+
logger.info("Regular log message")
183+
184+
expect(sentry_logs).to_not be_empty
185+
expect(sentry_logs.last[:body]).to eq("Regular log message")
186+
end
187+
188+
it "blocks logs from IO::NULL loggers" do
189+
null_logger.error("Null log message")
190+
191+
expect(sentry_logs).to be_empty
192+
end
193+
end
194+
195+
context "when filter uses message content for decisions" do
196+
before do
197+
Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) do
198+
!message.to_s.include?("SKIP")
199+
end
200+
end
201+
202+
it "allows messages without SKIP keyword" do
203+
logger.info("Regular info message")
204+
205+
expect(sentry_logs).to_not be_empty
206+
expect(sentry_logs.last[:body]).to eq("Regular info message")
207+
end
208+
209+
it "blocks messages containing SKIP keyword" do
210+
expect {
211+
logger.info("SKIP: this should be filtered")
212+
}.to output(/SKIP: this should be filtered/).to_stdout
213+
214+
expect(sentry_logs).to be_empty
215+
end
216+
end
217+
218+
context "when filter uses log level for decisions" do
219+
before do
220+
Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) do
221+
[:error, :fatal].include?(level)
222+
end
223+
end
224+
225+
it "allows error and fatal logs" do
226+
logger.error("Error message")
227+
logger.fatal("Fatal message")
228+
229+
expect(sentry_logs.size).to eq(2)
230+
expect(sentry_logs[0][:body]).to eq("Error message")
231+
expect(sentry_logs[1][:body]).to eq("Fatal message")
232+
end
233+
234+
it "blocks info and warn logs" do
235+
expect {
236+
logger.info("Info message")
237+
logger.warn("Warn message")
238+
}.to output(/Info message.*Warn message/m).to_stdout
239+
240+
expect(sentry_logs).to be_empty
241+
end
242+
end
243+
end
134244
end
135245
end

sentry-ruby/spec/sentry/configuration_spec.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,4 +805,48 @@ class SentryConfigurationSample < Sentry::Configuration
805805
expect { subject.before_send_metric = true }.to raise_error(ArgumentError, "before_send_metric must be callable (or nil to disable)")
806806
end
807807
end
808+
809+
describe "#std_lib_logger_filter" do
810+
it "defaults to nil" do
811+
expect(subject.std_lib_logger_filter).to be_nil
812+
end
813+
814+
it "accepts nil" do
815+
expect {
816+
subject.std_lib_logger_filter = nil
817+
}.not_to raise_error
818+
819+
expect(subject.std_lib_logger_filter).to be_nil
820+
end
821+
822+
it "accepts a proc" do
823+
filter_proc = ->(logger, message, level) { true }
824+
825+
expect {
826+
subject.std_lib_logger_filter = filter_proc
827+
}.not_to raise_error
828+
829+
expect(subject.std_lib_logger_filter).to eq(filter_proc)
830+
end
831+
832+
it "accepts a callable object" do
833+
callable_object = Class.new do
834+
def call(logger, message, level)
835+
false
836+
end
837+
end.new
838+
839+
expect {
840+
subject.std_lib_logger_filter = callable_object
841+
}.not_to raise_error
842+
843+
expect(subject.std_lib_logger_filter).to eq(callable_object)
844+
end
845+
846+
it "does not accept non-callable objects" do
847+
expect {
848+
subject.std_lib_logger_filter = "not a callable"
849+
}.to raise_error(ArgumentError, "std_lib_logger_filter must be callable (or nil to disable)")
850+
end
851+
end
808852
end

0 commit comments

Comments
 (0)