Repository navigation
fix: write log text that isn't valid UTF-8 instead of raising - #120
Merged
Merged
Conversation
Contributor
Author
|
In the mean time, I'm doing this in my project: # frozen_string_literal: true
# scout_apm_logging's formatter writes each line as JSON, which raises on text that isn't valid UTF-8,
# and the raise escapes the log call: logging Rails' 400 for a param key that isn't UTF-8, or a request
# path with a raw byte, made it a 500. Binary or invalid text is read as UTF-8; to_json converts the rest.
module ScrubInvalidLogText
def call(severity, time, progname, msg)
if msg.is_a?(String) && !(msg.valid_encoding? && msg.encoding != Encoding::BINARY)
msg = msg.dup.force_encoding(Encoding::UTF_8).scrub
end
super
end
end
ScoutApm::Logging::Loggers::Formatter.prepend(ScrubInvalidLogText) |
jrothrock
added a commit
that referenced
this pull request
Oct 19, 2026
logger_spec replaced Formatter#emit_log via class_exec at load time, which lasted for the whole process, so formatter_spec's InMemoryLogRecordExporter saw nothing when both ran together (`bundle exec rspec spec/unit`). Stub it with allow_any_instance_of in a before block instead; spec_helper already resets mocks after each example. Same change as in #120. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Formatter#call ends with to_json, which raises JSON::GeneratorError on text that isn't valid UTF-8. Since json 3.0 it also raises on binary text with any non-ASCII byte. Logger doesn't rescue a formatter's error, so the log call itself raised. In Rails, logging the 400 for a parameter key that isn't UTF-8, or a request path with a raw byte, escaped the app as a 500. msg2str now reads binary text, and text that's invalid in its encoding, as UTF-8, with bytes that aren't valid UTF-8 replaced by U+FFFD. Valid text in other encodings is left for to_json to convert. A String message's OpenTelemetry body is now that same text. The raw bytes made the OTLP encoder raise, and the exporter dropped the whole batch. logger_spec stubbed Formatter#emit_log for every spec loaded with it, so the new body spec failed when the unit specs ran in one process. The stub is now scoped to logger_spec's own examples. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jrothrock
force-pushed
the
fix/formatter-invalid-utf8
branch
from
October 19, 2026 22:49
de4f3dd to
00c70f7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Formatter#callends withto_json, which raisesJSON::GeneratorErroron text that isn't valid UTF-8. Since json 3.0 it also raises on binary text with any non-ASCII byte.Loggerdoesn't rescue a formatter's error, so the log call itself raises.In a Rails 8.1 app with
logs_monitor: true, a crafted request becomes a 500:GET /?%FF=1: Rails refuses the parameter key withActionController::BadRequest, meaning to answer 400. ThenActionDispatch::DebugExceptionslogs the error, and its message holds the raw key. The formatter raises, the raise escapes the app pastShowExceptions, and Puma answers its own 500. Nothing about the request reaches the log.A parameter value that isn't UTF-8 is unaffected, because Rails scrubs the value in its message.
Easy Repro
In this repo:
With this change, it prints the line with
"msg":"bad � byte".The fix
msg2str: it now reads binary text, and text that's invalid in its encoding, as UTF-8, with bytes that aren't valid UTF-8 replaced by U+FFFD. Valid text in other encodings is left forto_jsonto convert, so ISO-8859-1 or UTF-16 text is written as before. Overridingmsg2strcovers String messages, Exception messages andinspectoutput alike.spec/unit/loggers/formatter_spec.rbhas one example per rule:logger_spec.rb's top-levelclass_execreplacedFormatter#emit_logfor every spec loaded with it. It's now scoped to its own examples, so the body example also passes when the unit specs run in one process.Related
log_record_patch_spec.rbunderopentelemetry-exporter-otlp-logs0.7.0, which Remove severity-number exporter patch; map severity at emit time #119 removes; that's the break Log severity numbers will be wrong once opentelemetry-exporter-otlp-logs ships opentelemetry-ruby#2434 #116 describes.spec/unit/loggers/formatter_spec.rb, and edits the sameon_emitcall, so whichever merges second needs a rebase. I'm happy to rebase this onto Remove severity-number exporter patch; map severity at emit time #119, and to move these examples onto its in-memory exporter.logger_spec.rbchange helps Remove severity-number exporter patch; map severity at emit time #119 too. Without it, Remove severity-number exporter patch; map severity at emit time #119's formatter examples fail when the unit specs run in one process (BUNDLE_GEMFILE=gems/rails.gemfile bundle exec rspec spec/unit). The cause islogger_spec's top-levelclass_exec, which replacesemit_logfor every spec loaded with it.🤖 Generated with Claude Code