diff --git a/CHANGELOG.md b/CHANGELOG.md index 41dddc4c3..182f81030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Sentry.metrics.count("button.click", 1, attributes: { button_id: "submit" }) Sentry.metrics.distribution("response.time", 120.5, unit: "millisecond") Sentry.metrics.gauge("cpu.usage", 75.2, unit: "percent") - ``` + ``` - Support for tracing `Sequel` queries ([#2814](https://github.com/getsentry/sentry-ruby/pull/2814)) @@ -33,6 +33,19 @@ - Add support for OpenTelemetry messaging/queue system spans ([#2685](https://github.com/getsentry/sentry-ruby/pull/2685)) +- Add support for `config.std_lib_logger_filter` proc ([#2829](https://github.com/getsentry/sentry-ruby/pull/2829)) + + ```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 + ``` + ### Bug Fixes - Handle empty frames case gracefully with local vars ([#2807](https://github.com/getsentry/sentry-ruby/pull/2807)) diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 6c8788fc9..11ad5fb44 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -355,6 +355,15 @@ class Configuration # @return [Proc, nil] attr_reader :before_send_metric + # Optional Proc, called to filter log messages before sending to Sentry + # @example + # config.std_lib_logger_filter = lambda do |logger, message, level| + # # Only send error and fatal logs to Sentry + # [:error, :fatal].include?(level) + # end + # @return [Proc, nil] + attr_reader :std_lib_logger_filter + # these are not config options # @!visibility private attr_reader :errors, :gem_specs @@ -518,6 +527,7 @@ def initialize self.before_send_check_in = nil self.before_send_log = nil self.before_send_metric = nil + self.std_lib_logger_filter = nil self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT self.traces_sampler = nil self.enable_logs = false @@ -614,6 +624,12 @@ def before_breadcrumb=(value) @before_breadcrumb = value end + def std_lib_logger_filter=(value) + check_callable!("std_lib_logger_filter", value) + + @std_lib_logger_filter = value + end + def environment=(environment) @environment = environment.to_s end diff --git a/sentry-ruby/lib/sentry/std_lib_logger.rb b/sentry-ruby/lib/sentry/std_lib_logger.rb index fcae1f49f..f51bd74d4 100644 --- a/sentry-ruby/lib/sentry/std_lib_logger.rb +++ b/sentry-ruby/lib/sentry/std_lib_logger.rb @@ -37,6 +37,10 @@ def add(severity, message = nil, progname = nil, &block) message = message.to_s.strip if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity] + if (filter = Sentry.configuration.std_lib_logger_filter) && !filter.call(self, message, method) + return result + end + Sentry.logger.send(method, message, origin: ORIGIN) end end diff --git a/sentry-ruby/spec/isolated/std_lib_logger_spec.rb b/sentry-ruby/spec/isolated/std_lib_logger_spec.rb index 21674a8d7..e6a0eb68f 100644 --- a/sentry-ruby/spec/isolated/std_lib_logger_spec.rb +++ b/sentry-ruby/spec/isolated/std_lib_logger_spec.rb @@ -131,5 +131,115 @@ expect(log_event[:body]).to eql("Fatal message") end end + + context "with std_lib_logger_filter" do + let(:null_logger) { ::Logger.new(IO::NULL) } + + context "when no filter is configured" do + it "sends all log messages to Sentry" do + logger.info("Test message") + + expect(sentry_logs).to_not be_empty + expect(sentry_logs.last[:body]).to eq("Test message") + end + end + + context "when filter always returns true" do + before do + Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) { true } + end + + it "sends log messages to Sentry" do + logger.info("Test message") + + expect(sentry_logs).to_not be_empty + expect(sentry_logs.last[:body]).to eq("Test message") + end + end + + context "when filter always returns false" do + before do + Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) { false } + end + + it "blocks messages from Sentry but still logs locally" do + expect { + logger.info("Test message") + }.to output(/Test message/).to_stdout + + expect(sentry_logs).to be_empty + end + end + + context "when filter uses logger instance for decisions" do + before do + Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) do + !logger.instance_variable_get(:@logdev).nil? + end + end + + it "allows logs from regular loggers" do + logger.info("Regular log message") + + expect(sentry_logs).to_not be_empty + expect(sentry_logs.last[:body]).to eq("Regular log message") + end + + it "blocks logs from IO::NULL loggers" do + null_logger.error("Null log message") + + expect(sentry_logs).to be_empty + end + end + + context "when filter uses message content for decisions" do + before do + Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) do + !message.to_s.include?("SKIP") + end + end + + it "allows messages without SKIP keyword" do + logger.info("Regular info message") + + expect(sentry_logs).to_not be_empty + expect(sentry_logs.last[:body]).to eq("Regular info message") + end + + it "blocks messages containing SKIP keyword" do + expect { + logger.info("SKIP: this should be filtered") + }.to output(/SKIP: this should be filtered/).to_stdout + + expect(sentry_logs).to be_empty + end + end + + context "when filter uses log level for decisions" do + before do + Sentry.configuration.std_lib_logger_filter = ->(logger, message, level) do + [:error, :fatal].include?(level) + end + end + + it "allows error and fatal logs" do + logger.error("Error message") + logger.fatal("Fatal message") + + expect(sentry_logs.size).to eq(2) + expect(sentry_logs[0][:body]).to eq("Error message") + expect(sentry_logs[1][:body]).to eq("Fatal message") + end + + it "blocks info and warn logs" do + expect { + logger.info("Info message") + logger.warn("Warn message") + }.to output(/Info message.*Warn message/m).to_stdout + + expect(sentry_logs).to be_empty + end + end + end end end diff --git a/sentry-ruby/spec/sentry/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb index df41d76d0..b565225a6 100644 --- a/sentry-ruby/spec/sentry/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/configuration_spec.rb @@ -805,4 +805,48 @@ class SentryConfigurationSample < Sentry::Configuration expect { subject.before_send_metric = true }.to raise_error(ArgumentError, "before_send_metric must be callable (or nil to disable)") end end + + describe "#std_lib_logger_filter" do + it "defaults to nil" do + expect(subject.std_lib_logger_filter).to be_nil + end + + it "accepts nil" do + expect { + subject.std_lib_logger_filter = nil + }.not_to raise_error + + expect(subject.std_lib_logger_filter).to be_nil + end + + it "accepts a proc" do + filter_proc = ->(logger, message, level) { true } + + expect { + subject.std_lib_logger_filter = filter_proc + }.not_to raise_error + + expect(subject.std_lib_logger_filter).to eq(filter_proc) + end + + it "accepts a callable object" do + callable_object = Class.new do + def call(logger, message, level) + false + end + end.new + + expect { + subject.std_lib_logger_filter = callable_object + }.not_to raise_error + + expect(subject.std_lib_logger_filter).to eq(callable_object) + end + + it "does not accept non-callable objects" do + expect { + subject.std_lib_logger_filter = "not a callable" + }.to raise_error(ArgumentError, "std_lib_logger_filter must be callable (or nil to disable)") + end + end end