logstruct 0.1.12 → 0.2.1
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 +139 -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 +116 -33
- 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 +7 -1
- data/lib/log_struct/integrations/active_record.rb +39 -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 +80 -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/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 -87
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +0 -189
- data/lib/log_struct/integrations/rack.rb +0 -31
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "active_support/parameter_filter"
|
|
5
|
+
require "active_support/core_ext/object/duplicable"
|
|
4
6
|
require_relative "../configuration"
|
|
5
7
|
|
|
6
8
|
module LogStruct
|
|
7
9
|
module Concerns
|
|
8
|
-
#
|
|
10
|
+
# LogStruct.configure, whether LogStruct is on in this process, and merging Rails' filter_parameters
|
|
9
11
|
module Configuration
|
|
10
12
|
module ClassMethods
|
|
11
13
|
extend T::Sig
|
|
12
14
|
|
|
13
15
|
CONSOLE_COMMAND_ARGS = T.let(["console", "c"].freeze, T::Array[String])
|
|
14
16
|
EMPTY_ARGV = T.let([].freeze, T::Array[String])
|
|
15
|
-
|
|
17
|
+
RAILS_PATTERNS_LABEL = "Rails filter_parameters"
|
|
18
|
+
RAILS_REGEXPS_LABEL = "Rails filter_parameters (Regexps)"
|
|
19
|
+
RAILS_PATHS_LABEL = "Rails filter_parameters (paths)"
|
|
20
|
+
RAILS_PATTERN_LABELS = T.let([RAILS_PATTERNS_LABEL, RAILS_REGEXPS_LABEL].freeze, T::Array[String])
|
|
21
|
+
RAILS_FILTER_MASK = "[FILTERED]"
|
|
22
|
+
RAILS_FILTER_PROBE = "value"
|
|
16
23
|
|
|
17
24
|
sig { params(block: T.proc.params(config: LogStruct::Configuration).void).void }
|
|
18
25
|
def configure(&block)
|
|
@@ -43,26 +50,34 @@ module LogStruct
|
|
|
43
50
|
|
|
44
51
|
sig { void }
|
|
45
52
|
def set_enabled_from_rails_env!
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# 2. Otherwise, check if current Rails environment is in enabled_environments
|
|
52
|
-
# AND one of: Rails::Server is defined, OR test environment with CI=true
|
|
53
|
-
# BUT NOT Rails::Console (to exclude interactive console)
|
|
54
|
-
# 3. Otherwise, leave as config.enabled (defaults to true)
|
|
55
|
-
|
|
56
|
-
# Then check if LOGSTRUCT_ENABLED env var is set
|
|
57
|
-
config.enabled = if ENV["LOGSTRUCT_ENABLED"]
|
|
58
|
-
%w[true t yes y 1].include?(ENV["LOGSTRUCT_ENABLED"]&.strip&.downcase)
|
|
59
|
-
else
|
|
53
|
+
# 1. When LOGSTRUCT_ENABLED is set, it decides: on for true, t, yes, y or 1, off for anything else.
|
|
54
|
+
# 2. Otherwise LogStruct is on when the Rails environment is in enabled_environments, the process
|
|
55
|
+
# isn't a console, and it's a server (server_process?) or runs in the test environment.
|
|
56
|
+
env_var_enabled = enabled_from_env_var
|
|
57
|
+
config.enabled = if env_var_enabled.nil?
|
|
60
58
|
is_console = console_process?
|
|
61
59
|
is_server = server_process?
|
|
62
|
-
ci_build?
|
|
63
60
|
in_enabled_env = config.enabled_environments.include?(::Rails.env.to_sym)
|
|
64
61
|
|
|
65
62
|
in_enabled_env && !is_console && (is_server || ::Rails.env.test?)
|
|
63
|
+
else
|
|
64
|
+
env_var_enabled
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# apply_initializer_settings! compares the settings with these
|
|
68
|
+
@enabled_from_rails_env = T.let(config.enabled, T.nilable(T::Boolean))
|
|
69
|
+
@enabled_environments_from_rails_env = T.let(config.enabled_environments.dup, T.nilable(T::Array[Symbol]))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# After config/initializers run: LOGSTRUCT_ENABLED takes precedence, then `config.enabled`
|
|
73
|
+
# if an initializer changed it, then `config.enabled_environments` if an initializer changed that
|
|
74
|
+
sig { void }
|
|
75
|
+
def apply_initializer_settings!
|
|
76
|
+
env_var_enabled = enabled_from_env_var
|
|
77
|
+
if !env_var_enabled.nil?
|
|
78
|
+
config.enabled = env_var_enabled
|
|
79
|
+
elsif config.enabled == @enabled_from_rails_env && config.enabled_environments != @enabled_environments_from_rails_env
|
|
80
|
+
set_enabled_from_rails_env!
|
|
66
81
|
end
|
|
67
82
|
end
|
|
68
83
|
|
|
@@ -84,44 +99,50 @@ module LogStruct
|
|
|
84
99
|
return unless rails_filter_params.is_a?(Array)
|
|
85
100
|
return if rails_filter_params.empty?
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
blocks, patterns = rails_filter_params.partition { |entry| callable_filter?(entry) }
|
|
103
|
+
# Rails matches entries with a dot against a key's path (user.password), and the rest against the key
|
|
104
|
+
path_patterns, key_patterns = patterns.partition { |entry| path_pattern?(entry) }
|
|
105
|
+
regexps, partial_names = key_patterns.partition { |entry| entry.is_a?(Regexp) }
|
|
106
|
+
# Rails gives blocks with any arity but 2 the parameters as a third argument
|
|
107
|
+
path_blocks, key_blocks = blocks.partition { |block| arity_for_filter(block) == 3 }
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
normalized_symbol = normalize_filter_symbol(entry)
|
|
100
|
-
if normalized_symbol
|
|
101
|
-
symbol_filters << normalized_symbol
|
|
102
|
-
else
|
|
103
|
-
leftovers << entry
|
|
104
|
-
end
|
|
109
|
+
matchers = key_blocks.map { |block| build_callable_filter_matcher(block) }
|
|
110
|
+
if partial_names.any?
|
|
111
|
+
matchers << build_rails_pattern_matcher(partial_names, label: RAILS_PATTERNS_LABEL, matches_logstruct_fields: false)
|
|
105
112
|
end
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
config.filters.filter_keys |= symbol_filters
|
|
113
|
+
if regexps.any?
|
|
114
|
+
matchers << build_rails_pattern_matcher(regexps, label: RAILS_REGEXPS_LABEL, matches_logstruct_fields: true)
|
|
109
115
|
end
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
path_matchers = path_blocks.map { |block| build_path_block_matcher(block) }
|
|
117
|
+
path_matchers << build_rails_path_matcher(path_patterns) if path_patterns.any?
|
|
118
|
+
|
|
119
|
+
# Exact names also filter LogStruct's own fields, which the partial matches skip
|
|
120
|
+
names = key_patterns.filter_map { |entry| entry.to_s.downcase.to_sym if entry.is_a?(Symbol) || entry.is_a?(String) }
|
|
121
|
+
config.filters.filter_keys |= names
|
|
122
|
+
|
|
123
|
+
# The latest list replaces the patterns merged before
|
|
124
|
+
registered = config.filters.filter_matchers
|
|
125
|
+
registered.reject! { |matcher| RAILS_PATTERN_LABELS.include?(matcher.label) }
|
|
126
|
+
matchers.each do |matcher|
|
|
127
|
+
registered << matcher unless registered.any? { |existing| existing.label == matcher.label }
|
|
128
|
+
end
|
|
129
|
+
registered_paths = config.filters.path_matchers
|
|
130
|
+
registered_paths.reject! { |matcher| matcher.label == RAILS_PATHS_LABEL }
|
|
131
|
+
path_matchers.each do |matcher|
|
|
132
|
+
registered_paths << matcher unless registered_paths.any? { |existing| existing.label == matcher.label }
|
|
118
133
|
end
|
|
119
|
-
|
|
120
|
-
replace_filter_parameters(rails_filter_params, leftovers)
|
|
121
134
|
end
|
|
122
135
|
|
|
123
136
|
private
|
|
124
137
|
|
|
138
|
+
sig { returns(T.nilable(T::Boolean)) }
|
|
139
|
+
def enabled_from_env_var
|
|
140
|
+
value = ENV["LOGSTRUCT_ENABLED"]
|
|
141
|
+
return nil if value.nil?
|
|
142
|
+
|
|
143
|
+
%w[true t yes y 1].include?(value.strip.downcase)
|
|
144
|
+
end
|
|
145
|
+
|
|
125
146
|
sig { returns(T::Boolean) }
|
|
126
147
|
def console_process?
|
|
127
148
|
return true if defined?(::Rails::Console)
|
|
@@ -138,6 +159,7 @@ module LogStruct
|
|
|
138
159
|
return true if defined?(::Falcon::Server)
|
|
139
160
|
return true if defined?(::Rails::Server)
|
|
140
161
|
return true if sidekiq_server?
|
|
162
|
+
return true if good_job_cli?
|
|
141
163
|
|
|
142
164
|
false
|
|
143
165
|
end
|
|
@@ -158,6 +180,12 @@ module LogStruct
|
|
|
158
180
|
!!(defined?(::Sidekiq) && ::Sidekiq.respond_to?(:server?) && ::Sidekiq.server?)
|
|
159
181
|
end
|
|
160
182
|
|
|
183
|
+
# GoodJob's executable (`good_job start`) sets within_exe before it loads the app
|
|
184
|
+
sig { returns(T::Boolean) }
|
|
185
|
+
def good_job_cli?
|
|
186
|
+
!!(defined?(::GoodJob::CLI) && ::GoodJob::CLI.respond_to?(:within_exe?) && ::GoodJob::CLI.within_exe?)
|
|
187
|
+
end
|
|
188
|
+
|
|
161
189
|
sig { returns(T::Boolean) }
|
|
162
190
|
def logstruct_server_mode?
|
|
163
191
|
::LogStruct.server_mode?
|
|
@@ -172,55 +200,81 @@ module LogStruct
|
|
|
172
200
|
EMPTY_ARGV
|
|
173
201
|
end
|
|
174
202
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
# Rails matches Symbols and Strings as case-insensitive substrings (:passw filters
|
|
204
|
+
# "user_password") and Regexps as they are. Entries with a dot match a key by its path
|
|
205
|
+
# instead (build_rails_path_matcher).
|
|
206
|
+
#
|
|
207
|
+
# The substring matches skip LogStruct's own fields (Rails' :_key would hide GoodJob's
|
|
208
|
+
# cron_key); their exact names are added to filter_keys instead. A Regexp says exactly
|
|
209
|
+
# what it matches, so Regexps apply to every key, LogStruct's fields included.
|
|
210
|
+
sig { params(patterns: T::Array[T.untyped], label: String, matches_logstruct_fields: T::Boolean).returns(ConfigStruct::FilterMatcher) }
|
|
211
|
+
def build_rails_pattern_matcher(patterns, label:, matches_logstruct_fields:)
|
|
212
|
+
parameter_filter = ::ActiveSupport::ParameterFilter.new(patterns, mask: RAILS_FILTER_MASK)
|
|
213
|
+
callable = Kernel.lambda do |key, _value|
|
|
214
|
+
parameter_filter.filter_param(key, RAILS_FILTER_PROBE) == RAILS_FILTER_MASK
|
|
215
|
+
end
|
|
216
|
+
ConfigStruct::FilterMatcher.new(callable: callable, label: label, matches_logstruct_fields: matches_logstruct_fields)
|
|
184
217
|
end
|
|
185
218
|
|
|
186
|
-
sig { params(filter: T.untyped).returns(T
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
return filter.downcase.to_sym if filter.is_a?(String)
|
|
190
|
-
|
|
191
|
-
return nil unless filter.respond_to?(:to_sym)
|
|
192
|
-
|
|
193
|
-
begin
|
|
194
|
-
sym = filter.to_sym
|
|
195
|
-
sym.is_a?(Symbol) ? sym : nil
|
|
196
|
-
rescue
|
|
197
|
-
nil
|
|
198
|
-
end
|
|
219
|
+
sig { params(filter: T.untyped).returns(T::Boolean) }
|
|
220
|
+
def callable_filter?(filter)
|
|
221
|
+
filter.respond_to?(:call)
|
|
199
222
|
end
|
|
200
223
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
filter.match?(key)
|
|
207
|
-
end
|
|
208
|
-
return ConfigStruct::FilterMatcher.new(callable: callable, label: filter.inspect)
|
|
224
|
+
# Rails treats Strings and Symbols with a dot, and Regexps with an escaped dot, as paths
|
|
225
|
+
sig { params(entry: T.untyped).returns(T::Boolean) }
|
|
226
|
+
def path_pattern?(entry)
|
|
227
|
+
if entry.is_a?(Regexp)
|
|
228
|
+
entry.source.include?("\\.")
|
|
209
229
|
else
|
|
210
|
-
|
|
230
|
+
entry.to_s.include?(".")
|
|
211
231
|
end
|
|
232
|
+
end
|
|
212
233
|
|
|
213
|
-
|
|
234
|
+
# Rails joins a nested parameter's keys with dots ("user.password") and matches its dotted
|
|
235
|
+
# entries against that, as case-insensitive substrings or as Regexps. LogStruct's paths
|
|
236
|
+
# start at the line's field ("params.user.password"), so an entry matches the path from
|
|
237
|
+
# there or from the field's value down, as it would in Rails.
|
|
238
|
+
sig { params(patterns: T::Array[T.untyped]).returns(ConfigStruct::PathFilterMatcher) }
|
|
239
|
+
def build_rails_path_matcher(patterns)
|
|
240
|
+
regexps = patterns.select { |entry| entry.is_a?(Regexp) }
|
|
241
|
+
strings = patterns.reject { |entry| entry.is_a?(Regexp) }.map { |entry| Regexp.escape(entry.to_s) }
|
|
242
|
+
regexps << Regexp.new(strings.join("|"), true) if strings.any?
|
|
243
|
+
callable = Kernel.lambda do |path, _value, _root|
|
|
244
|
+
full_path = path.join(".")
|
|
245
|
+
field_path = path.drop(1).join(".")
|
|
246
|
+
regexps.any? { |regexp| regexp.match?(full_path) || (!field_path.empty? && regexp.match?(field_path)) }
|
|
247
|
+
end
|
|
248
|
+
ConfigStruct::PathFilterMatcher.new(callable: callable, label: RAILS_PATHS_LABEL, uses_root: false)
|
|
214
249
|
end
|
|
215
250
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
251
|
+
# A Rails filter block with three arguments, given the parameters around the key: the
|
|
252
|
+
# line's top-level field's value (a request line's params), or the line for a top-level key
|
|
253
|
+
sig { params(filter: T.untyped).returns(ConfigStruct::PathFilterMatcher) }
|
|
254
|
+
def build_path_block_matcher(filter)
|
|
255
|
+
callable = Kernel.lambda do |path, original_value, root|
|
|
256
|
+
original_key = path.last.to_s
|
|
257
|
+
key = original_key.dup
|
|
258
|
+
value = original_value.duplicable? ? original_value.dup : original_value
|
|
259
|
+
result = filter.call(key, value, root)
|
|
260
|
+
!!result || (value.is_a?(String) && value != original_value)
|
|
261
|
+
rescue => e
|
|
262
|
+
handle_filter_error(e, filter, original_key || "")
|
|
263
|
+
false
|
|
264
|
+
end
|
|
265
|
+
ConfigStruct::PathFilterMatcher.new(callable: callable, label: filter.inspect)
|
|
219
266
|
end
|
|
220
267
|
|
|
221
|
-
|
|
268
|
+
# Rails gives filter blocks copies of the key and value, ignores what they return, and
|
|
269
|
+
# logs the value as the block left it: the Rails guides' example reverses it in place,
|
|
270
|
+
# and a block can replace it with "[FILTERED]". LogStruct filters a key when its block
|
|
271
|
+
# returns a truthy value or changes a String it's given, and never changes the logged
|
|
272
|
+
# value itself. Blocks with three arguments are path matchers (build_path_block_matcher).
|
|
273
|
+
sig { params(filter: T.untyped).returns(ConfigStruct::FilterMatcher) }
|
|
222
274
|
def build_callable_filter_matcher(filter)
|
|
223
|
-
callable = Kernel.lambda do |
|
|
275
|
+
callable = Kernel.lambda do |original_key, original_value|
|
|
276
|
+
key = original_key.dup
|
|
277
|
+
value = original_value.duplicable? ? original_value.dup : original_value
|
|
224
278
|
call_args = case arity_for_filter(filter)
|
|
225
279
|
when 0
|
|
226
280
|
[]
|
|
@@ -231,7 +285,7 @@ module LogStruct
|
|
|
231
285
|
end
|
|
232
286
|
|
|
233
287
|
result = filter.call(*call_args)
|
|
234
|
-
!!result
|
|
288
|
+
!!result || (value.is_a?(String) && value != original_value)
|
|
235
289
|
rescue ArgumentError
|
|
236
290
|
begin
|
|
237
291
|
!!filter.call(key)
|
|
@@ -251,12 +305,6 @@ module LogStruct
|
|
|
251
305
|
filter.respond_to?(:arity) ? filter.arity : 2
|
|
252
306
|
end
|
|
253
307
|
|
|
254
|
-
sig { params(filter_params: T::Array[T.untyped], leftovers: T::Array[T.untyped]).void }
|
|
255
|
-
def replace_filter_parameters(filter_params, leftovers)
|
|
256
|
-
filter_params.clear
|
|
257
|
-
filter_params.concat(leftovers)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
308
|
sig { params(error: StandardError, filter: T.untyped, key: String).void }
|
|
261
309
|
def handle_filter_error(error, filter, key)
|
|
262
310
|
context = {
|
|
@@ -42,10 +42,19 @@ module LogStruct
|
|
|
42
42
|
LogStruct.error(error_log)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
#
|
|
45
|
+
# Log an error and report it using the configured handler or MultiErrorReporter
|
|
46
46
|
sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
|
47
47
|
def log_and_report_error(error, source:, context: nil)
|
|
48
48
|
log_error(error, source: source, context: context)
|
|
49
|
+
report_error(error, source: source, context: context)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Report an error using the configured handler or MultiErrorReporter, without logging it. A reporter that
|
|
53
|
+
# raises (a timeout calling its service) doesn't change what happens to the error: its own error is logged,
|
|
54
|
+
# and not reported through the same reporter.
|
|
55
|
+
sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
|
56
|
+
def report_error(error, source:, context: nil)
|
|
57
|
+
context = filtered_context(context)
|
|
49
58
|
error_handler = LogStruct.config.error_reporting_handler
|
|
50
59
|
if error_handler
|
|
51
60
|
# Use the configured handler
|
|
@@ -54,11 +63,31 @@ module LogStruct
|
|
|
54
63
|
# Fall back to MultiErrorReporter (detects Sentry, Bugsnag, etc.)
|
|
55
64
|
LogStruct::MultiErrorReporter.report_error(error, context || {})
|
|
56
65
|
end
|
|
66
|
+
rescue => e
|
|
67
|
+
LogStruct.error(Log::Error.new(
|
|
68
|
+
source: Source::Internal,
|
|
69
|
+
error_class: e.class,
|
|
70
|
+
message: "The error reporter raised while reporting #{error.class}",
|
|
71
|
+
error_message: e.message,
|
|
72
|
+
backtrace: e.backtrace,
|
|
73
|
+
level: Level::Error
|
|
74
|
+
))
|
|
57
75
|
end
|
|
58
76
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
# The context filtered and scrubbed like a log line, for error reporters: it can hold a
|
|
78
|
+
# request's parameters or a mail's recipients
|
|
79
|
+
sig { params(context: T.nilable(T::Hash[Symbol, T.untyped])).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
80
|
+
def filtered_context(context)
|
|
81
|
+
return context if context.nil? || context.empty?
|
|
82
|
+
|
|
83
|
+
filtered = Formatter.new.process_values(context)
|
|
84
|
+
filtered.is_a?(Hash) ? filtered : {}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Handle an error according to the configured error handling mode (log, report, raise, etc).
|
|
88
|
+
# Pass `log: false` when the caller has already logged the error in its own structured line.
|
|
89
|
+
sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped]), log: T::Boolean).void }
|
|
90
|
+
def handle_exception(error, source:, context: nil, log: true)
|
|
62
91
|
mode = error_handling_mode_for(source)
|
|
63
92
|
|
|
64
93
|
# Log / report in production, raise locally (dev/test)
|
|
@@ -74,10 +103,11 @@ module LogStruct
|
|
|
74
103
|
raise(error)
|
|
75
104
|
|
|
76
105
|
when ErrorHandlingMode::Log, ErrorHandlingMode::LogProduction
|
|
77
|
-
log_error(error, source: source, context: context)
|
|
106
|
+
log_error(error, source: source, context: context) if log
|
|
78
107
|
|
|
79
108
|
when ErrorHandlingMode::Report, ErrorHandlingMode::ReportProduction
|
|
80
|
-
|
|
109
|
+
log_error(error, source: source, context: context) if log
|
|
110
|
+
report_error(error, source: source, context: context)
|
|
81
111
|
|
|
82
112
|
else
|
|
83
113
|
# Ensures the case statement is exhaustive
|
|
@@ -16,7 +16,9 @@ module LogStruct
|
|
|
16
16
|
# - Raise - Always raise the error
|
|
17
17
|
|
|
18
18
|
# Configurable error handling categories
|
|
19
|
-
|
|
19
|
+
# Sorbet type errors in your app and its gems: logged and reported in production, so they don't pass unseen
|
|
20
|
+
prop :type_checking_errors, ErrorHandlingMode, default: ErrorHandlingMode::ReportProduction
|
|
21
|
+
# LogStruct's own errors
|
|
20
22
|
prop :logstruct_errors, ErrorHandlingMode, default: ErrorHandlingMode::LogProduction
|
|
21
23
|
prop :security_errors, ErrorHandlingMode, default: ErrorHandlingMode::Report
|
|
22
24
|
prop :standard_errors, ErrorHandlingMode, default: ErrorHandlingMode::Raise
|
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "active_support/hash_with_indifferent_access"
|
|
5
|
+
|
|
4
6
|
module LogStruct
|
|
7
|
+
# A key's words, lowercase and joined by underscores, so that keys match however they're written:
|
|
8
|
+
# apiKey, api-key, X-Api-Key and HTTP_X_API_KEY all contain "api_key"
|
|
9
|
+
module KeyWords
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(key: T.untyped).returns(String) }
|
|
13
|
+
def self.normalize(key)
|
|
14
|
+
key.to_s
|
|
15
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
16
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
17
|
+
.tr("- .", "___")
|
|
18
|
+
.downcase
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
5
22
|
module ConfigStruct
|
|
6
23
|
class FilterMatcher < T::Struct
|
|
7
24
|
extend T::Sig
|
|
8
25
|
|
|
9
26
|
const :callable, T.proc.params(key: String, value: T.untyped).returns(T::Boolean)
|
|
10
27
|
const :label, String
|
|
28
|
+
# Whether the matcher also applies to the names of LogStruct's own fields at the top of a line.
|
|
29
|
+
# LogStruct's matcher for Rails' partial matches doesn't: Rails' default :_key would filter cron_key.
|
|
30
|
+
const :matches_logstruct_fields, T::Boolean, default: true
|
|
11
31
|
|
|
12
32
|
sig { params(key: String, value: T.untyped).returns(T::Boolean) }
|
|
13
33
|
def matches?(key, value)
|
|
@@ -15,22 +35,67 @@ module LogStruct
|
|
|
15
35
|
end
|
|
16
36
|
end
|
|
17
37
|
|
|
38
|
+
# A filter that needs more than the key: Rails' entries with a dot (`card.number`) match a key by its
|
|
39
|
+
# path, and Rails gives filter blocks with three arguments the parameters around the key. The path
|
|
40
|
+
# starts at the line's top-level field (`["params", "card", "number"]`); `root` is that field's value,
|
|
41
|
+
# or the whole line for a top-level key, with indifferent access as Rails gives it. Building it copies
|
|
42
|
+
# every value inside it, so a matcher that doesn't read it sets uses_root to false and gets nil.
|
|
43
|
+
class PathFilterMatcher < T::Struct
|
|
44
|
+
extend T::Sig
|
|
45
|
+
|
|
46
|
+
const :callable, T.proc.params(path: T::Array[String], value: T.untyped, root: T.untyped).returns(T::Boolean)
|
|
47
|
+
const :label, String
|
|
48
|
+
const :uses_root, T::Boolean, default: true
|
|
49
|
+
|
|
50
|
+
sig { params(path: T::Array[String], value: T.untyped, root: T.untyped).returns(T::Boolean) }
|
|
51
|
+
def matches?(path, value, root)
|
|
52
|
+
root = nil unless uses_root
|
|
53
|
+
callable.call(path, value, root.is_a?(LazyRoot) ? root.value : root)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The root a PathFilterMatcher gets, built the first time a matcher reads it
|
|
58
|
+
class LazyRoot
|
|
59
|
+
extend T::Sig
|
|
60
|
+
|
|
61
|
+
sig { params(value: T.untyped).void }
|
|
62
|
+
def initialize(value)
|
|
63
|
+
@source = value
|
|
64
|
+
@built = T.let(false, T::Boolean)
|
|
65
|
+
@value = T.let(nil, T.untyped)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The value with indifferent access, as Rails gives filter blocks the request's parameters
|
|
69
|
+
sig { returns(T.untyped) }
|
|
70
|
+
def value
|
|
71
|
+
return @value if @built
|
|
72
|
+
|
|
73
|
+
@built = true
|
|
74
|
+
@value = begin
|
|
75
|
+
@source.is_a?(Hash) ? ActiveSupport::HashWithIndifferentAccess.new(@source) : @source
|
|
76
|
+
rescue SystemStackError, StandardError
|
|
77
|
+
@source
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
18
82
|
class Filters < T::Struct
|
|
19
83
|
include Sorbet::SerializeSymbolKeys
|
|
20
84
|
|
|
21
85
|
# Keys that should be filtered in nested structures such as request params and job arguments.
|
|
22
|
-
#
|
|
86
|
+
# A filtered value is replaced by a summary of it, with no length (which would say how long a
|
|
87
|
+
# secret is):
|
|
23
88
|
#
|
|
24
|
-
# { _filtered: {
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# _keys_count: 3, # Number of keys in the hash
|
|
28
|
-
# _keys: [:key1, :key2, :key3], # First 10 keys in the hash
|
|
29
|
-
# }
|
|
30
|
-
# }
|
|
89
|
+
# { _filtered: { _class: "Hash", _keys_count: 3, _keys: [:key1, :key2, :key3] } } # first 10 keys
|
|
90
|
+
# { _filtered: { _class: "Array", _count: 2 } }
|
|
91
|
+
# { _filtered: { _class: "String" } } # plus its _hash for filter_keys_with_hashes
|
|
31
92
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
93
|
+
# A key matches when its name, or its words joined by underscores (KeyWords.normalize), equal an
|
|
94
|
+
# entry: :api_key also filters apiKey and X-Api-Key, but doesn't by itself filter api_key_id (the
|
|
95
|
+
# default filter_matchers' sensitive words do).
|
|
96
|
+
#
|
|
97
|
+
# Default: [:password, :password_confirmation, :pass, :pw, :token, :secret, :credentials, :auth,
|
|
98
|
+
# :authentication, :authorization, :credit_card, :ssn, :social_security]
|
|
34
99
|
#
|
|
35
100
|
prop :filter_keys,
|
|
36
101
|
T::Array[Symbol],
|
|
@@ -42,22 +107,22 @@ module LogStruct
|
|
|
42
107
|
]
|
|
43
108
|
}
|
|
44
109
|
|
|
45
|
-
# Keys
|
|
46
|
-
#
|
|
110
|
+
# Keys that are filtered like filter_keys, and whose String values also get a _hash in their summary
|
|
111
|
+
# (see hash_salt), so lines about the same person can be followed without logging the address.
|
|
47
112
|
# Default: [:email, :email_address]
|
|
48
113
|
prop :filter_keys_with_hashes,
|
|
49
114
|
T::Array[Symbol],
|
|
50
115
|
factory: -> { %i[email email_address] }
|
|
51
116
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# Default:
|
|
55
|
-
|
|
117
|
+
# The key for the HMAC-SHA256 hashes of emails and filter_keys_with_hashes values. Anyone who has
|
|
118
|
+
# the key and a list of candidate emails can match them to the hashes, so keep it secret.
|
|
119
|
+
# Default: nil, which uses a key derived from the Rails app's secret_key_base. Set it to keep the
|
|
120
|
+
# same hashes in every app that logs the same people.
|
|
121
|
+
prop :hash_salt, T.nilable(String), default: nil
|
|
56
122
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
prop :hash_length, Integer, default: 12
|
|
123
|
+
# How many hex characters of each hash to log
|
|
124
|
+
# Default: 16
|
|
125
|
+
prop :hash_length, Integer, default: 16
|
|
61
126
|
|
|
62
127
|
# Filter email addresses. Also controls email filtering for the ActionMailer integration
|
|
63
128
|
# (to, from, recipient fields, etc.)
|
|
@@ -68,6 +133,17 @@ module LogStruct
|
|
|
68
133
|
# Default: true
|
|
69
134
|
prop :url_passwords, T::Boolean, default: true
|
|
70
135
|
|
|
136
|
+
# Filter sensitive params in URLs' query strings: params that filter_keys or the filter matchers
|
|
137
|
+
# would filter as keys (?token=, ?api_key=, user[password]=), and OAuth's ?code=
|
|
138
|
+
# Default: true
|
|
139
|
+
prop :url_query_params, T::Boolean, default: true
|
|
140
|
+
|
|
141
|
+
# Scrub the credentials after Bearer and Basic in strings (an Authorization header's value,
|
|
142
|
+
# logged as text): Bearer tokens of 16 or more characters, and Basic credentials that decode to
|
|
143
|
+
# user:password
|
|
144
|
+
# Default: true
|
|
145
|
+
prop :authorization_credentials, T::Boolean, default: true
|
|
146
|
+
|
|
71
147
|
# Filter credit card numbers
|
|
72
148
|
# Default: true
|
|
73
149
|
prop :credit_card_numbers, T::Boolean, default: true
|
|
@@ -88,32 +164,39 @@ module LogStruct
|
|
|
88
164
|
# Default: false
|
|
89
165
|
prop :mac_addresses, T::Boolean, default: false
|
|
90
166
|
|
|
91
|
-
# Default
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
167
|
+
# Default pattern for sensitive keys, matched against a key's words joined by underscores
|
|
168
|
+
# (KeyWords.normalize: apiKey, api-key and API_KEY are all "api_key"):
|
|
169
|
+
# - these words anywhere in the key: password, passwd, passphrase, passcode, pwd, token, secret, auth,
|
|
170
|
+
# authorization, authentication, cred, credential, cookie, session, jwt, bearer, otp, apikey, cvv,
|
|
171
|
+
# cvc, ssn, social_security, social_security_number, credit_card, card_number, crypt, crypted (and
|
|
172
|
+
# their plurals): access_token, newPassword, Set-Cookie, HTTP_AUTHORIZATION, credit_card_number
|
|
173
|
+
# - api_key, secret_key, private_key, access_key, encryption_key, signing_key, master_key: X-Api-Key
|
|
174
|
+
# - signature, sig, salt and certificate as the last word: X-Amz-Signature (but not signature_field_id)
|
|
175
|
+
# Matching whole words avoids false positives like "keyboard", "author" or "cron_key".
|
|
99
176
|
DEFAULT_SENSITIVE_KEY_PATTERN = T.let(
|
|
100
|
-
/(
|
|
177
|
+
/(?:\A|_)(?:passwords?|passwd|passphrases?|passcodes?|pwd|tokens?|secrets?|auth|authorization|authentication|creds?|credentials?|cookies?|sessions?|jwts?|bearer|otps?|apikeys?|cvv|cvc|ssn|social_security(?:_numbers?)?|credit_cards?|card_numbers?|crypt|crypted)(?:_|\z)|(?:\A|_)(?:api|secret|private|access|encryption|signing|master)_keys?(?:_|\z)|(?:\A|_)(?:signature|sig|salt|certificate)s?\z/,
|
|
101
178
|
Regexp
|
|
102
179
|
)
|
|
103
180
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
181
|
+
# Matchers for keys that aren't listed by name in filter_keys. Each matcher receives the key (String)
|
|
182
|
+
# and optional value, returning true when the pair should be filtered. Includes a regex matcher for
|
|
183
|
+
# common sensitive key patterns, and LogStruct adds matchers for Rails' filter_parameters at boot.
|
|
107
184
|
prop :filter_matchers,
|
|
108
185
|
T::Array[FilterMatcher],
|
|
109
186
|
factory: -> {
|
|
110
187
|
[
|
|
111
188
|
FilterMatcher.new(
|
|
112
|
-
callable: ->(key, _value) { DEFAULT_SENSITIVE_KEY_PATTERN.match?(key) },
|
|
189
|
+
callable: ->(key, _value) { DEFAULT_SENSITIVE_KEY_PATTERN.match?(KeyWords.normalize(key)) },
|
|
113
190
|
label: "default_sensitive_pattern"
|
|
114
191
|
)
|
|
115
192
|
]
|
|
116
193
|
}
|
|
194
|
+
|
|
195
|
+
# Matchers that need a key's path or the parameters around it (see PathFilterMatcher). LogStruct
|
|
196
|
+
# adds them for Rails' filter_parameters at boot.
|
|
197
|
+
prop :path_matchers,
|
|
198
|
+
T::Array[PathFilterMatcher],
|
|
199
|
+
factory: -> { [] }
|
|
117
200
|
end
|
|
118
201
|
end
|
|
119
202
|
end
|
|
@@ -61,10 +61,6 @@ module LogStruct
|
|
|
61
61
|
# Default: true
|
|
62
62
|
prop :enable_goodjob, T::Boolean, default: true
|
|
63
63
|
|
|
64
|
-
# Enable SemanticLogger integration for high-performance logging
|
|
65
|
-
# Default: true
|
|
66
|
-
prop :enable_semantic_logger, T::Boolean, default: true
|
|
67
|
-
|
|
68
64
|
# Enable SQL query logging through ActiveRecord instrumentation
|
|
69
65
|
# Default: false (can be resource intensive)
|
|
70
66
|
prop :enable_sql_logging, T::Boolean, default: false
|