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
@@ -8,24 +8,24 @@ module Timber
8
8
  # @note This event should be installed automatically through integrations,
9
9
  # such as the {Integrations::ActionDispatch::DebugExceptions} integration.
10
10
  class Error < Timber::Event
11
- MAX_MESSAGE_BYTES = 8192.freeze
11
+ BACKTRACE_JSON_MAX_BYTES = 8192.freeze
12
+ MESSAGE_MAX_BYTES = 8192.freeze
12
13
 
13
14
  attr_reader :name, :error_message, :backtrace
14
15
 
15
16
  def initialize(attributes)
16
- @name = attributes[:name] || raise(ArgumentError.new(":name is required"))
17
-
18
- @error_message = attributes[:error_message] || raise(ArgumentError.new(":error_message is required"))
19
- @error_message = @error_message.byteslice(0, MAX_MESSAGE_BYTES)
20
-
21
- backtrace = attributes[:backtrace]
22
- if !backtrace.nil? && backtrace != []
23
- @backtrace = backtrace[0..9].collect { |line| parse_backtrace_line(line) }
24
- end
17
+ normalizer = Util::AttributeNormalizer.new(attributes)
18
+ @name = normalizer.fetch!(:name, :string)
19
+ @error_message = normalizer.fetch!(:error_message, :string, :limit => MESSAGE_MAX_BYTES)
20
+ @backtrace = normalizer.fetch(:backtrace, :array)
25
21
  end
26
22
 
27
23
  def to_hash
28
- {name: name, message: error_message, backtrace: backtrace}
24
+ @to_hash ||= Util::NonNilHashBuilder.build do |h|
25
+ h.add(:name, name)
26
+ h.add(:message, error_message)
27
+ h.add(:backtrace_json, backtrace, :json_encode => true, :limit => BACKTRACE_JSON_MAX_BYTES)
28
+ end
29
29
  end
30
30
  alias to_h to_hash
31
31
 
@@ -37,26 +37,6 @@ module Timber
37
37
  def message
38
38
  "#{name} (#{error_message})"
39
39
  end
40
-
41
- private
42
- def parse_backtrace_line(line)
43
- # using split for performance reasons
44
- file, line, function_part = line.split(":", 3)
45
-
46
- parsed_line = {file: file}
47
-
48
- if line
49
- parsed_line[:line] = line.to_i
50
- end
51
-
52
- if function_part
53
- _prefix, function_pre = function_part.split("`", 2)
54
- function = Util::Object.try(function_pre, :chomp, "'")
55
- parsed_line[:function] = function
56
- end
57
-
58
- parsed_line
59
- end
60
40
  end
61
41
  end
62
42
  end
@@ -9,26 +9,52 @@ module Timber
9
9
  # @note This event should be installed automatically through integrations,
10
10
  # such as the {Integrations::ActionController::LogSubscriber} integration.
11
11
  class HTTPRequest < Timber::Event
12
+ BODY_MAX_BYTES = 8192.freeze
13
+ HEADERS_JSON_MAX_BYTES = 8192.freeze
14
+ HEADERS_TO_SANITIZE = ['authorization', 'x-amz-security-token'].freeze
15
+ HOST_MAX_BYTES = 256.freeze
16
+ METHOD_MAX_BYTES = 20.freeze
17
+ PATH_MAX_BYTES = 2048.freeze
18
+ QUERY_STRING_MAX_BYTES = 2048.freeze
19
+ REQUEST_ID_MAX_BYTES = 256.freeze
20
+ SCHEME_MAX_BYTES = 20.freeze
21
+ SERVICE_NAME_MAX_BYTES = 256.freeze
22
+
12
23
  attr_reader :body, :content_length, :headers, :host, :method, :path, :port, :query_string,
13
24
  :request_id, :scheme, :service_name
14
25
 
15
26
  def initialize(attributes)
16
- @body = attributes[:body] && Util::HTTPEvent.normalize_body(attributes[:body])
17
- @content_length = Timber::Util::Object.try(attributes[:content_length], :to_i)
18
- @headers = Util::HTTPEvent.normalize_headers(attributes[:headers])
19
- @host = attributes[:host]
20
- @method = Util::HTTPEvent.normalize_method(attributes[:method]) || raise(ArgumentError.new(":method is required"))
21
- @path = attributes[:path]
22
- @port = attributes[:port]
23
- @query_string = Util::HTTPEvent.normalize_query_string(attributes[:query_string])
24
- @scheme = attributes[:scheme]
25
- @request_id = attributes[:request_id]
27
+ normalizer = Util::AttributeNormalizer.new(attributes)
28
+ body_limit = Config.instance.http_body_limit || BODY_MAX_BYTES
29
+ headers_to_sanitize = HEADERS_TO_SANITIZE + (Config.instance.http_header_filters || [])
30
+
31
+ @body = normalizer.fetch(:body, :string, :limit => body_limit)
32
+ @content_length = normalizer.fetch(:content_length, :integer)
33
+ @headers = normalizer.fetch(:headers, :hash, :sanitize => headers_to_sanitize)
34
+ @host = normalizer.fetch(:host, :string, :limit => HOST_MAX_BYTES)
35
+ @method = normalizer.fetch!(:method, :string, :upcase => true, :limit => METHOD_MAX_BYTES)
36
+ @path = normalizer.fetch(:path, :string, :limit => PATH_MAX_BYTES)
37
+ @port = normalizer.fetch(:port, :integer)
38
+ @query_string = normalizer.fetch(:query_string, :string, :limit => QUERY_STRING_MAX_BYTES)
39
+ @scheme = normalizer.fetch(:scheme, :string, :limit => SCHEME_MAX_BYTES)
40
+ @request_id = normalizer.fetch(:request_id, :string, :limit => REQUEST_ID_MAX_BYTES)
41
+ @service_name = normalizer.fetch(:service_name, :string, :limit => SERVICE_NAME_MAX_BYTES)
26
42
  end
27
43
 
28
44
  def to_hash
29
- {body: body, content_length: content_length, headers: headers, host: host, method: method,
30
- path: path, port: port, query_string: query_string, request_id: request_id,
31
- scheme: scheme}
45
+ @to_hash ||= Util::NonNilHashBuilder.build do |h|
46
+ h.add(:body, body)
47
+ h.add(:content_length, content_length)
48
+ h.add(:headers_json, headers, :json_encode => true, :limit => HEADERS_JSON_MAX_BYTES)
49
+ h.add(:host, host)
50
+ h.add(:method, method)
51
+ h.add(:path, path)
52
+ h.add(:port, port)
53
+ h.add(:query_string, query_string)
54
+ h.add(:request_id, request_id)
55
+ h.add(:scheme, scheme)
56
+ h.add(:service_name, service_name)
57
+ end
32
58
  end
33
59
  alias to_h to_hash
34
60
 
@@ -9,22 +9,40 @@ module Timber
9
9
  # @note This event should be installed automatically through integrations,
10
10
  # such as the {Integrations::ActionController::LogSubscriber} integration.
11
11
  class HTTPResponse < Timber::Event
12
- attr_reader :body, :content_length, :headers, :http_context, :request_id, :service_name, :status, :time_ms
12
+ BODY_MAX_BYTES = 8192.freeze
13
+ HEADERS_JSON_MAX_BYTES = 256.freeze
14
+ HEADERS_TO_SANITIZE = ['authorization', 'x-amz-security-token'].freeze
15
+ REQUEST_ID_MAX_BYTES = 256.freeze
16
+ SERVICE_NAME_MAX_BYTES = 256.freeze
17
+
18
+ attr_reader :body, :content_length, :headers, :http_context, :request_id, :service_name,
19
+ :status, :time_ms
13
20
 
14
21
  def initialize(attributes)
15
- @body = attributes[:body] && Util::HTTPEvent.normalize_body(attributes[:body])
16
- @content_length = Timber::Util::Object.try(attributes[:content_length], :to_i)
17
- @headers = Util::HTTPEvent.normalize_headers(attributes[:headers])
22
+ normalizer = Util::AttributeNormalizer.new(attributes)
23
+ body_limit = Config.instance.http_body_limit || BODY_MAX_BYTES
24
+ headers_to_sanitize = HEADERS_TO_SANITIZE + (Config.instance.http_header_filters || [])
25
+
26
+ @body = normalizer.fetch(:body, :string, :limit => body_limit)
27
+ @content_length = normalizer.fetch(:content_length, :integer)
28
+ @headers = normalizer.fetch(:headers, :hash, :sanitize => headers_to_sanitize)
18
29
  @http_context = attributes[:http_context]
19
- @request_id = attributes[:request_id]
20
- @status = attributes[:status] || raise(ArgumentError.new(":status is required"))
21
- @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
22
- @time_ms = @time_ms.round(6)
30
+ @request_id = normalizer.fetch(:request_id, :string, :limit => REQUEST_ID_MAX_BYTES)
31
+ @service_name = normalizer.fetch(:service_name, :string, :limit => SERVICE_NAME_MAX_BYTES)
32
+ @status = normalizer.fetch!(:status, :integer)
33
+ @time_ms = normalizer.fetch!(:time_ms, :float, :precision => 6)
23
34
  end
24
35
 
25
36
  def to_hash
26
- {body: body, content_length: content_length, headers: headers, request_id: request_id,
27
- status: status, time_ms: time_ms}
37
+ @to_hash ||= Util::NonNilHashBuilder.build do |h|
38
+ h.add(:body, body)
39
+ h.add(:content_length, content_length)
40
+ h.add(:headers_json, headers, :json_encode => true, :limit => HEADERS_JSON_MAX_BYTES)
41
+ h.add(:request_id, request_id)
42
+ h.add(:service_name, service_name)
43
+ h.add(:status, status)
44
+ h.add(:time_ms, time_ms)
45
+ end
28
46
  end
29
47
  alias to_h to_hash
30
48
 
@@ -40,18 +58,18 @@ module Timber
40
58
  "#{status} #{status_description} "
41
59
 
42
60
  if content_length
43
- message += ", #{content_length} bytes, "
61
+ message << ", #{content_length} bytes, "
44
62
  end
45
63
 
46
- message + "in #{time_ms}ms"
64
+ message << "in #{time_ms}ms"
47
65
  else
48
66
  message = "Completed #{status} #{status_description} "
49
67
 
50
68
  if content_length
51
- message += ", #{content_length} bytes, "
69
+ message << ", #{content_length} bytes, "
52
70
  end
53
71
 
54
- message + "in #{time_ms}ms"
72
+ message << "in #{time_ms}ms"
55
73
  end
56
74
  end
57
75
 
@@ -7,20 +7,23 @@ module Timber
7
7
  # @note This event should be installed automatically through integrations,
8
8
  # such as the {Integrations::ActiveRecord::LogSubscriber} integration.
9
9
  class SQLQuery < Timber::Event
10
- MAX_QUERY_BYTES = 1024.freeze
10
+ MESSAGE_MAX_BYTES = 8192.freeze
11
+ SQL_MAX_BYTES = 4096.freeze
11
12
 
12
13
  attr_reader :sql, :time_ms, :message
13
14
 
14
15
  def initialize(attributes)
15
- @sql = attributes[:sql] || raise(ArgumentError.new(":sql is required"))
16
- @sql = @sql.byteslice(0, MAX_QUERY_BYTES)
17
- @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
18
- @time_ms = @time_ms.round(6)
19
- @message = attributes[:message] || raise(ArgumentError.new(":message is required"))
16
+ normalizer = Util::AttributeNormalizer.new(attributes)
17
+ @message = normalizer.fetch!(:message, :string, :limit => MESSAGE_MAX_BYTES)
18
+ @sql = normalizer.fetch!(:sql, :string, :limit => SQL_MAX_BYTES)
19
+ @time_ms = normalizer.fetch!(:time_ms, :float, :precision => 6)
20
20
  end
21
21
 
22
22
  def to_hash
23
- {sql: sql, time_ms: time_ms}
23
+ @to_hash ||= Util::NonNilHashBuilder.build do |h|
24
+ h.add(:sql, sql)
25
+ h.add(:time_ms, time_ms)
26
+ end
24
27
  end
25
28
  alias to_h to_hash
26
29
 
@@ -7,17 +7,23 @@ module Timber
7
7
  # @note This event should be installed automatically through integrations,
8
8
  # such as the {Integrations::ActionView::LogSubscriber} integration.
9
9
  class TemplateRender < Timber::Event
10
+ MESSAGE_MAX_BYTES = 8192.freeze
11
+ NAME_MAX_BYTES = 1024.freeze
12
+
10
13
  attr_reader :message, :name, :time_ms
11
14
 
12
15
  def initialize(attributes)
13
- @message = attributes[:message] || raise(ArgumentError.new(":message is required"))
14
- @name = attributes[:name] || raise(ArgumentError.new(":name is required"))
15
- @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
16
- @time_ms = @time_ms.round(6)
16
+ normalizer = Util::AttributeNormalizer.new(attributes)
17
+ @message = normalizer.fetch!(:message, :string, :limit => MESSAGE_MAX_BYTES)
18
+ @name = normalizer.fetch!(:name, :string, :limit => NAME_MAX_BYTES)
19
+ @time_ms = normalizer.fetch!(:time_ms, :float, :precision => 6)
17
20
  end
18
21
 
19
22
  def to_hash
20
- {name: name, time_ms: time_ms}
23
+ @to_hash ||= Util::NonNilHashBuilder.build do |h|
24
+ h.add(:name, name)
25
+ h.add(:time_ms, time_ms)
26
+ end
21
27
  end
22
28
  alias to_h to_hash
23
29
 
@@ -13,7 +13,7 @@ module Timber
13
13
  MESSAGE_MAX_BYTES = 8192.freeze
14
14
  SCHEMA = "https://raw.githubusercontent.com/timberio/log-event-json-schema/v3.2.0/schema.json".freeze
15
15
 
16
- attr_reader :context_snapshot, :event, :level, :message, :progname, :tags, :time, :time_ms
16
+ attr_reader :context_snapshot, :event, :level, :message, :progname, :tags, :time
17
17
 
18
18
  # Creates a log entry suitable to be sent to the Timber API.
19
19
  # @param level [Integer] the log level / severity
@@ -36,7 +36,6 @@ module Timber
36
36
  @message = message.is_a?(String) ? message : message.inspect
37
37
  @message = @message.byteslice(0, MESSAGE_MAX_BYTES)
38
38
  @tags = options[:tags]
39
- @time_ms = options[:time_ms]
40
39
  @context_snapshot = context_snapshot
41
40
  @event = event
42
41
  end
@@ -47,11 +46,13 @@ module Timber
47
46
  hash = {
48
47
  :level => level,
49
48
  :dt => formatted_dt,
50
- :message => message,
51
- :tags => tags,
52
- :time_ms => time_ms
49
+ :message => message
53
50
  }
54
51
 
52
+ if !tags.nil? && tags.length > 0
53
+ hash[:tags] = tags
54
+ end
55
+
55
56
  if !event.nil?
56
57
  hash[:event] = event.as_json
57
58
  end
@@ -62,7 +63,7 @@ module Timber
62
63
 
63
64
  hash[:"$schema"] = SCHEMA
64
65
 
65
- hash = if options[:only]
66
+ if options[:only]
66
67
  hash.select do |key, _value|
67
68
  options[:only].include?(key)
68
69
  end
@@ -73,31 +74,6 @@ module Timber
73
74
  else
74
75
  hash
75
76
  end
76
-
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
101
77
  end
102
78
 
103
79
  def inspect
data/lib/timber/logger.rb CHANGED
@@ -47,15 +47,11 @@ module Timber
47
47
  tags.concat(logged_obj.delete(:tags)) if logged_obj.key?(:tags)
48
48
  tags.uniq!
49
49
 
50
- # Extract the time_ms
51
- time_ms = logged_obj.delete(:time_ms)
52
-
53
50
  # Build the event
54
51
  event = Events.build(logged_obj)
55
52
  message = event ? event.message : logged_obj[:message]
56
53
 
57
- LogEntry.new(level, time, progname, message, context_snapshot, event, tags: tags,
58
- time_ms: time_ms)
54
+ LogEntry.new(level, time, progname, message, context_snapshot, event, tags: tags)
59
55
  else
60
56
  LogEntry.new(level, time, progname, logged_obj, context_snapshot, nil, tags: tags)
61
57
  end
data/lib/timber/util.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require "timber/util/active_support_log_subscriber"
2
+ require "timber/util/attribute_normalizer"
2
3
  require "timber/util/hash"
3
- require "timber/util/http_event"
4
- require "timber/util/object"
4
+ require "timber/util/non_nil_hash_builder"
5
5
  require "timber/util/request"
6
6
  require "timber/util/struct"
7
7
 
@@ -0,0 +1,90 @@
1
+ module Timber
2
+ module Util
3
+ # @private
4
+ #
5
+ # The purpose of this class is to normalize parameters passed to events
6
+ # and contexts. Timber validates a rigid JSON schema against the defined
7
+ # Timber log event JSON schema. This normalization process ensures the
8
+ # data passed to events and contexts conforms to this structure.
9
+ class AttributeNormalizer
10
+ def initialize(attributes)
11
+ @attributes = attributes
12
+ end
13
+
14
+ def fetch!(key, type, options = {})
15
+ v = fetch(key, type, options)
16
+ if v.nil?
17
+ raise @attributes.inspect
18
+ raise ArgumentError.new("The #{key.inspect} attribute is required")
19
+ end
20
+ v
21
+ end
22
+
23
+ def fetch(key, type, options = {})
24
+ v = @attributes[key]
25
+
26
+ if blank?(v)
27
+ nil
28
+ else
29
+ case type
30
+ when :array
31
+ if !v.is_a?(Array)
32
+ raise ArgumentError.new("The #{key.inspect} attribute must be a list if provided")
33
+ end
34
+
35
+ v
36
+
37
+ when :float
38
+ v = v.to_f
39
+
40
+ if options[:precision]
41
+ v = v.round(options[:precision])
42
+ end
43
+
44
+ v
45
+
46
+ when :hash
47
+ if options[:sanitize]
48
+ v = Util::Hash.sanitize_keys(v, options[:sanitize])
49
+ end
50
+
51
+ v = Util::Hash.jsonify(v)
52
+
53
+ if v == {}
54
+ nil
55
+ else
56
+ v
57
+ end
58
+
59
+ when :integer
60
+ v.to_i
61
+
62
+ when :string
63
+ v = v.to_s
64
+
65
+ if options[:limit]
66
+ v = v.byteslice(0, options[:limit])
67
+ end
68
+
69
+ if options[:upcase]
70
+ v = v.upcase
71
+ end
72
+
73
+ v
74
+
75
+ when :symbol
76
+ v.to_sym
77
+
78
+ else
79
+ raise ArgumentError.new("Unknown normalization type #{type}")
80
+ end
81
+ end
82
+ end
83
+
84
+ private
85
+ def blank?(v)
86
+ v.nil? || (v.respond_to?(:length) && v.length == 0)
87
+ end
88
+ end
89
+ end
90
+ end