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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -1
- data/lib/timber/config.rb +2 -1
- data/lib/timber/contexts/custom.rb +10 -3
- data/lib/timber/contexts/http.rb +23 -7
- data/lib/timber/contexts/organization.rb +14 -3
- data/lib/timber/contexts/release.rb +17 -4
- data/lib/timber/contexts/runtime.rb +28 -9
- data/lib/timber/contexts/session.rb +11 -2
- data/lib/timber/contexts/system.rb +13 -3
- data/lib/timber/contexts/user.rb +22 -6
- data/lib/timber/events/controller_call.rb +14 -24
- data/lib/timber/events/custom.rb +8 -5
- data/lib/timber/events/error.rb +11 -31
- data/lib/timber/events/http_request.rb +39 -13
- data/lib/timber/events/http_response.rb +32 -14
- data/lib/timber/events/sql_query.rb +10 -7
- data/lib/timber/events/template_render.rb +11 -5
- data/lib/timber/log_entry.rb +7 -31
- data/lib/timber/logger.rb +1 -5
- data/lib/timber/util.rb +2 -2
- data/lib/timber/util/attribute_normalizer.rb +90 -0
- data/lib/timber/util/hash.rb +51 -1
- data/lib/timber/util/non_nil_hash_builder.rb +38 -0
- data/lib/timber/version.rb +1 -1
- data/spec/timber/events/error_spec.rb +8 -22
- data/spec/timber/events/http_request_spec.rb +1 -1
- data/spec/timber/events_spec.rb +1 -1
- data/spec/timber/integrations/action_dispatch/debug_exceptions_spec.rb +1 -1
- data/spec/timber/log_entry_spec.rb +0 -39
- data/spec/timber/logger_spec.rb +0 -5
- data/spec/timber/util/attribute_normalizer_spec.rb +90 -0
- metadata +8 -8
- data/lib/timber/util/http_event.rb +0 -69
- data/lib/timber/util/object.rb +0 -15
- data/spec/timber/util/http_event_spec.rb +0 -15
data/lib/timber/events/error.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
@error_message =
|
19
|
-
@
|
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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@
|
23
|
-
@
|
24
|
-
@
|
25
|
-
@
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
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 =
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@time_ms =
|
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
|
-
|
27
|
-
|
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
|
61
|
+
message << ", #{content_length} bytes, "
|
44
62
|
end
|
45
63
|
|
46
|
-
message
|
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
|
69
|
+
message << ", #{content_length} bytes, "
|
52
70
|
end
|
53
71
|
|
54
|
-
message
|
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
|
-
|
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
|
-
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@time_ms =
|
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
|
-
|
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
|
-
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@time_ms =
|
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
|
-
|
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
|
|
data/lib/timber/log_entry.rb
CHANGED
@@ -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
|
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
|
-
|
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/
|
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
|