Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ node_modules
.devcontainer/.env
vendor/gems
sentry-rails/Gemfile-*.lock
mise.toml
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
### Bug Fixes

- Handle empty frames case gracefully with local vars ([#2807](https://github.com/getsentry/sentry-ruby/pull/2807))
- Prevent SDK crash when SDK logging fails ([#2817](https://github.com/getsentry/sentry-ruby/pull/2817))

## 6.2.0

Expand Down
22 changes: 18 additions & 4 deletions sentry-ruby/lib/sentry/utils/logging_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,40 @@ module LoggingHelper
# @!visibility private
def log_error(message, exception, debug: false)
message = "#{message}: #{exception.message}"
message += "\n#{exception.backtrace.join("\n")}" if debug
message += "\n#{exception.backtrace.join("\n")}" if debug && exception.backtrace

sdk_logger&.error(LOGGER_PROGNAME) do
message
end
sdk_logger&.error(LOGGER_PROGNAME) { message }
rescue StandardError => e
log_to_stderr(e, message)
end

# @!visibility private
def log_debug(message)
sdk_logger&.debug(LOGGER_PROGNAME) { message }
rescue StandardError => e
log_to_stderr(e, message)
end

# @!visibility private
def log_warn(message)
sdk_logger&.warn(LOGGER_PROGNAME) { message }
rescue StandardError => e
log_to_stderr(e, message)
end

# @!visibility private
def sdk_logger
@sdk_logger ||= Sentry.sdk_logger
end

# @!visibility private
def log_to_stderr(error, message)
error_msg = "Sentry SDK logging failed (#{error.class}: #{error.message}): #{message}".scrub(%q(<?>))
error_msg += "\n#{error.backtrace.join("\n")}" if error.backtrace
Comment thread
dingsdax marked this conversation as resolved.
Outdated

$stderr.puts(error_msg)
rescue StandardError
# swallow everything – logging must never crash the app
end
end
end
122 changes: 122 additions & 0 deletions sentry-ruby/spec/sentry/utils/logging_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# frozen_string_literal: true

RSpec.describe Sentry::LoggingHelper do
let(:string_io) { StringIO.new }
let(:logger) { Logger.new(string_io) }

let(:helper_class) do
Class.new do
include Sentry::LoggingHelper
attr_accessor :sdk_logger

def initialize(sdk_logger)
@sdk_logger = sdk_logger
end
end
end

let(:logger_helper) { helper_class.new(logger) }

describe "#log_error" do
it "logs exception message with description" do
exception = StandardError.new("Something went wrong")
logger_helper.log_error("test_error", exception)

expect(string_io.string).to include("test_error: Something went wrong")
end

it "includes backtrace when debug is true" do
exception = StandardError.new("Error")
exception.set_backtrace(["it_broke.rb:1"])

logger_helper.log_error("test_error", exception, debug: true)

expect(string_io.string).to include("it_broke.rb:1")
end
end

describe "stderr fallback when logger fails" do
shared_examples "falls back to stderr" do |method_name, *args|
it "outputs to stderr with error class and message" do
broken_logger = Class.new do
def debug(*); raise IOError, "oops"; end
def warn(*); raise IOError, "oops"; end
end.new

helper = helper_class.new(broken_logger)

expect($stderr).to receive(:puts).with(/Sentry SDK logging failed \(IOError:/)
expect { helper.public_send(method_name, *args) }.not_to raise_error
end
end

context "#log_debug" do
include_examples "falls back to stderr", :log_debug, "Debug message"
end

context "#log_warn" do
include_examples "falls back to stderr", :log_warn, "Warning message"
end

it "includes backtrace in stderr output" do
broken_logger = Class.new do
def error(*)
error = IOError.new("oops")
error.set_backtrace([
"logger.rb:42:in `write'",
"logger.rb:10:in `error'"
])

raise error
end
end.new

helper = helper_class.new(broken_logger)

stderr_output = nil
expect($stderr).to receive(:puts) { |msg| stderr_output = msg }

helper.log_error("Test message", StandardError.new("Error"))

expect(stderr_output).to include("IOError: oops")
expect(stderr_output).to include("logger.rb:42:in `write'")
expect(stderr_output).to include("logger.rb:10:in `error'")
end
end

describe "custom JSON logger with encoding errors" do
# Custom logger from GitHub issue #2805
let(:json_logger) do
Class.new(::Logger) do
class JsonFormatter
def call(level, _, _, m)
{ severity: level, message: m }.to_json << "\n"
end
end

def initialize(*)
super
self.formatter = JsonFormatter.new
end
end.new(StringIO.new)
end

let(:logger_helper) { helper_class.new(json_logger) }

it "scrubs invalid UTF-8 in stderr output when JSON logger fails on encoding" do
helper = helper_class.new(json_logger)

invalid_message = "a\x92b"
exception = StandardError.new("oops")

stderr_message = nil
expect($stderr).to receive(:puts) { |msg| stderr_message = msg }

expect { helper.log_error(invalid_message, exception) }.not_to raise_error

expect(stderr_message).to include("JSON::GeneratorError")
expect(stderr_message).to include("a<?>b")
expect(stderr_message).to include("oops")
end
end
end