Skip to content
This repository was archived by the owner on Dec 8, 2020. It is now read-only.

Commit 5d83a04

Browse files
committed
Drop ASCII-8BIT (binary) strings before encoding log data to JSON
1 parent d7f2e0f commit 5d83a04

File tree

6 files changed

+96
-18
lines changed

6 files changed

+96
-18
lines changed

.travis.yml

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ rvm:
55
- 1.9.3
66
- 2.2.6
77
- 2.3.3
8-
- 2.4.1
9-
- jruby
8+
- 2.4.2
9+
- jruby-1.7.26
10+
- jruby-9.1.9.0
1011
gemfile:
1112
- gemfiles/rails-3.0.gemfile
1213
- gemfiles/rails-3.1.gemfile
1314
- gemfiles/rails-3.2.gemfile
1415
- gemfiles/rails-4.0.gemfile
1516
- gemfiles/rails-4.1.gemfile
1617
- gemfiles/rails-4.2.gemfile
17-
- gemfiles/rails-4.2.gemfile
1818
- gemfiles/rails-5.0.gemfile
1919
- gemfiles/rails-5.1.gemfile
2020
- gemfiles/rails-edge.gemfile
@@ -38,15 +38,10 @@ matrix:
3838
gemfile: "gemfiles/rails-3.2.gemfile"
3939
allow_failures:
4040
- rvm: 1.9.3 # bundler wont install the gems
41-
- rvm: 2.4.1
42-
- rvm: jruby
43-
gemfile: gemfiles/rails-3.0.gemfile
44-
- rvm: jruby
45-
gemfile: gemfiles/rails-5.0.gemfile
46-
- rvm: jruby
47-
gemfile: gemfiles/rails-5.1.gemfile
48-
- rvm: jruby
49-
gemfile: gemfiles/rails-edge.gemfile
41+
- rvm: 2.4.2
42+
gemfile: gemfiles/rails-4.0.gemfile
43+
- rvm: 2.4.2
44+
gemfile: gemfiles/rails-4.1.gemfile
5045
notifications:
5146
email: false
5247
slack:

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Drop ASCII-8BIT (binary) data before encoding to JSON. This resolves encoding errors during
13+
this process.
14+
1015
## [2.3.1] - 2017-09-26
1116

1217
### Fixed

lib/timber/log_entry.rb

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Timber
88
# Represents a new log entry into the log. This is an intermediary class between
99
# `Logger` and the log device that you set it up with.
1010
class LogEntry #:nodoc:
11+
BINARY_LIMIT_THRESHOLD = 1_000.freeze
1112
DT_PRECISION = 6.freeze
1213
MESSAGE_MAX_BYTES = 8192.freeze
1314
SCHEMA = "https://raw.githubusercontent.com/timberio/log-event-json-schema/v3.1.1/schema.json".freeze
@@ -73,7 +74,30 @@ def as_json(options = {})
7374
hash
7475
end
7576

76-
Util::Hash.deep_compact(hash)
77+
# Preparing a log event for JSON should remove any blank values. Timber strictly
78+
# validates incoming data, including message size. Blank values will fail validation.
79+
# Moreover, binary data (ASCII-8BIT) generally cannot be encoded into JSON because it
80+
# contains characters outside of the valid UTF-8 space.
81+
Util::Hash.deep_reduce(hash) do |k, v, h|
82+
# Discard blank values
83+
if !v.nil? && (!v.respond_to?(:length) || v.length > 0)
84+
# If the value is a binary string, give it special treatment
85+
if v.respond_to?(:encoding) && v.encoding == ::Encoding::ASCII_8BIT
86+
# Only keep binary values less than a certain size. Sizes larger than this
87+
# are almost always file uploads and data we do not want to log.
88+
if v.length < BINARY_LIMIT_THRESHOLD
89+
# Attempt to safely encode the data to UTF-8
90+
encoded_value = encode_string(v)
91+
if !encoded_value.nil?
92+
h[k] = encoded_value
93+
end
94+
end
95+
else
96+
# Keep all other values
97+
h[k] = v
98+
end
99+
end
100+
end
77101
end
78102

79103
def inspect
@@ -112,5 +136,17 @@ def to_s
112136
def formatted_dt
113137
@formatted_dt ||= time.iso8601(DT_PRECISION)
114138
end
139+
140+
# Attempts to encode a non UTF-8 string into UTF-8, discarding invalid characters.
141+
# If it fails, a nil is returned.
142+
def encode_string(string)
143+
string.encode('UTF-8', {
144+
:invalid => :replace,
145+
:undef => :replace,
146+
:replace => '?'
147+
})
148+
rescue Exception
149+
nil
150+
end
115151
end
116152
end

lib/timber/util/hash.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@ module Hash
66

77
extend self
88

9-
def deep_compact(hash)
9+
# Deeply reduces a hash into a new hash, passing the current key, value,
10+
# and accumulated map up to that point. This allows the caller to
11+
# conditionally rebuild the hash.
12+
def deep_reduce(hash, &block)
1013
new_hash = {}
1114

1215
hash.each do |k, v|
1316
v = if v.is_a?(::Hash)
14-
deep_compact(v)
17+
deep_reduce(v, &block)
1518
else
1619
v
1720
end
1821

19-
if v != nil && (!v.respond_to?(:length) || v.length > 0)
20-
new_hash[k] = v
21-
end
22+
block.call(k, v, new_hash)
2223
end
2324

2425
new_hash

spec/spec_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
require File.join(File.dirname(__FILE__), 'support', 'active_record')
2121
end
2222

23+
RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 5_000
24+
2325
RSpec.configure do |config|
2426
config.color = true
2527
config.order = :random

spec/timber/log_entry_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,45 @@
33
describe Timber::LogEntry, :rails_23 => true do
44
let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
55

6+
describe "#as_json" do
7+
it "should drop nil value keys" do
8+
event = Timber::Events::Custom.new(type: :event_type, message: "event_message", data: {a: nil})
9+
log_entry = described_class.new("INFO", time, nil, "log message", {}, event)
10+
hash = log_entry.as_json
11+
expect(hash.key?(:event)).to be false
12+
end
13+
14+
it "should drop blank string value keys" do
15+
event = Timber::Events::Custom.new(type: :event_type, message: "event_message", data: {a: ""})
16+
log_entry = described_class.new("INFO", time, nil, "log message", {}, event)
17+
hash = log_entry.as_json
18+
expect(hash.key?(:event)).to be false
19+
end
20+
21+
it "should drop empty array value keys" do
22+
event = Timber::Events::Custom.new(type: :event_type, message: "event_message", data: {a: []})
23+
log_entry = described_class.new("INFO", time, nil, "log message", {}, event)
24+
hash = log_entry.as_json
25+
expect(hash.key?(:event)).to be false
26+
end
27+
28+
it "should drop ascii-8bit (binary) value keys" do
29+
binary = ("a" * 1001).force_encoding("ASCII-8BIT")
30+
event = Timber::Events::Custom.new(type: :event_type, message: "event_message", data: {a: binary})
31+
log_entry = described_class.new("INFO", time, nil, "log message", {}, event)
32+
hash = log_entry.as_json
33+
expect(hash.key?(:event)).to be false
34+
end
35+
36+
it "should keep ascii-8bit (binary) values below the threshold" do
37+
binary = "test".force_encoding("ASCII-8BIT")
38+
event = Timber::Events::Custom.new(type: :event_type, message: "event_message", data: {a: binary})
39+
log_entry = described_class.new("INFO", time, nil, "log message", {}, event)
40+
hash = log_entry.as_json
41+
expect(hash[:event][:custom][:event_type][:a].encoding).to eq(::Encoding::UTF_8)
42+
end
43+
end
44+
645
describe "#to_msgpack" do
746
it "should encode properly with an event and context" do
847
event = Timber::Events::Custom.new(type: :event_type, message: "event_message", data: {a: 1})

0 commit comments

Comments
 (0)