Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -39,6 +39,7 @@
# This now works too and the nested hash is dumped to JSON string
Sentry.logger.info("Hello World", extra: { today: Date.today, user_id: user.id })
```
- 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.map { |line| line.scrub(%q(<?>)) }.join("\n")}" if error.backtrace

$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
Loading