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
@@ -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