logstruct 0.1.11 → 0.2.0
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 +146 -1
- data/README.md +76 -68
- data/lib/log_struct/concerns/configuration.rb +139 -91
- data/lib/log_struct/concerns/error_handling.rb +36 -6
- data/lib/log_struct/config_struct/error_handling_modes.rb +3 -1
- data/lib/log_struct/config_struct/filters.rb +126 -22
- data/lib/log_struct/config_struct/integrations.rb +0 -4
- data/lib/log_struct/config_struct/limits.rb +29 -0
- data/lib/log_struct/configuration.rb +2 -0
- data/lib/log_struct/enums/event.rb +3 -1
- data/lib/log_struct/enums/log_field.rb +5 -0
- data/lib/log_struct/formatter.rb +304 -56
- data/lib/log_struct/hash_utils.rb +33 -4
- data/lib/log_struct/integrations/action_mailer/error_handling.rb +78 -20
- data/lib/log_struct/integrations/action_mailer/fallback_error_handlers.rb +81 -0
- data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +0 -20
- data/lib/log_struct/integrations/action_mailer.rb +22 -17
- data/lib/log_struct/integrations/active_job/log_subscriber.rb +177 -25
- data/lib/log_struct/integrations/active_job.rb +2 -7
- data/lib/log_struct/integrations/active_model_serializers.rb +9 -3
- data/lib/log_struct/integrations/active_record.rb +45 -17
- data/lib/log_struct/integrations/active_storage.rb +31 -31
- data/lib/log_struct/integrations/ahoy.rb +19 -15
- data/lib/log_struct/integrations/carrierwave.rb +55 -51
- data/lib/log_struct/integrations/dotenv.rb +22 -32
- data/lib/log_struct/integrations/good_job/logger.rb +30 -24
- data/lib/log_struct/integrations/good_job.rb +111 -40
- data/lib/log_struct/integrations/host_authorization.rb +47 -36
- data/lib/log_struct/integrations/lograge.rb +39 -4
- data/lib/log_struct/integrations/puma.rb +74 -34
- data/lib/log_struct/integrations/rack_error_handler/debug_exceptions_logging.rb +35 -0
- data/lib/log_struct/integrations/rack_error_handler/error_lines.rb +160 -0
- data/lib/log_struct/integrations/rack_error_handler/middleware.rb +14 -169
- data/lib/log_struct/integrations/rack_error_handler/parse_error_logging.rb +36 -0
- data/lib/log_struct/integrations/rack_error_handler.rb +20 -4
- data/lib/log_struct/integrations/rack_middleware.rb +23 -0
- data/lib/log_struct/integrations/rails_log_subscribers.rb +32 -0
- data/lib/log_struct/integrations/request_context/middleware.rb +4 -9
- data/lib/log_struct/integrations/shrine.rb +48 -73
- data/lib/log_struct/integrations/sidekiq/job_context_middleware.rb +32 -0
- data/lib/log_struct/integrations/sidekiq/logger.rb +17 -9
- data/lib/log_struct/integrations/sidekiq.rb +98 -7
- data/lib/log_struct/integrations/sorbet.rb +10 -46
- data/lib/log_struct/integrations.rb +3 -2
- data/lib/log_struct/log/{good_job/enqueue.rb → active_job/discard.rb} +13 -13
- data/lib/log_struct/log/active_job/enqueue.rb +0 -4
- data/lib/log_struct/log/{good_job/schedule.rb → active_job/error.rb} +15 -15
- data/lib/log_struct/log/{good_job/start.rb → active_job/retry.rb} +13 -13
- data/lib/log_struct/log/{good_job/finish.rb → active_job/retry_stopped.rb} +13 -17
- data/lib/log_struct/log/active_job.rb +4 -0
- data/lib/log_struct/log/active_storage/delete.rb +6 -2
- data/lib/log_struct/log/active_storage/download.rb +2 -4
- data/lib/log_struct/log/active_storage/exist.rb +2 -2
- data/lib/log_struct/log/active_storage/metadata.rb +4 -4
- data/lib/log_struct/log/active_storage/upload.rb +2 -10
- data/lib/log_struct/log/active_storage/url.rb +2 -6
- data/lib/log_struct/log/active_storage.rb +2 -3
- data/lib/log_struct/log/error.rb +24 -1
- data/lib/log_struct/log/good_job.rb +0 -4
- data/lib/log_struct/log/puma/error.rb +63 -0
- data/lib/log_struct/log/puma.rb +1 -0
- data/lib/log_struct/log/security/blocked_host.rb +0 -4
- data/lib/log_struct/log/shrine/exist.rb +0 -2
- data/lib/log_struct/log/shrine/metadata.rb +2 -4
- data/lib/log_struct/log/shrine/upload.rb +4 -0
- data/lib/log_struct/log/sidekiq/error.rb +63 -0
- data/lib/log_struct/log/{active_storage/stream.rb → sidekiq/log.rb} +17 -11
- data/lib/log_struct/log/sidekiq.rb +3 -45
- data/lib/log_struct/log.rb +4 -2
- data/lib/log_struct/logged_errors.rb +25 -0
- data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +2 -3
- data/lib/log_struct/param_filters.rb +51 -38
- data/lib/log_struct/params_errors.rb +78 -0
- data/lib/log_struct/rails_boot_banner_silencer.rb +7 -2
- data/lib/log_struct/railtie.rb +29 -14
- data/lib/log_struct/semantic_logger/color_formatter.rb +60 -53
- data/lib/log_struct/semantic_logger/concerns/log_methods.rb +65 -57
- data/lib/log_struct/semantic_logger/formatter.rb +79 -57
- data/lib/log_struct/semantic_logger/logger.rb +29 -53
- data/lib/log_struct/semantic_logger/reopen_after_fork.rb +40 -0
- data/lib/log_struct/semantic_logger/setup.rb +115 -112
- data/lib/log_struct/shared/merge_additional_data_fields.rb +14 -1
- data/lib/log_struct/shared/serialize_common.rb +2 -7
- data/lib/log_struct/string_scrubber.rb +229 -20
- data/lib/log_struct/version.rb +1 -1
- data/lib/log_struct.rb +6 -4
- data/logstruct.gemspec +19 -10
- metadata +63 -11
- data/lib/log_struct/builders/active_job.rb +0 -84
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +0 -179
- data/lib/log_struct/integrations/rack.rb +0 -31
data/lib/log_struct/log.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "enums/level"
|
|
|
8
8
|
require_relative "enums/log_field"
|
|
9
9
|
require_relative "log/interfaces/public_common_fields"
|
|
10
10
|
require_relative "shared/serialize_common_public"
|
|
11
|
+
require_relative "params_errors"
|
|
11
12
|
|
|
12
13
|
# Dynamically require all top-level log structs under log/*
|
|
13
14
|
# Nested per-event files are required by their parent files.
|
|
@@ -23,7 +24,7 @@ module LogStruct
|
|
|
23
24
|
sig do
|
|
24
25
|
params(
|
|
25
26
|
source: Source,
|
|
26
|
-
ex:
|
|
27
|
+
ex: Exception,
|
|
27
28
|
additional_data: T::Hash[T.any(String, Symbol), T.untyped],
|
|
28
29
|
timestamp: Time
|
|
29
30
|
).returns(LogStruct::Log::Error)
|
|
@@ -32,7 +33,8 @@ module LogStruct
|
|
|
32
33
|
LogStruct::Log::Error.new(
|
|
33
34
|
source: source,
|
|
34
35
|
error_class: ex.class,
|
|
35
|
-
|
|
36
|
+
# Without the input a params parser quoted, for a request's params error that the app logs itself
|
|
37
|
+
message: ParamsErrors.messages(ex).first,
|
|
36
38
|
backtrace: ex.backtrace,
|
|
37
39
|
additional_data: additional_data,
|
|
38
40
|
timestamp: timestamp
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module LogStruct
|
|
5
|
+
# Exceptions LogStruct has already logged as a structured line, so an integration that sees the same
|
|
6
|
+
# exception again doesn't log it twice. GoodJob passes a failed job's exception to on_thread_error after
|
|
7
|
+
# the ActiveJob integration has logged it, for example. Exceptions are held weakly, so a mark doesn't keep
|
|
8
|
+
# one alive.
|
|
9
|
+
module LoggedErrors
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
@logged = T.let(ObjectSpace::WeakMap.new, ObjectSpace::WeakMap)
|
|
13
|
+
@mutex = T.let(Mutex.new, Mutex)
|
|
14
|
+
|
|
15
|
+
sig { params(error: Exception).void }
|
|
16
|
+
def self.mark(error)
|
|
17
|
+
@mutex.synchronize { @logged[error] = true }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(error: Exception).returns(T::Boolean) }
|
|
21
|
+
def self.logged?(error)
|
|
22
|
+
@mutex.synchronize { @logged.key?(error) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -31,14 +31,13 @@ module ActiveSupport
|
|
|
31
31
|
# Preserve original Rails behavior when LogStruct is disabled
|
|
32
32
|
return super unless ::LogStruct.enabled?
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
tags = T.unsafe(self).respond_to?(:current_tags) ? current_tags : (Thread.current[:activesupport_tagged_logging_tags] || [])
|
|
34
|
+
tags = current_tags
|
|
36
35
|
|
|
37
36
|
# Add tags to data as hash entry (not text prefix)
|
|
38
37
|
data_with_tags = if data.is_a?(Hash)
|
|
39
38
|
tags.present? ? data.merge(tags: tags) : data
|
|
40
39
|
elsif data.is_a?(::LogStruct::Log::Interfaces::CommonFields) || (data.is_a?(T::Struct) && data.respond_to?(:serialize))
|
|
41
|
-
hash =
|
|
40
|
+
hash = data.serialize
|
|
42
41
|
tags.present? ? hash.merge(tags: tags) : hash
|
|
43
42
|
else
|
|
44
43
|
base = {LogStruct::LogField::Message.serialize => data.to_s}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "
|
|
4
|
+
require "json"
|
|
5
5
|
require_relative "hash_utils"
|
|
6
|
+
require_relative "string_scrubber"
|
|
6
7
|
require_relative "config_struct/filters"
|
|
7
8
|
require_relative "enums/source"
|
|
8
9
|
|
|
@@ -13,19 +14,47 @@ module LogStruct
|
|
|
13
14
|
class << self
|
|
14
15
|
extend T::Sig
|
|
15
16
|
|
|
16
|
-
# Check if a key should be filtered based on our defined sensitive keys
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# Check if a key should be filtered based on our defined sensitive keys.
|
|
18
|
+
# logstruct_field: the key is one of LogStruct's own fields at the top of a line.
|
|
19
|
+
# path: the keys from the line's top-level field down to this one (the key alone if not given).
|
|
20
|
+
# root: the top-level field's value, or the whole line for a top-level key.
|
|
21
|
+
sig do
|
|
22
|
+
params(
|
|
23
|
+
key: T.untyped,
|
|
24
|
+
value: T.untyped,
|
|
25
|
+
logstruct_field: T::Boolean,
|
|
26
|
+
path: T.nilable(T::Array[String]),
|
|
27
|
+
root: T.untyped
|
|
28
|
+
).returns(T::Boolean)
|
|
29
|
+
end
|
|
30
|
+
def should_filter_key?(key, value = nil, logstruct_field: false, path: nil, root: nil)
|
|
19
31
|
filters = LogStruct.config.filters
|
|
20
32
|
normalized_key = key.to_s
|
|
21
|
-
normalized_symbol = normalized_key.downcase.to_sym
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
words = KeyWords.normalize(normalized_key).to_sym
|
|
35
|
+
# By its words too, so :api_key filters apiKey and X-Api-Key. Keys in filter_keys_with_hashes are
|
|
36
|
+
# filtered too, with a hash of their value.
|
|
37
|
+
[filters.filter_keys, filters.filter_keys_with_hashes].each do |keys|
|
|
38
|
+
return true if keys.include?(normalized_key.downcase.to_sym) || keys.include?(words)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
matched = filters.filter_matchers.any? do |matcher|
|
|
42
|
+
next false if logstruct_field && !matcher.matches_logstruct_fields
|
|
24
43
|
|
|
25
|
-
filters.filter_matchers.any? do |matcher|
|
|
26
44
|
matcher.matches?(normalized_key, value)
|
|
27
45
|
rescue => e
|
|
28
|
-
handle_filter_matcher_error(e, matcher, normalized_key)
|
|
46
|
+
handle_filter_matcher_error(e, matcher.label, normalized_key)
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
return true if matched
|
|
50
|
+
return false if filters.path_matchers.empty?
|
|
51
|
+
|
|
52
|
+
path ||= [normalized_key]
|
|
53
|
+
root = {} if root.nil?
|
|
54
|
+
filters.path_matchers.any? do |matcher|
|
|
55
|
+
matcher.matches?(path, value, root)
|
|
56
|
+
rescue => e
|
|
57
|
+
handle_filter_matcher_error(e, matcher.label, normalized_key)
|
|
29
58
|
false
|
|
30
59
|
end
|
|
31
60
|
end
|
|
@@ -33,7 +62,8 @@ module LogStruct
|
|
|
33
62
|
# Check if a key should be hashed rather than completely filtered
|
|
34
63
|
sig { params(key: T.untyped).returns(T::Boolean) }
|
|
35
64
|
def should_include_string_hash?(key)
|
|
36
|
-
LogStruct.config.filters.filter_keys_with_hashes
|
|
65
|
+
keys = LogStruct.config.filters.filter_keys_with_hashes
|
|
66
|
+
keys.include?(key.to_s.downcase.to_sym) || keys.include?(KeyWords.normalize(key.to_s).to_sym)
|
|
37
67
|
end
|
|
38
68
|
|
|
39
69
|
# Convert a value to a filtered summary hash (e.g. { _filtered: { class: "String", ... }})
|
|
@@ -51,56 +81,39 @@ module LogStruct
|
|
|
51
81
|
end
|
|
52
82
|
end
|
|
53
83
|
|
|
54
|
-
# Summarize a String
|
|
84
|
+
# Summarize a filtered String: its class, and an HMAC of it for keys in filter_keys_with_hashes.
|
|
85
|
+
# No length, which would say how long a password or token is.
|
|
55
86
|
sig { params(string: String, include_hash: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
|
56
87
|
def summarize_string(string, include_hash)
|
|
57
88
|
filtered_string = {
|
|
58
89
|
_class: String
|
|
59
90
|
}
|
|
60
|
-
if include_hash
|
|
61
|
-
filtered_string[:_hash] = HashUtils.hash_value(string)
|
|
62
|
-
else
|
|
63
|
-
filtered_string[:_bytes] = string.bytesize
|
|
64
|
-
end
|
|
91
|
+
filtered_string[:_hash] = StringScrubber.value_hash(string) if include_hash
|
|
65
92
|
|
|
66
93
|
filtered_string
|
|
67
94
|
end
|
|
68
95
|
|
|
69
|
-
# Summarize a Hash
|
|
96
|
+
# Summarize a filtered Hash: its first 10 keys and how many there are. No size, which would say
|
|
97
|
+
# how long the secrets in it are.
|
|
70
98
|
sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
71
99
|
def summarize_hash(hash)
|
|
72
100
|
return {_class: "Hash", _empty: true} if hash.empty?
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
has_sensitive_keys = T.let(false, T::Boolean)
|
|
76
|
-
normalized_keys = []
|
|
77
|
-
|
|
78
|
-
hash.each do |key, value|
|
|
79
|
-
has_sensitive_keys ||= should_filter_key?(key, value)
|
|
80
|
-
normalized_keys << normalize_summary_key(key)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
summary = {
|
|
102
|
+
{
|
|
84
103
|
_class: Hash,
|
|
85
104
|
_keys_count: hash.keys.size,
|
|
86
|
-
_keys:
|
|
105
|
+
_keys: hash.keys.take(10).map { |key| normalize_summary_key(StringScrubber.scrub_name(key)) }
|
|
87
106
|
}
|
|
88
|
-
|
|
89
|
-
# Only add byte size if no sensitive keys are present
|
|
90
|
-
summary[:_bytes] = hash.to_json.bytesize unless has_sensitive_keys
|
|
91
|
-
|
|
92
|
-
summary
|
|
93
107
|
end
|
|
94
108
|
|
|
95
|
-
# Summarize
|
|
109
|
+
# Summarize a filtered Array: how many items it has
|
|
96
110
|
sig { params(array: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
97
111
|
def summarize_array(array)
|
|
98
112
|
return {_class: "Array", _empty: true} if array.empty?
|
|
99
113
|
|
|
100
114
|
{
|
|
101
115
|
_class: Array,
|
|
102
|
-
_count: array.size
|
|
103
|
-
_bytes: array.to_json.bytesize
|
|
116
|
+
_count: array.size
|
|
104
117
|
}
|
|
105
118
|
end
|
|
106
119
|
|
|
@@ -119,10 +132,10 @@ module LogStruct
|
|
|
119
132
|
key.to_s
|
|
120
133
|
end
|
|
121
134
|
|
|
122
|
-
sig { params(error: StandardError,
|
|
123
|
-
def handle_filter_matcher_error(error,
|
|
135
|
+
sig { params(error: StandardError, label: String, key: String).void }
|
|
136
|
+
def handle_filter_matcher_error(error, label, key)
|
|
124
137
|
context = {
|
|
125
|
-
matcher:
|
|
138
|
+
matcher: label,
|
|
126
139
|
key: key
|
|
127
140
|
}
|
|
128
141
|
LogStruct.handle_exception(error, source: Source::Internal, context: context)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module LogStruct
|
|
5
|
+
# Errors from parsing a request's params: Rails' ParseError (and its ParamErrors), and Rack's. Their messages, and
|
|
6
|
+
# those of errors raised because of them (Rails' "Invalid request parameters: ..."), quote the request's input: a
|
|
7
|
+
# JSON parser's excerpt of the body, Rack's "invalid %-encoding (<value>)". Nothing can filter a body that didn't
|
|
8
|
+
# parse, so LogStruct's lines leave the input out.
|
|
9
|
+
module ParamsErrors
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
# Matched by name, so this works without ActionDispatch or Rack loaded
|
|
13
|
+
NAMES = T.let(
|
|
14
|
+
[
|
|
15
|
+
"ActionDispatch::Http::Parameters::ParseError",
|
|
16
|
+
"Rack::BadRequest",
|
|
17
|
+
"Rack::QueryParser::InvalidParameterError",
|
|
18
|
+
"Rack::QueryParser::ParameterTypeError",
|
|
19
|
+
"Rack::QueryParser::ParamsTooDeepError"
|
|
20
|
+
].freeze,
|
|
21
|
+
T::Array[String]
|
|
22
|
+
)
|
|
23
|
+
# Rails' message for a ParseError
|
|
24
|
+
PARSE_ERROR_MESSAGE = "Error occurred while parsing request parameters"
|
|
25
|
+
PARAMS_ERROR_MESSAGE = "Invalid request parameters"
|
|
26
|
+
CAUSE_MESSAGE_LEFT_OUT = "(left out: it quotes the request's input)"
|
|
27
|
+
|
|
28
|
+
# The message and the cause's message to log for an error, without the input a params parser quoted
|
|
29
|
+
sig { params(error: Exception).returns([String, T.nilable(String)]) }
|
|
30
|
+
def self.messages(error)
|
|
31
|
+
cause = error.cause
|
|
32
|
+
if params_error?(error)
|
|
33
|
+
[parse_error?(error) ? PARSE_ERROR_MESSAGE : PARAMS_ERROR_MESSAGE, nil]
|
|
34
|
+
elsif cause && params_error?(cause)
|
|
35
|
+
message = error.message.sub(cause.message, "").sub(/[\s:]+\z/, "")
|
|
36
|
+
[message.empty? ? PARAMS_ERROR_MESSAGE : message, nil]
|
|
37
|
+
else
|
|
38
|
+
[error.message, cause&.message]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# For text that writes an error's whole chain (its causes too): each message in it that quotes a params parser's
|
|
43
|
+
# input, with what to write instead. Longest first, since an error's message can contain its cause's. An error
|
|
44
|
+
# raised without a message (Rack's and Rails' depth limits) has its class name as its message, which quotes
|
|
45
|
+
# nothing and is also the text's label for its class, so it stays.
|
|
46
|
+
sig { params(error: Exception).returns(T::Array[[String, String]]) }
|
|
47
|
+
def self.replacements(error)
|
|
48
|
+
pairs = T.let([], T::Array[[String, String]])
|
|
49
|
+
seen = T.let({}.compare_by_identity, T::Hash[Exception, TrueClass])
|
|
50
|
+
current = T.let(error, T.nilable(Exception))
|
|
51
|
+
while current && !seen.key?(current)
|
|
52
|
+
seen[current] = true
|
|
53
|
+
message, cause_message = messages(current)
|
|
54
|
+
pairs << [current.message, message] if message != current.message && !default_message?(current)
|
|
55
|
+
cause = current.cause
|
|
56
|
+
pairs << [cause.message, CAUSE_MESSAGE_LEFT_OUT] if cause && cause_message.nil? && !default_message?(cause)
|
|
57
|
+
current = cause
|
|
58
|
+
end
|
|
59
|
+
pairs.reject { |quoted, _| quoted.empty? }.uniq.sort_by { |quoted, _| -quoted.length }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(error: Exception).returns(T::Boolean) }
|
|
63
|
+
def self.default_message?(error)
|
|
64
|
+
error.message == error.class.name
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(error: Exception).returns(T::Boolean) }
|
|
68
|
+
def self.parse_error?(error)
|
|
69
|
+
!!defined?(::ActionDispatch::Http::Parameters::ParseError) &&
|
|
70
|
+
error.instance_of?(::ActionDispatch::Http::Parameters::ParseError)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { params(error: Exception).returns(T::Boolean) }
|
|
74
|
+
def self.params_error?(error)
|
|
75
|
+
error.class.ancestors.any? { |ancestor| NAMES.include?(ancestor.name) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -15,6 +15,9 @@ module LogStruct
|
|
|
15
15
|
@installed = true
|
|
16
16
|
|
|
17
17
|
return unless ARGV.include?("server")
|
|
18
|
+
# Rails' own banner stays when LogStruct is disabled
|
|
19
|
+
return unless LogStruct.enabled?
|
|
20
|
+
|
|
18
21
|
patch!
|
|
19
22
|
end
|
|
20
23
|
|
|
@@ -51,6 +54,8 @@ module LogStruct
|
|
|
51
54
|
|
|
52
55
|
module ServerCommandSilencer
|
|
53
56
|
extend T::Sig
|
|
57
|
+
extend T::Helpers
|
|
58
|
+
requires_ancestor { Object }
|
|
54
59
|
|
|
55
60
|
sig { params(args: T.untyped, block: T.nilable(T.proc.returns(T.untyped))).returns(T.untyped) }
|
|
56
61
|
def perform(*args, &block)
|
|
@@ -105,9 +110,9 @@ module LogStruct
|
|
|
105
110
|
|
|
106
111
|
sig { returns(String) }
|
|
107
112
|
def lookup_executable
|
|
108
|
-
return "rails" unless
|
|
113
|
+
return "rails" unless respond_to?(:executable, true)
|
|
109
114
|
|
|
110
|
-
T.cast(
|
|
115
|
+
T.cast(send(:executable), String)
|
|
111
116
|
rescue
|
|
112
117
|
"rails"
|
|
113
118
|
end
|
data/lib/log_struct/railtie.rb
CHANGED
|
@@ -10,31 +10,49 @@ require_relative "integrations"
|
|
|
10
10
|
module LogStruct
|
|
11
11
|
# Railtie to integrate with Rails
|
|
12
12
|
class Railtie < ::Rails::Railtie
|
|
13
|
-
#
|
|
13
|
+
# Replace the logger as soon as Rails creates it, so boot-time logs and anything that keeps
|
|
14
|
+
# a reference to Rails.logger use LogStruct's logger
|
|
14
15
|
initializer "logstruct.configure_logger", after: :initialize_logger do |app|
|
|
15
16
|
next unless LogStruct.enabled?
|
|
16
17
|
|
|
17
18
|
# Use SemanticLogger for powerful logging features
|
|
18
19
|
LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
|
|
20
|
+
|
|
21
|
+
# Initializers that configure Sidekiq log with its logger while config/initializers run
|
|
22
|
+
Integrations::Sidekiq.configure_logger if LogStruct.config.integrations.enable_sidekiq
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
#
|
|
22
|
-
|
|
25
|
+
# Set up integrations after config/initializers, so that the settings in
|
|
26
|
+
# config/initializers/logstruct.rb and filter_parameters added by other initializers apply
|
|
27
|
+
initializer "logstruct.setup", after: :load_config_initializers, before: :build_middleware_stack do |app|
|
|
28
|
+
# An initializer may have changed `enabled` or `enabled_environments` after the logger was
|
|
29
|
+
# (or wasn't) replaced
|
|
30
|
+
LogStruct.apply_initializer_settings!
|
|
31
|
+
if LogStruct.enabled?
|
|
32
|
+
if LogStruct::SemanticLogger::Setup.configured?
|
|
33
|
+
# The formatter, colors, noisy logger filter and log file settings in config/initializers apply too
|
|
34
|
+
LogStruct::SemanticLogger::Setup.reconfigure_appenders(app)
|
|
35
|
+
else
|
|
36
|
+
LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
|
|
37
|
+
end
|
|
38
|
+
elsif LogStruct::SemanticLogger::Setup.configured?
|
|
39
|
+
LogStruct::SemanticLogger::Setup.restore_rails_logger(app)
|
|
40
|
+
end
|
|
41
|
+
unless LogStruct.enabled? && LogStruct.config.integrations.enable_sidekiq
|
|
42
|
+
Integrations::Sidekiq.restore_logger
|
|
43
|
+
end
|
|
44
|
+
Integrations::ActionMailer.setup_fallback_error_handlers(LogStruct.config)
|
|
45
|
+
|
|
23
46
|
next unless LogStruct.enabled?
|
|
24
47
|
|
|
25
48
|
# Merge Rails filter parameters into our filters
|
|
26
49
|
LogStruct.merge_rails_filter_parameters!
|
|
27
50
|
|
|
28
|
-
# Set up non-middleware integrations first
|
|
29
51
|
Integrations.setup_integrations(stage: :non_middleware)
|
|
30
|
-
|
|
31
|
-
# Note: Host allowances are managed by the test app itself.
|
|
32
52
|
end
|
|
33
53
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
initializer "logstruct.configure_middleware", before: :build_middleware_stack do |app|
|
|
37
|
-
# This runs before middleware stack is frozen, so we can configure it
|
|
54
|
+
# Add middleware before the middleware stack is built
|
|
55
|
+
initializer "logstruct.configure_middleware", after: "logstruct.setup", before: :build_middleware_stack do |app|
|
|
38
56
|
next unless LogStruct.enabled?
|
|
39
57
|
|
|
40
58
|
Integrations.setup_integrations(stage: :middleware)
|
|
@@ -42,6 +60,7 @@ module LogStruct
|
|
|
42
60
|
|
|
43
61
|
# Emit Puma lifecycle logs when running `rails server`
|
|
44
62
|
initializer "logstruct.puma_lifecycle", after: "logstruct.configure_logger" do
|
|
63
|
+
next unless LogStruct.enabled?
|
|
45
64
|
is_server = ::LogStruct.server_mode?
|
|
46
65
|
next unless is_server
|
|
47
66
|
begin
|
|
@@ -53,10 +72,6 @@ module LogStruct
|
|
|
53
72
|
process_id: Process.pid,
|
|
54
73
|
listening_addresses: port ? ["tcp://127.0.0.1:#{port}"] : nil
|
|
55
74
|
)
|
|
56
|
-
begin
|
|
57
|
-
warn("[logstruct] puma lifecycle init")
|
|
58
|
-
rescue
|
|
59
|
-
end
|
|
60
75
|
LogStruct.info(started)
|
|
61
76
|
|
|
62
77
|
at_exit do
|
|
@@ -6,46 +6,12 @@ require_relative "formatter"
|
|
|
6
6
|
|
|
7
7
|
module LogStruct
|
|
8
8
|
module SemanticLogger
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# ## Benefits of Colorized Output:
|
|
16
|
-
#
|
|
17
|
-
# ### Readability
|
|
18
|
-
# - **Syntax highlighting**: JSON keys, values, and data types are color-coded
|
|
19
|
-
# - **Visual hierarchy**: Different colors help identify structure at a glance
|
|
20
|
-
# - **Error spotting**: Quickly identify malformed data or unexpected values
|
|
21
|
-
# - **Context separation**: Log entries are visually distinct from each other
|
|
22
|
-
#
|
|
23
|
-
# ### Performance in Development
|
|
24
|
-
# - **Faster debugging**: Quickly scan logs without reading every character
|
|
25
|
-
# - **Pattern recognition**: Colors help identify common log patterns
|
|
26
|
-
# - **Reduced cognitive load**: Less mental effort required to parse log output
|
|
27
|
-
# - **Improved workflow**: Spend less time reading logs, more time coding
|
|
28
|
-
#
|
|
29
|
-
# ### Customization
|
|
30
|
-
# - **Configurable colors**: Customize colors for keys, strings, numbers, etc.
|
|
31
|
-
# - **Environment-aware**: Automatically disabled in production/CI environments
|
|
32
|
-
# - **Fallback support**: Gracefully falls back to standard formatting if needed
|
|
33
|
-
#
|
|
34
|
-
# ## Color Mapping:
|
|
35
|
-
# - **Keys**: Yellow - Easy to spot field names
|
|
36
|
-
# - **Strings**: Green - Clear indication of text values
|
|
37
|
-
# - **Numbers**: Blue - Numeric values stand out
|
|
38
|
-
# - **Booleans**: Magenta - true/false values are distinctive
|
|
39
|
-
# - **Null**: Red - Missing values are immediately visible
|
|
40
|
-
# - **Logger names**: Cyan - Source identification
|
|
41
|
-
#
|
|
42
|
-
# ## Integration with SemanticLogger:
|
|
43
|
-
# This formatter preserves all SemanticLogger benefits (performance, threading,
|
|
44
|
-
# reliability) while adding visual enhancements. It processes LogStruct types,
|
|
45
|
-
# hashes, and plain messages with appropriate colorization.
|
|
46
|
-
#
|
|
47
|
-
# The formatter is automatically enabled in development when `enable_color_output`
|
|
48
|
-
# is true (default), providing zero-configuration enhanced logging experience.
|
|
9
|
+
# Writes lines as pretty-printed JSON in color, for reading in a terminal: keys yellow, strings green, numbers
|
|
10
|
+
# blue, booleans magenta, nil red and logger names cyan (config.color_map changes them). LogStruct uses it in
|
|
11
|
+
# development when LogStruct is enabled there, prefer_json_in_development is false and enable_color_output is
|
|
12
|
+
# true (its default). LogStruct entries and Hashes are the JSON line, pretty-printed. Plain lines keep
|
|
13
|
+
# SemanticLogger's layout, with their message, exception and tags scrubbed and their named tags filtered as a
|
|
14
|
+
# JSON line's ctx is.
|
|
49
15
|
class ColorFormatter < ::SemanticLogger::Formatters::Color
|
|
50
16
|
extend T::Sig
|
|
51
17
|
|
|
@@ -53,19 +19,17 @@ module LogStruct
|
|
|
53
19
|
def initialize(color_map: nil, **args)
|
|
54
20
|
super(**args)
|
|
55
21
|
@logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
|
|
22
|
+
@json_formatter = T.let(LogStruct::SemanticLogger::Formatter.new, LogStruct::SemanticLogger::Formatter)
|
|
56
23
|
|
|
57
24
|
# Set up custom color mapping
|
|
58
|
-
@custom_colors = T.let(color_map ||
|
|
25
|
+
@custom_colors = T.let(default_color_map.merge(color_map || {}), T::Hash[Symbol, Symbol])
|
|
59
26
|
end
|
|
60
27
|
|
|
61
28
|
sig { override.params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
|
|
62
29
|
def call(log, logger)
|
|
63
30
|
# Handle LogStruct types specially with colorization
|
|
64
|
-
if log.payload.is_a?(
|
|
65
|
-
formatted = format_logstruct_payload(log)
|
|
66
|
-
formatted if formatted
|
|
67
|
-
elsif log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
|
|
68
|
-
formatted = format_logstruct_payload(log)
|
|
31
|
+
if log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
|
|
32
|
+
formatted = format_logstruct_payload(log, logger)
|
|
69
33
|
formatted if formatted
|
|
70
34
|
else
|
|
71
35
|
# For plain messages, use SemanticLogger's default colorization
|
|
@@ -73,8 +37,51 @@ module LogStruct
|
|
|
73
37
|
end
|
|
74
38
|
end
|
|
75
39
|
|
|
40
|
+
# The parts of a plain line that hold what was logged, scrubbed like the strings in a JSON line
|
|
41
|
+
sig { returns(T.untyped) }
|
|
42
|
+
def message
|
|
43
|
+
scrub_text(super)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { returns(T.untyped) }
|
|
47
|
+
def payload
|
|
48
|
+
scrub_text(super)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# SemanticLogger's text for the exception and its causes, without the input a params parser quoted
|
|
52
|
+
sig { returns(T.untyped) }
|
|
53
|
+
def exception
|
|
54
|
+
text = super
|
|
55
|
+
error = log.exception
|
|
56
|
+
if text.is_a?(String) && error.is_a?(Exception)
|
|
57
|
+
ParamsErrors.replacements(error).each { |quoted, logged| text = text.gsub(quoted, logged) }
|
|
58
|
+
end
|
|
59
|
+
scrub_text(text)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { returns(T.untyped) }
|
|
63
|
+
def tags
|
|
64
|
+
scrub_text(super)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Filtered and scrubbed as they are in a JSON line's ctx: a key filtering hides there is hidden here too
|
|
68
|
+
sig { returns(T.nilable(String)) }
|
|
69
|
+
def named_tags
|
|
70
|
+
named_tags = log.named_tags
|
|
71
|
+
return if named_tags.nil? || named_tags.empty?
|
|
72
|
+
|
|
73
|
+
filtered = @logstruct_formatter.process_values({ctx: named_tags.to_h.transform_keys(&:to_sym)})[:ctx]
|
|
74
|
+
list = filtered.map { |name, value| "#{color}#{name}: #{value}#{color_map.clear}" }
|
|
75
|
+
"{#{list.join(", ")}}"
|
|
76
|
+
end
|
|
77
|
+
|
|
76
78
|
private
|
|
77
79
|
|
|
80
|
+
sig { params(text: T.untyped).returns(T.untyped) }
|
|
81
|
+
def scrub_text(text)
|
|
82
|
+
text.is_a?(String) ? StringScrubber.scrub(text) : text
|
|
83
|
+
end
|
|
84
|
+
|
|
78
85
|
sig { returns(LogStruct::Formatter) }
|
|
79
86
|
attr_reader :logstruct_formatter
|
|
80
87
|
|
|
@@ -91,11 +98,9 @@ module LogStruct
|
|
|
91
98
|
}
|
|
92
99
|
end
|
|
93
100
|
|
|
94
|
-
#
|
|
101
|
+
# The line as pretty-printed JSON, with ANSI colors for its keys, strings, numbers, booleans and nulls
|
|
95
102
|
sig { params(data: T::Hash[String, T.untyped]).returns(String) }
|
|
96
103
|
def colorize_json(data)
|
|
97
|
-
# For now, just return a simple colorized version of the JSON
|
|
98
|
-
# This is much simpler than the full recursive approach
|
|
99
104
|
json_str = JSON.pretty_generate(data)
|
|
100
105
|
|
|
101
106
|
# Apply basic colorization with regex
|
|
@@ -106,9 +111,10 @@ module LogStruct
|
|
|
106
111
|
.gsub(": null", ": " + colorize_text("null", :nil))
|
|
107
112
|
end
|
|
108
113
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
# The line LogStruct::SemanticLogger::Formatter writes (message, tags and ctx included), in color
|
|
115
|
+
sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(T.nilable(String)) }
|
|
116
|
+
def format_logstruct_payload(log, logger)
|
|
117
|
+
logstruct_json = @json_formatter.call(log, logger)
|
|
112
118
|
|
|
113
119
|
parsed_data = T.let(JSON.parse(logstruct_json), T::Hash[String, T.untyped])
|
|
114
120
|
colorized_json = colorize_json(parsed_data)
|
|
@@ -119,7 +125,8 @@ module LogStruct
|
|
|
119
125
|
process: log.process_info,
|
|
120
126
|
name: format_name(log.name))
|
|
121
127
|
|
|
122
|
-
|
|
128
|
+
# SemanticLogger's appenders add the newline
|
|
129
|
+
"#{prefix}#{colorized_json}"
|
|
123
130
|
rescue JSON::ParserError
|
|
124
131
|
nil
|
|
125
132
|
end
|