Skip to content

fix: Add block syntax support to StructuredLogger for compatibility#2911

Open
jeromepl wants to merge 1 commit intogetsentry:masterfrom
jeromepl:master
Open

fix: Add block syntax support to StructuredLogger for compatibility#2911
jeromepl wants to merge 1 commit intogetsentry:masterfrom
jeromepl:master

Conversation

@jeromepl
Copy link
Contributor

Description

Ruby's Logger supports passing the message as a block. For Sentry's structured logger to be used interchangeably with Ruby's logger, it should also support this syntax.

For example, I wanted to pass Sentry.logger in our Rails app configured with config.rails.structured_logging.subscribers = { ... } to Faraday's logging middleware but that did not work because that middleware uses the block syntax:
https://github.com/lostisland/faraday/blob/8b4d1fd06fd47dd33f3720794d4df38498c240ec/lib/faraday/logging/formatter.rb#L26:L28

Ruby's Logger supports passing the message as a block. For Sentry's structured logger
to be used interchangeably with Ruby's logger, it should also support this syntax.
Comment on lines 126 to 133
# @param attributes [Hash] Additional attributes to include with the log
#
# @return [LogEvent, nil] The created log event or nil if logging is disabled
def log(level, message, parameters:, **attributes)
def log(level, message = nil, parameters:, **attributes, &block)
message = yield if message.nil? && block_given?
case parameters
when Array then
Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: parameters, **attributes)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: DebugStructuredLogger methods were not updated to support block syntax. Calling them with a block will raise an ArgumentError because the message parameter is still required.
Severity: CRITICAL

Suggested Fix

Update the method definitions in DebugStructuredLogger to match StructuredLogger. Make the message parameter optional with a default nil value, add a &block parameter, and evaluate the block if no message is provided. This will ensure both logger classes behave consistently.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: sentry-ruby/lib/sentry/structured_logger.rb#L126-L133

Potential issue: The pull request adds block syntax support to `StructuredLogger`'s
logging methods, but the corresponding methods in `DebugStructuredLogger` were not
updated. The `DebugStructuredLogger` methods do not accept a block parameter (`&block`)
and still require a non-optional `message` argument. Consequently, if
`config.structured_logging.logger_class` is set to `Sentry::DebugStructuredLogger`, any
call to the logger using block syntax, such as `Sentry.logger.info { "message" }`, will
raise an `ArgumentError` and crash the application. This breaks the intended
interchangeability with Ruby's standard logger.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: DebugStructuredLogger not updated for block syntax support
    • Updated DebugStructuredLogger log-level methods and log to accept optional message with block forwarding so block syntax works and no longer raises arity errors.
  • ✅ Fixed: Nil message causes NoMethodError in LogEvent
    • Added an explicit ArgumentError guard in StructuredLogger.log when both message and block are absent to prevent nil from reaching LogEvent.

Create PR

Or push these changes by commenting:

@cursor push 811f47aaff
Preview (811f47aaff)
diff --git a/sentry-ruby/lib/sentry/debug_structured_logger.rb b/sentry-ruby/lib/sentry/debug_structured_logger.rb
--- a/sentry-ruby/lib/sentry/debug_structured_logger.rb
+++ b/sentry-ruby/lib/sentry/debug_structured_logger.rb
@@ -26,14 +26,20 @@
 
     # Override all log level methods to capture events
     %i[trace debug info warn error fatal].each do |level|
-      define_method(level) do |message, parameters = [], **attributes|
+      define_method(level) do |message = nil, parameters = [], **attributes, &block|
+        message = block.call if message.nil? && block
+        raise ArgumentError, "message or block is required" if message.nil?
+
         log_event = capture_log_event(level, message, parameters, **attributes)
         backend.public_send(level, message, parameters, **attributes)
         log_event
       end
     end
 
-    def log(level, message, parameters:, **attributes)
+    def log(level, message = nil, parameters:, **attributes, &block)
+      message = block.call if message.nil? && block
+      raise ArgumentError, "message or block is required" if message.nil?
+
       log_event = capture_log_event(level, message, parameters, **attributes)
       backend.log(level, message, parameters: parameters, **attributes)
       log_event

diff --git a/sentry-ruby/lib/sentry/structured_logger.rb b/sentry-ruby/lib/sentry/structured_logger.rb
--- a/sentry-ruby/lib/sentry/structured_logger.rb
+++ b/sentry-ruby/lib/sentry/structured_logger.rb
@@ -59,8 +59,8 @@
     # @param attributes [Hash] Additional attributes to include with the log
     #
     # @return [LogEvent, nil] The created log event or nil if logging is disabled
-    def trace(message, parameters = [], **attributes)
-      log(__method__, message, parameters: parameters, **attributes)
+    def trace(message = nil, parameters = [], **attributes, &block)
+      log(__method__, message, parameters: parameters, **attributes, &block)
     end
 
     # Logs a message at DEBUG level
@@ -70,8 +70,8 @@
     # @param attributes [Hash] Additional attributes to include with the log
     #
     # @return [LogEvent, nil] The created log event or nil if logging is disabled
-    def debug(message, parameters = [], **attributes)
-      log(__method__, message, parameters: parameters, **attributes)
+    def debug(message = nil, parameters = [], **attributes, &block)
+      log(__method__, message, parameters: parameters, **attributes, &block)
     end
 
     # Logs a message at INFO level
@@ -81,8 +81,8 @@
     # @param attributes [Hash] Additional attributes to include with the log
     #
     # @return [LogEvent, nil] The created log event or nil if logging is disabled
-    def info(message, parameters = [], **attributes)
-      log(__method__, message, parameters: parameters, **attributes)
+    def info(message = nil, parameters = [], **attributes, &block)
+      log(__method__, message, parameters: parameters, **attributes, &block)
     end
 
     # Logs a message at WARN level
@@ -92,8 +92,8 @@
     # @param attributes [Hash] Additional attributes to include with the log
     #
     # @return [LogEvent, nil] The created log event or nil if logging is disabled
-    def warn(message, parameters = [], **attributes)
-      log(__method__, message, parameters: parameters, **attributes)
+    def warn(message = nil, parameters = [], **attributes, &block)
+      log(__method__, message, parameters: parameters, **attributes, &block)
     end
 
     # Logs a message at ERROR level
@@ -103,8 +103,8 @@
     # @param attributes [Hash] Additional attributes to include with the log
     #
     # @return [LogEvent, nil] The created log event or nil if logging is disabled
-    def error(message, parameters = [], **attributes)
-      log(__method__, message, parameters: parameters, **attributes)
+    def error(message = nil, parameters = [], **attributes, &block)
+      log(__method__, message, parameters: parameters, **attributes, &block)
     end
 
     # Logs a message at FATAL level
@@ -114,8 +114,8 @@
     # @param attributes [Hash] Additional attributes to include with the log
     #
     # @return [LogEvent, nil] The created log event or nil if logging is disabled
-    def fatal(message, parameters = [], **attributes)
-      log(__method__, message, parameters: parameters, **attributes)
+    def fatal(message = nil, parameters = [], **attributes, &block)
+      log(__method__, message, parameters: parameters, **attributes, &block)
     end
 
     # Logs a message at the specified level
@@ -126,7 +126,10 @@
     # @param attributes [Hash] Additional attributes to include with the log
     #
     # @return [LogEvent, nil] The created log event or nil if logging is disabled
-    def log(level, message, parameters:, **attributes)
+    def log(level, message = nil, parameters:, **attributes, &block)
+      message = yield if message.nil? && block_given?
+      raise ArgumentError, "message or block is required" if message.nil?
+
       case parameters
       when Array then
         Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: parameters, **attributes)

diff --git a/sentry-ruby/spec/sentry/debug_structured_logger_spec.rb b/sentry-ruby/spec/sentry/debug_structured_logger_spec.rb
--- a/sentry-ruby/spec/sentry/debug_structured_logger_spec.rb
+++ b/sentry-ruby/spec/sentry/debug_structured_logger_spec.rb
@@ -66,6 +66,21 @@
           expect(log_event["parameters"]).to eq(["param1", "param2"])
           expect(log_event["attributes"]["extra_attr"]).to eq("extra")
         end
+
+        it "supports block syntax for compatibility with StructuredLogger" do
+          debug_logger.public_send(level) { "Test #{level} block message" }
+
+          logged_events = debug_logger.logged_events
+          expect(logged_events).not_to be_empty
+
+          log_event = logged_events.last
+          expect(log_event["message"]).to eq("Test #{level} block message")
+        end
+
+        it "raises an argument error when no message or block is given" do
+          expect { debug_logger.public_send(level) }
+            .to raise_error(ArgumentError, "message or block is required")
+        end
       end
     end
   end
@@ -82,6 +97,22 @@
       expect(log_event["message"]).to eq("Test log message")
       expect(log_event["attributes"]["custom_attr"]).to eq("custom_value")
     end
+
+    it "supports block syntax for compatibility with StructuredLogger" do
+      debug_logger.log(:info, parameters: []) { "Test log block message" }
+
+      logged_events = debug_logger.logged_events
+      expect(logged_events).not_to be_empty
+
+      log_event = logged_events.last
+      expect(log_event["level"]).to eq("info")
+      expect(log_event["message"]).to eq("Test log block message")
+    end
+
+    it "raises an argument error when no message or block is given" do
+      expect { debug_logger.log(:info, parameters: []) }
+        .to raise_error(ArgumentError, "message or block is required")
+    end
   end
 
   describe "#logged_events" do

diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb
--- a/sentry-ruby/spec/sentry/structured_logger_spec.rb
+++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb
@@ -135,6 +135,22 @@
           expect(log_event[:attributes]["sentry.message.parameter.day"]).to eql({ value: "Monday", type: "string" })
         end
 
+        it "logs with block syntax for compatibility with ruby Logger" do
+          Sentry.logger.public_send(level) { "Hello world" }
+
+          expect(sentry_logs).to_not be_empty
+
+          log_event = sentry_logs.last
+
+          expect(log_event[:level]).to eql(level)
+          expect(log_event[:body]).to eql("Hello world")
+        end
+
+        it "raises an argument error when no message or block is given" do
+          expect { Sentry.logger.public_send(level) }
+            .to raise_error(ArgumentError, "message or block is required")
+        end
+
         context "handling of malformed strings" do
           let(:malformed_string_default) do
             Sentry::Utils::EncodingHelper::MALFORMED_STRING

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

def trace(message, parameters = [], **attributes)
log(__method__, message, parameters: parameters, **attributes)
def trace(message = nil, parameters = [], **attributes, &block)
log(__method__, message, parameters: parameters, **attributes, &block)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DebugStructuredLogger not updated for block syntax support

Medium Severity

DebugStructuredLogger overrides all the same log-level methods and log with the old signatures — message is required and no &block is accepted. After this change, calling block syntax like logger.info { "Hello" } on a DebugStructuredLogger instance raises an ArgumentError because it still expects a mandatory message argument and silently ignores the block. This breaks the compatibility goal for anyone using the debug logger variant.

Additional Locations (1)
Fix in Cursor Fix in Web

# @return [LogEvent, nil] The created log event or nil if logging is disabled
def log(level, message, parameters:, **attributes)
def log(level, message = nil, parameters:, **attributes, &block)
message = yield if message.nil? && block_given?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nil message causes NoMethodError in LogEvent

Low Severity

Making message optional without a nil guard means calling a log method with no arguments and no block passes nil through to LogEvent#is_template?, which calls body.include?("%s") on nil, raising a NoMethodError. Previously, this scenario raised a clear ArgumentError at the method boundary.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant