timber 2.5.1 → 2.6.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -1
  3. data/lib/timber/config.rb +2 -1
  4. data/lib/timber/contexts/custom.rb +10 -3
  5. data/lib/timber/contexts/http.rb +23 -7
  6. data/lib/timber/contexts/organization.rb +14 -3
  7. data/lib/timber/contexts/release.rb +17 -4
  8. data/lib/timber/contexts/runtime.rb +28 -9
  9. data/lib/timber/contexts/session.rb +11 -2
  10. data/lib/timber/contexts/system.rb +13 -3
  11. data/lib/timber/contexts/user.rb +22 -6
  12. data/lib/timber/events/controller_call.rb +14 -24
  13. data/lib/timber/events/custom.rb +8 -5
  14. data/lib/timber/events/error.rb +11 -31
  15. data/lib/timber/events/http_request.rb +39 -13
  16. data/lib/timber/events/http_response.rb +32 -14
  17. data/lib/timber/events/sql_query.rb +10 -7
  18. data/lib/timber/events/template_render.rb +11 -5
  19. data/lib/timber/log_entry.rb +7 -31
  20. data/lib/timber/logger.rb +1 -5
  21. data/lib/timber/util.rb +2 -2
  22. data/lib/timber/util/attribute_normalizer.rb +90 -0
  23. data/lib/timber/util/hash.rb +51 -1
  24. data/lib/timber/util/non_nil_hash_builder.rb +38 -0
  25. data/lib/timber/version.rb +1 -1
  26. data/spec/timber/events/error_spec.rb +8 -22
  27. data/spec/timber/events/http_request_spec.rb +1 -1
  28. data/spec/timber/events_spec.rb +1 -1
  29. data/spec/timber/integrations/action_dispatch/debug_exceptions_spec.rb +1 -1
  30. data/spec/timber/log_entry_spec.rb +0 -39
  31. data/spec/timber/logger_spec.rb +0 -5
  32. data/spec/timber/util/attribute_normalizer_spec.rb +90 -0
  33. metadata +8 -8
  34. data/lib/timber/util/http_event.rb +0 -69
  35. data/lib/timber/util/object.rb +0 -15
  36. data/spec/timber/util/http_event_spec.rb +0 -15
@@ -2,6 +2,7 @@ module Timber
2
2
  module Util
3
3
  # @private
4
4
  module Hash
5
+ BINARY_LIMIT_THRESHOLD = 1_000.freeze
5
6
  SANITIZED_VALUE = '[sanitized]'.freeze
6
7
 
7
8
  extend self
@@ -25,7 +26,37 @@ module Timber
25
26
  new_hash
26
27
  end
27
28
 
28
- def sanitize(hash, keys_to_sanitize)
29
+ # Recursively traverses a hash, dropping non-JSON compatible types.
30
+ # If the string is a binary, and it is > 1000 characters, it is dropped.
31
+ # We are assuming it represents file contents that should not be included
32
+ # in the logs.
33
+ def jsonify(hash)
34
+ deep_reduce(hash) do |k, v, h|
35
+ if v.is_a?(String)
36
+ if v.encoding == ::Encoding::ASCII_8BIT
37
+ # Only keep binary values less than a certain size. Sizes larger than this
38
+ # are almost always file uploads and data we do not want to log.
39
+ if v.length < BINARY_LIMIT_THRESHOLD
40
+ # Attempt to safely encode the data to UTF-8
41
+ encoded_value = encode_string(v)
42
+ if !encoded_value.nil?
43
+ h[k] = encoded_value
44
+ end
45
+ end
46
+ elsif v.encoding != ::Encoding::UTF_8
47
+ h[k] = encode_string(v)
48
+ else
49
+ h[k] = v
50
+ end
51
+ elsif is_a_primitive_type?(v)
52
+ # Keep all other primitive types
53
+ h[k] = v
54
+ end
55
+ end
56
+ end
57
+
58
+ # Replaces matching keys with a `[Sanitized]` value.
59
+ def sanitize_keys(hash, keys_to_sanitize)
29
60
  hash.each_with_object({}) do |(k, v), h|
30
61
  k = k.to_s.downcase
31
62
  if keys_to_sanitize.include?(k)
@@ -35,6 +66,25 @@ module Timber
35
66
  end
36
67
  end
37
68
  end
69
+
70
+ private
71
+ # Attempts to encode a non UTF-8 string into UTF-8, discarding invalid characters.
72
+ # If it fails, a nil is returned.
73
+ def encode_string(string)
74
+ string.encode('UTF-8', {
75
+ :invalid => :replace,
76
+ :undef => :replace,
77
+ :replace => '?'
78
+ })
79
+ rescue Exception
80
+ nil
81
+ end
82
+
83
+ # We use is_a? because it accounts for inheritance.
84
+ def is_a_primitive_type?(v)
85
+ v.is_a?(Array) || v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(TrueClass) ||
86
+ v.is_a?(FalseClass) || v.is_a?(String) || v.is_a?(Time)
87
+ end
38
88
  end
39
89
  end
40
90
  end
@@ -0,0 +1,38 @@
1
+ module Timber
2
+ module Util
3
+ # @private
4
+ #
5
+ # The purpose of this class is to efficiently build a hash that does not
6
+ # include nil values. It's proactive instead of reactive, avoiding the
7
+ # need to traverse and reduce a new hash dropping blanks.
8
+ class NonNilHashBuilder
9
+ class << self
10
+ def build(&block)
11
+ builder = new
12
+ yield builder
13
+ builder.target
14
+ end
15
+ end
16
+
17
+ attr_reader :target
18
+
19
+ def initialize
20
+ @target = {}
21
+ end
22
+
23
+ def add(k, v, options = {})
24
+ if !v.nil?
25
+ if options[:json_encode]
26
+ v = v.to_json
27
+ end
28
+
29
+ if options[:limit]
30
+ v = v.byteslice(0, options[:limit])
31
+ end
32
+
33
+ @target[k] = v
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  module Timber
2
- VERSION = "2.5.1"
2
+ VERSION = "2.6.0-beta1"
3
3
  end
@@ -1,34 +1,20 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Timber::Events::Error, :rails_23 => true do
4
- describe ".initialize" do
5
- it "should clean the backtrace" do
4
+ describe "#to_hash" do
5
+ it "should jsonify the stacktrace" do
6
6
  backtrace = [
7
7
  "/path/to/file1.rb:26:in `function1'",
8
8
  "path/to/file2.rb:86:in `function2'"
9
9
  ]
10
-
11
10
  exception_event = described_class.new(name: "RuntimeError", error_message: "Boom", backtrace: backtrace)
12
- expect(exception_event.backtrace).to eq([{:file=>"/path/to/file1.rb", :line=>26, :function=>"function1"}, {:file=>"path/to/file2.rb", :line=>86, :function=>"function2"}])
13
- end
14
-
15
- it "parses valid lines" do
16
- backtrace = [
17
- "/path/to/file1.rb:26:in `function1'",
18
- "path/to/file2.rb:86" # function names are optional
19
- ]
20
11
 
21
- exception_event = described_class.new(name: "RuntimeError", error_message: "Boom", backtrace: backtrace)
22
- expect(exception_event.backtrace).to eq([{:file=>"/path/to/file1.rb", :line=>26, :function=>"function1"}, {:file=>"path/to/file2.rb", :line=>86}])
12
+ expected_hash = {
13
+ :name => "RuntimeError",
14
+ :message => "Boom",
15
+ :backtrace_json => "[\"/path/to/file1.rb:26:in `function1'\",\"path/to/file2.rb:86:in `function2'\"]"
16
+ }
17
+ expect(exception_event.to_hash).to eq(expected_hash)
23
18
  end
24
-
25
- it "handles malformed lines" do
26
- backtrace = [
27
- "malformed"
28
- ]
29
-
30
- exception_event = described_class.new(name: "RuntimeError", error_message: "Boom", backtrace: backtrace)
31
- expect(exception_event.backtrace).to eq([{:file=>"malformed"}])
32
- end
33
19
  end
34
20
  end
@@ -26,7 +26,7 @@ describe Timber::Events::HTTPRequest, :rails_23 => true do
26
26
  it "should handle header encoding" do
27
27
  referer = 'http://www.metrojobb.se/jobb/1013893-skadeadministratör'.force_encoding('ASCII-8BIT')
28
28
  event = described_class.new(:headers => {'Referer' => referer}, :host => 'my.host.com', :method => 'GET', :path => '/path', :scheme => 'https')
29
- expect(event.headers.key?("referer")).to eq(false)
29
+ expect(event.headers["referer"].encoding).to eq(::Encoding::UTF_8)
30
30
  end
31
31
  end
32
32
  end
@@ -18,7 +18,7 @@ describe Timber::Events, :rails_23 => true do
18
18
  Timber::Events::Custom.new(
19
19
  type: :payment_rejected,
20
20
  message: "Payment rejected for #{customer_id}",
21
- data: respond_to?(:hash) ? hash : to_hash
21
+ data: respond_to?(:to_h) ? to_h : hash
22
22
  )
23
23
  end
24
24
  end
@@ -41,7 +41,7 @@ if defined?(::ActionDispatch)
41
41
  lines = clean_lines(io.string.split("\n"))
42
42
  expect(lines.length).to eq(3)
43
43
  expect(lines[2]).to start_with('RuntimeError (boom) @metadata {"level":"fatal",')
44
- expect(lines[2]).to include("\"event\":{\"error\":{\"name\":\"RuntimeError\",\"message\":\"boom\",\"backtrace\":[")
44
+ expect(lines[2]).to include("\"event\":{\"error\":{\"name\":\"RuntimeError\",\"message\":\"boom\",\"backtrace_json\":\"[")
45
45
  end
46
46
 
47
47
  # Remove blank lines since Rails does this to space out requests in the logs
@@ -3,45 +3,6 @@ require "spec_helper"
3
3
  describe Timber::LogEntry, :rails_23 => true do
4
4
  let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
5
5
 
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
-
45
6
  describe "#to_msgpack" do
46
7
  it "should encode properly with an event and context" do
47
8
  event = Timber::Events::Custom.new(type: :event_type, message: "event_message", data: {a: 1})
@@ -130,11 +130,6 @@ describe Timber::Logger, :rails_23 => true do
130
130
  expect(io.string).to include("\"event\":{\"sql_query\":{\"sql\":\"select * from users\",\"time_ms\":56.0}}")
131
131
  end
132
132
 
133
- it "should allow :time_ms" do
134
- logger.info("event complete", time_ms: 54.5)
135
- expect(io.string).to include("\"time_ms\":54.5")
136
- end
137
-
138
133
  it "should allow :tag" do
139
134
  logger.info("event complete", tag: "tag1")
140
135
  expect(io.string).to include("\"tags\":[\"tag1\"]")
@@ -0,0 +1,90 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::Util::AttributeNormalizer, :rails_23 => true do
4
+ describe "#fetch" do
5
+ it "should return nil values" do
6
+ normalizer = described_class.new({:key => nil})
7
+ v = normalizer.fetch(:key, :string)
8
+ expect(v).to be_nil
9
+ end
10
+
11
+ it "should nillify blank strings" do
12
+ normalizer = described_class.new({:key => ""})
13
+ v = normalizer.fetch(:key, :string)
14
+ expect(v).to be_nil
15
+ end
16
+
17
+ it "should nillify empty arrays" do
18
+ normalizer = described_class.new({:key => []})
19
+ v = normalizer.fetch(:key, :string)
20
+ expect(v).to be_nil
21
+ end
22
+
23
+ it "should nillify empty hashes" do
24
+ normalizer = described_class.new({:key => {}})
25
+ v = normalizer.fetch(:key, :string)
26
+ expect(v).to be_nil
27
+ end
28
+
29
+ it "should raise an error for non arrays" do
30
+ normalizer = described_class.new({:key => "value"})
31
+ expect(lambda { normalizer.fetch(:key, :array) }).to raise_error(ArgumentError)
32
+ end
33
+
34
+ it "should return arrays" do
35
+ normalizer = described_class.new({:key => [1]})
36
+ v = normalizer.fetch(:key, :array)
37
+ expect(v).to eq([1])
38
+ end
39
+
40
+ it "should return a float with the correct precision" do
41
+ normalizer = described_class.new({:key => 1.111111})
42
+ v = normalizer.fetch(:key, :float, :precision => 2)
43
+ expect(v).to eq(1.11)
44
+ end
45
+
46
+ it "should sanitize a hash" do
47
+ normalizer = described_class.new({:key => {:PASSWORD => "password"}})
48
+ v = normalizer.fetch(:key, :hash, :sanitize => ["password"])
49
+ expect(v).to eq({"password"=>"[sanitized]"})
50
+ end
51
+
52
+ it "should normalize encodings" do
53
+ value = "test".force_encoding('ASCII-8BIT')
54
+ normalizer = described_class.new({:key => {:key => value}})
55
+ v = normalizer.fetch(:key, :hash)
56
+ expect(v[:key].encoding).to eq(::Encoding::UTF_8)
57
+ end
58
+
59
+ it "should drop large binaries" do
60
+ value = ("a" * 1_001).force_encoding('ASCII-8BIT')
61
+ normalizer = described_class.new({:key => {:key => value}})
62
+ v = normalizer.fetch(:key, :hash)
63
+ expect(v).to be_nil
64
+ end
65
+
66
+ it "should return an integer" do
67
+ normalizer = described_class.new({:key => "1"})
68
+ v = normalizer.fetch(:key, :integer)
69
+ expect(v).to eq(1)
70
+ end
71
+
72
+ it "should limit a string" do
73
+ normalizer = described_class.new({:key => "aaa"})
74
+ v = normalizer.fetch(:key, :string, :limit => 1)
75
+ expect(v).to eq("a")
76
+ end
77
+
78
+ it "should upcase a string" do
79
+ normalizer = described_class.new({:key => "aaa"})
80
+ v = normalizer.fetch(:key, :string, :upcase => true)
81
+ expect(v).to eq("AAA")
82
+ end
83
+
84
+ it "should return a symbol" do
85
+ normalizer = described_class.new({:key => "sym"})
86
+ v = normalizer.fetch(:key, :symbol)
87
+ expect(v).to eq(:sym)
88
+ end
89
+ end
90
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timber
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.1
4
+ version: 2.6.0.pre.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timber Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-27 00:00:00.000000000 Z
11
+ date: 2017-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -242,9 +242,9 @@ files:
242
242
  - lib/timber/timer.rb
243
243
  - lib/timber/util.rb
244
244
  - lib/timber/util/active_support_log_subscriber.rb
245
+ - lib/timber/util/attribute_normalizer.rb
245
246
  - lib/timber/util/hash.rb
246
- - lib/timber/util/http_event.rb
247
- - lib/timber/util/object.rb
247
+ - lib/timber/util/non_nil_hash_builder.rb
248
248
  - lib/timber/util/request.rb
249
249
  - lib/timber/util/struct.rb
250
250
  - lib/timber/version.rb
@@ -291,7 +291,7 @@ files:
291
291
  - spec/timber/log_devices/http_spec.rb
292
292
  - spec/timber/log_entry_spec.rb
293
293
  - spec/timber/logger_spec.rb
294
- - spec/timber/util/http_event_spec.rb
294
+ - spec/timber/util/attribute_normalizer_spec.rb
295
295
  - spec/timber/util/request_spec.rb
296
296
  - timber.gemspec
297
297
  homepage: https://github.com/timberio/timber-ruby
@@ -308,9 +308,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
308
308
  version: 1.9.0
309
309
  required_rubygems_version: !ruby/object:Gem::Requirement
310
310
  requirements:
311
- - - ">="
311
+ - - ">"
312
312
  - !ruby/object:Gem::Version
313
- version: '0'
313
+ version: 1.3.1
314
314
  requirements: []
315
315
  rubyforge_project:
316
316
  rubygems_version: 2.4.5.2
@@ -361,5 +361,5 @@ test_files:
361
361
  - spec/timber/log_devices/http_spec.rb
362
362
  - spec/timber/log_entry_spec.rb
363
363
  - spec/timber/logger_spec.rb
364
- - spec/timber/util/http_event_spec.rb
364
+ - spec/timber/util/attribute_normalizer_spec.rb
365
365
  - spec/timber/util/request_spec.rb
@@ -1,69 +0,0 @@
1
- module Timber
2
- module Util
3
- # Utility module for dealing with HTTP events {Events::HTTPRequest} and {Events::HTTPResponse}.
4
- module HTTPEvent
5
- HEADERS_TO_SANITIZE = ['authorization', 'x-amz-security-token'].freeze
6
- MAX_QUERY_STRING_BYTES = 2048.freeze
7
- STRING_CLASS_NAME = 'String'.freeze
8
-
9
- extend self
10
-
11
- def full_path(path, query_string)
12
- if query_string
13
- "#{path}?#{query_string}"
14
- else
15
- path
16
- end
17
- end
18
-
19
- # Normalizes the body. If limit if passed it will truncate the body to that limit.
20
- def normalize_body(body)
21
- if body.respond_to?(:body)
22
- body = body.body.to_s
23
- end
24
-
25
- limit = Config.instance.http_body_limit
26
- if limit
27
- body.byteslice(0, limit)
28
- else
29
- body
30
- end
31
- end
32
-
33
- # Normalizes headers to:
34
- #
35
- # 1. Only select values that are UTF8, otherwise they will throw errors when serializing.
36
- # 2. Sanitize sensitive headers such as `Authorization` or custom headers specified in
37
- def normalize_headers(headers)
38
- if headers.is_a?(::Hash)
39
- h = headers.each_with_object({}) do |(k, v), h|
40
- if v
41
- v = v.to_s
42
- if [Encoding::UTF_8, Encoding::US_ASCII].include?(v.encoding)
43
- h[k] = v
44
- end
45
- end
46
- end
47
-
48
- keys_to_sanitize = HEADERS_TO_SANITIZE + (Config.instance.http_header_filters || [])
49
- Util::Hash.sanitize(h, keys_to_sanitize)
50
- else
51
- headers
52
- end
53
- end
54
-
55
- # Normalizes the HTTP method into an uppercase string.
56
- def normalize_method(method)
57
- method.is_a?(::String) ? method.upcase : method
58
- end
59
-
60
- def normalize_query_string(query_string)
61
- if !query_string.nil?
62
- query_string = query_string.to_s
63
- end
64
-
65
- query_string && query_string.byteslice(0, MAX_QUERY_STRING_BYTES)
66
- end
67
- end
68
- end
69
- end