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