timber 2.5.1 → 2.6.0.pre.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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