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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../../params_errors"
|
|
5
|
+
|
|
6
|
+
module LogStruct
|
|
7
|
+
module Integrations
|
|
8
|
+
module RackErrorHandler
|
|
9
|
+
# The structured lines for errors raised while Rails handles a request. Each error is logged once:
|
|
10
|
+
# security errors from the app by DebugExceptions' interceptor, anything else that gets past
|
|
11
|
+
# ShowExceptions by the middleware. Rails still turns each error into its usual response.
|
|
12
|
+
module ErrorLines
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
CSRF_ERROR_NAMES = T.let(
|
|
16
|
+
["ActionController::InvalidAuthenticityToken", "ActionController::InvalidCrossOriginRequest"].freeze,
|
|
17
|
+
T::Array[String]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
sig { returns(T::Boolean) }
|
|
21
|
+
def self.enabled?
|
|
22
|
+
LogStruct.enabled? && LogStruct.config.integrations.enable_rack_error_handler
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# For DebugExceptions.register_interceptor: CSRF and IP spoofing errors from the app, which
|
|
26
|
+
# ShowExceptions would otherwise turn into a response before any middleware saw them.
|
|
27
|
+
# Other errors are left to Rails' own logging and error reporting.
|
|
28
|
+
sig { params(request: ::ActionDispatch::Request, error: Exception).void }
|
|
29
|
+
def self.intercept(request, error)
|
|
30
|
+
return unless enabled?
|
|
31
|
+
return unless security_error?(error)
|
|
32
|
+
return if LoggedErrors.logged?(error)
|
|
33
|
+
|
|
34
|
+
log_security_error(request, error)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# An error that got past ShowExceptions (or came from a middleware outside it)
|
|
38
|
+
sig { params(request: ::ActionDispatch::Request, error: StandardError).void }
|
|
39
|
+
def self.log_escaped(request, error)
|
|
40
|
+
return unless enabled?
|
|
41
|
+
return if LoggedErrors.logged?(error)
|
|
42
|
+
return log_security_error(request, error) if security_error?(error)
|
|
43
|
+
|
|
44
|
+
LoggedErrors.mark(error)
|
|
45
|
+
LogStruct.error(
|
|
46
|
+
Log::Error.new(
|
|
47
|
+
source: Source::Rails,
|
|
48
|
+
error_class: error.class,
|
|
49
|
+
message: ParamsErrors.messages(error).first,
|
|
50
|
+
backtrace: error.backtrace,
|
|
51
|
+
path: request.path,
|
|
52
|
+
http_method: request.method,
|
|
53
|
+
source_ip: source_ip(request),
|
|
54
|
+
user_agent: request.user_agent,
|
|
55
|
+
referer: StringScrubber.without_query(request.referer)
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# An error the app didn't handle, which DebugExceptions logs before ShowExceptions renders Rails'
|
|
61
|
+
# error page: at config.action_dispatch.debug_exception_log_level (fatal by default), with the
|
|
62
|
+
# trace Rails would log and the response's status. A security error already has its own line.
|
|
63
|
+
sig { params(request: ::ActionDispatch::Request, wrapper: T.untyped).void }
|
|
64
|
+
def self.log_unhandled(request, wrapper)
|
|
65
|
+
error = T.cast(wrapper.exception, Exception)
|
|
66
|
+
return if LoggedErrors.logged?(error)
|
|
67
|
+
|
|
68
|
+
LoggedErrors.mark(error)
|
|
69
|
+
# Without a backtrace cleaner, Rails' trace is Thread::Backtrace::Locations
|
|
70
|
+
trace = Array(wrapper.exception_trace).map(&:to_s)
|
|
71
|
+
message, cause_message = ParamsErrors.messages(error)
|
|
72
|
+
line = Log::Error.new(
|
|
73
|
+
source: Source::Rails,
|
|
74
|
+
error_class: error.class,
|
|
75
|
+
message: message,
|
|
76
|
+
backtrace: trace.empty? ? error.backtrace : trace,
|
|
77
|
+
path: request.path,
|
|
78
|
+
http_method: request.method,
|
|
79
|
+
source_ip: source_ip(request),
|
|
80
|
+
user_agent: request.user_agent,
|
|
81
|
+
referer: StringScrubber.without_query(request.referer),
|
|
82
|
+
status: wrapper.status_code,
|
|
83
|
+
cause_class: error.cause&.class&.name,
|
|
84
|
+
cause_message: cause_message
|
|
85
|
+
)
|
|
86
|
+
case request.get_header("action_dispatch.debug_exception_log_level")
|
|
87
|
+
when ::Logger::DEBUG then LogStruct.debug(line)
|
|
88
|
+
when ::Logger::INFO then LogStruct.info(line)
|
|
89
|
+
when ::Logger::WARN then LogStruct.warn(line)
|
|
90
|
+
when ::Logger::ERROR then LogStruct.error(line)
|
|
91
|
+
else LogStruct.fatal(line)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# A security line, and the error handled in the security_errors mode (reported by default)
|
|
96
|
+
sig { params(request: ::ActionDispatch::Request, error: Exception).void }
|
|
97
|
+
def self.log_security_error(request, error)
|
|
98
|
+
LoggedErrors.mark(error)
|
|
99
|
+
if ip_spoof_error?(error)
|
|
100
|
+
LogStruct.warn(
|
|
101
|
+
Log::Security::IPSpoof.new(
|
|
102
|
+
path: request.path,
|
|
103
|
+
http_method: request.method,
|
|
104
|
+
user_agent: request.user_agent,
|
|
105
|
+
referer: StringScrubber.without_query(request.referer),
|
|
106
|
+
message: error.message,
|
|
107
|
+
source_ip: request.ip,
|
|
108
|
+
client_ip: request.get_header("HTTP_CLIENT_IP"),
|
|
109
|
+
x_forwarded_for: request.x_forwarded_for,
|
|
110
|
+
timestamp: Time.now
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
else
|
|
114
|
+
LogStruct.error(
|
|
115
|
+
Log::Security::CSRFViolation.new(
|
|
116
|
+
path: request.path,
|
|
117
|
+
http_method: request.method,
|
|
118
|
+
source_ip: source_ip(request),
|
|
119
|
+
user_agent: request.user_agent,
|
|
120
|
+
referer: StringScrubber.without_query(request.referer),
|
|
121
|
+
message: error.message,
|
|
122
|
+
timestamp: Time.now
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
reported = error.is_a?(StandardError) ? error : RuntimeError.new(error.message)
|
|
127
|
+
LogStruct.handle_exception(reported, source: Source::Security, context: request_context(request), log: false)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { params(request: ::ActionDispatch::Request).returns(T::Hash[Symbol, T.untyped]) }
|
|
131
|
+
def self.request_context(request)
|
|
132
|
+
{
|
|
133
|
+
path: request.path,
|
|
134
|
+
method: request.method,
|
|
135
|
+
user_agent: request.user_agent,
|
|
136
|
+
referer: StringScrubber.without_query(request.referer)
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The client's IP, as Rails works it out, or the connection's when Rails finds a spoofed one
|
|
141
|
+
sig { params(request: ::ActionDispatch::Request).returns(T.nilable(String)) }
|
|
142
|
+
def self.source_ip(request)
|
|
143
|
+
request.remote_ip
|
|
144
|
+
rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError
|
|
145
|
+
request.ip
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
sig { params(error: Exception).returns(T::Boolean) }
|
|
149
|
+
def self.security_error?(error)
|
|
150
|
+
ip_spoof_error?(error) || CSRF_ERROR_NAMES.include?(error.class.name)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sig { params(error: Exception).returns(T::Boolean) }
|
|
154
|
+
def self.ip_spoof_error?(error)
|
|
155
|
+
error.is_a?(::ActionDispatch::RemoteIp::IpSpoofAttackError)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -1,181 +1,26 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "../rack_middleware"
|
|
5
|
+
require_relative "error_lines"
|
|
6
|
+
|
|
4
7
|
module LogStruct
|
|
5
8
|
module Integrations
|
|
6
9
|
module RackErrorHandler
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
IP_SPOOF_HTML = T.let(
|
|
13
|
-
"<html><head><title>IP Spoofing Detected</title></head><body>" \
|
|
14
|
-
"<h1>Forbidden</h1>" \
|
|
15
|
-
"<p>IP spoofing detected. This request has been blocked for security reasons.</p>" \
|
|
16
|
-
"</body></html>",
|
|
17
|
-
String
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
# CSRF error response
|
|
21
|
-
CSRF_HTML = T.let(
|
|
22
|
-
"<html><head><title>CSRF Error</title></head><body>" \
|
|
23
|
-
"<h1>Forbidden</h1>" \
|
|
24
|
-
"<p>Invalid authenticity token. This request has been blocked to prevent cross-site request forgery.</p>" \
|
|
25
|
-
"</body></html>",
|
|
26
|
-
String
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
# Response headers calculated at load time
|
|
30
|
-
IP_SPOOF_HEADERS = T.let(
|
|
31
|
-
{
|
|
32
|
-
"Content-Type" => "text/html",
|
|
33
|
-
"Content-Length" => IP_SPOOF_HTML.bytesize.to_s
|
|
34
|
-
}.freeze,
|
|
35
|
-
T::Hash[String, String]
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
CSRF_HEADERS = T.let(
|
|
39
|
-
{
|
|
40
|
-
"Content-Type" => "text/html",
|
|
41
|
-
"Content-Length" => CSRF_HTML.bytesize.to_s
|
|
42
|
-
}.freeze,
|
|
43
|
-
T::Hash[String, String]
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
# HTTP status code for forbidden responses
|
|
47
|
-
FORBIDDEN_STATUS = T.let(403, Integer)
|
|
48
|
-
|
|
49
|
-
sig { params(app: T.untyped).void }
|
|
50
|
-
def initialize(app)
|
|
51
|
-
@app = app
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
sig { params(env: T.untyped).returns(T.untyped) }
|
|
10
|
+
# Logs the errors that get past ShowExceptions, or come from the middleware outside it (an IP
|
|
11
|
+
# spoofing error from a middleware that reads remote_ip, for example), then raises them again
|
|
12
|
+
# for the server to handle as it would without LogStruct
|
|
13
|
+
class Middleware < RackMiddleware
|
|
14
|
+
sig { override.params(env: T.untyped).returns(T.untyped) }
|
|
55
15
|
def call(env)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
request = ::ActionDispatch::Request.new(env)
|
|
59
|
-
|
|
60
|
-
begin
|
|
61
|
-
# Trigger the same spoofing checks that ActionDispatch::RemoteIp performs after
|
|
62
|
-
# it is initialized in the middleware stack. We run this manually because we
|
|
63
|
-
# execute before that middleware and still want spoofing attacks to surface here.
|
|
64
|
-
perform_remote_ip_check!(request)
|
|
65
|
-
|
|
66
|
-
@app.call(env)
|
|
67
|
-
rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError => ip_spoof_error
|
|
68
|
-
# Create a security log for IP spoofing
|
|
69
|
-
security_log = Log::Security::IPSpoof.new(
|
|
70
|
-
path: env["PATH_INFO"],
|
|
71
|
-
http_method: env["REQUEST_METHOD"],
|
|
72
|
-
user_agent: env["HTTP_USER_AGENT"],
|
|
73
|
-
referer: env["HTTP_REFERER"],
|
|
74
|
-
message: ip_spoof_error.message,
|
|
75
|
-
client_ip: env["HTTP_CLIENT_IP"],
|
|
76
|
-
x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
|
|
77
|
-
timestamp: Time.now
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
::Rails.logger.warn(security_log)
|
|
81
|
-
|
|
82
|
-
[FORBIDDEN_STATUS, IP_SPOOF_HEADERS.dup, [IP_SPOOF_HTML]]
|
|
83
|
-
rescue => error
|
|
84
|
-
if csrf_error?(error)
|
|
85
|
-
# Create a security log for CSRF error
|
|
86
|
-
security_log = Log::Security::CSRFViolation.new(
|
|
87
|
-
path: request.path,
|
|
88
|
-
http_method: request.method,
|
|
89
|
-
source_ip: request.remote_ip,
|
|
90
|
-
user_agent: request.user_agent,
|
|
91
|
-
referer: request.referer,
|
|
92
|
-
message: error.message,
|
|
93
|
-
timestamp: Time.now
|
|
94
|
-
)
|
|
95
|
-
LogStruct.error(security_log)
|
|
96
|
-
|
|
97
|
-
# Report to error reporting service and/or re-raise
|
|
98
|
-
context = extract_request_context(env, request)
|
|
99
|
-
LogStruct.handle_exception(error, source: Source::Security, context: context)
|
|
100
|
-
|
|
101
|
-
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
|
|
102
|
-
# If we are only logging or reporting these security errors, then return a default response
|
|
103
|
-
[FORBIDDEN_STATUS, CSRF_HEADERS.dup, [CSRF_HTML]]
|
|
104
|
-
else
|
|
105
|
-
# Extract request context for error reporting
|
|
106
|
-
context = extract_request_context(env, request)
|
|
107
|
-
|
|
108
|
-
# Create and log a structured exception with request context
|
|
109
|
-
exception_log = Log.from_exception(Source::Rails, error, context)
|
|
110
|
-
LogStruct.error(exception_log)
|
|
111
|
-
|
|
112
|
-
# Re-raise any standard errors to let Rails or error reporter handle it.
|
|
113
|
-
# Rails will also log the request details separately
|
|
114
|
-
raise error
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
private
|
|
120
|
-
|
|
121
|
-
sig { params(request: ::ActionDispatch::Request).void }
|
|
122
|
-
def perform_remote_ip_check!(request)
|
|
123
|
-
action_dispatch_config = ::Rails.application.config.action_dispatch
|
|
124
|
-
check_ip = action_dispatch_config.ip_spoofing_check
|
|
125
|
-
return unless check_ip
|
|
126
|
-
|
|
127
|
-
proxies = normalized_trusted_proxies(action_dispatch_config.trusted_proxies)
|
|
128
|
-
|
|
129
|
-
::ActionDispatch::RemoteIp::GetIp
|
|
130
|
-
.new(request, check_ip, proxies)
|
|
131
|
-
.to_s
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
sig { params(env: T::Hash[String, T.untyped], request: T.nilable(::ActionDispatch::Request)).returns(T::Hash[Symbol, T.untyped]) }
|
|
135
|
-
def extract_request_context(env, request = nil)
|
|
136
|
-
request ||= ::ActionDispatch::Request.new(env)
|
|
137
|
-
{
|
|
138
|
-
path: request.path,
|
|
139
|
-
method: request.method,
|
|
140
|
-
user_agent: request.user_agent,
|
|
141
|
-
referer: request.referer
|
|
142
|
-
}
|
|
16
|
+
@app.call(env)
|
|
143
17
|
rescue => error
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
sig { params(error: StandardError).returns(T::Boolean) }
|
|
149
|
-
def csrf_error?(error)
|
|
150
|
-
error_name = error.class.name
|
|
151
|
-
error_name == "ActionController::InvalidAuthenticityToken" ||
|
|
152
|
-
error_name == "ActionController::InvalidCrossOriginRequest"
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
sig { params(configured_proxies: T.untyped).returns(T.untyped) }
|
|
156
|
-
def normalized_trusted_proxies(configured_proxies)
|
|
157
|
-
if configured_proxies.nil? || (configured_proxies.respond_to?(:empty?) && configured_proxies.empty?)
|
|
158
|
-
return ::ActionDispatch::RemoteIp::TRUSTED_PROXIES
|
|
18
|
+
begin
|
|
19
|
+
ErrorLines.log_escaped(::ActionDispatch::Request.new(env), error)
|
|
20
|
+
rescue => logging_error
|
|
21
|
+
LogStruct.handle_exception(logging_error, source: Source::Internal)
|
|
159
22
|
end
|
|
160
|
-
|
|
161
|
-
return configured_proxies if configured_proxies.respond_to?(:any?)
|
|
162
|
-
|
|
163
|
-
raise(
|
|
164
|
-
ArgumentError,
|
|
165
|
-
<<~EOM
|
|
166
|
-
Setting config.action_dispatch.trusted_proxies to a single value isn't
|
|
167
|
-
supported. Please set this to an enumerable instead. For
|
|
168
|
-
example, instead of:
|
|
169
|
-
|
|
170
|
-
config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
|
|
171
|
-
|
|
172
|
-
Wrap the value in an Array:
|
|
173
|
-
|
|
174
|
-
config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
|
|
175
|
-
|
|
176
|
-
Note that passing an enumerable will *replace* the default set of trusted proxies.
|
|
177
|
-
EOM
|
|
178
|
-
)
|
|
23
|
+
raise error
|
|
179
24
|
end
|
|
180
25
|
end
|
|
181
26
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module LogStruct
|
|
5
|
+
module Integrations
|
|
6
|
+
module RackErrorHandler
|
|
7
|
+
# Prepended to ActionDispatch::Request. When Rails can't parse a request's body (malformed JSON), it logs the
|
|
8
|
+
# whole body at debug level, where nothing filters it. LogStruct logs the body's size and type instead. The
|
|
9
|
+
# request line and the ParseError's error line are written as usual.
|
|
10
|
+
module ParseErrorLogging
|
|
11
|
+
extend T::Sig
|
|
12
|
+
extend T::Helpers
|
|
13
|
+
|
|
14
|
+
requires_ancestor { ::ActionDispatch::Request }
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
sig { returns(T.untyped) }
|
|
19
|
+
def log_parse_error_once
|
|
20
|
+
return super unless ErrorLines.enabled?
|
|
21
|
+
|
|
22
|
+
@logstruct_parse_error_logged ||= T.let(
|
|
23
|
+
begin
|
|
24
|
+
(logger || ::ActiveSupport::Logger.new($stderr)).debug(
|
|
25
|
+
"Error occurred while parsing request parameters. LogStruct leaves out the body " \
|
|
26
|
+
"(#{raw_post.to_s.bytesize} bytes of #{media_type}), which it can't filter."
|
|
27
|
+
)
|
|
28
|
+
true
|
|
29
|
+
end,
|
|
30
|
+
T.nilable(TrueClass)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "rack"
|
|
5
|
-
require "action_dispatch/middleware/
|
|
5
|
+
require "action_dispatch/middleware/debug_exceptions"
|
|
6
6
|
require_relative "rack_error_handler/middleware"
|
|
7
|
+
require_relative "rack_error_handler/debug_exceptions_logging"
|
|
8
|
+
require_relative "rack_error_handler/parse_error_logging"
|
|
7
9
|
require_relative "rack_setup"
|
|
8
10
|
|
|
9
11
|
module LogStruct
|
|
@@ -13,17 +15,31 @@ module LogStruct
|
|
|
13
15
|
extend T::Sig
|
|
14
16
|
extend IntegrationInterface
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
@interceptor_registered = T.let(false, T::Boolean)
|
|
19
|
+
|
|
20
|
+
# Set up structured lines for CSRF and IP spoofing errors, errors the app didn't handle, and
|
|
21
|
+
# errors that get past ShowExceptions
|
|
17
22
|
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
|
18
23
|
def self.setup(config)
|
|
19
24
|
return nil unless RackSetup.enabled?(config)
|
|
20
25
|
|
|
21
|
-
#
|
|
22
|
-
# Need to insert before RemoteIp to catch IP spoofing errors it raises
|
|
26
|
+
# Errors from the middleware outside ShowExceptions, and any that get past it
|
|
23
27
|
RackSetup.insert_before(
|
|
24
28
|
::ActionDispatch::RemoteIp,
|
|
25
29
|
Integrations::RackErrorHandler::Middleware
|
|
26
30
|
)
|
|
31
|
+
# DebugExceptions calls its interceptors with each error from the app (in every environment),
|
|
32
|
+
# before ShowExceptions turns it into Rails' usual response: 422 for a CSRF error
|
|
33
|
+
unless @interceptor_registered
|
|
34
|
+
::ActionDispatch::DebugExceptions.register_interceptor(ErrorLines.method(:intercept))
|
|
35
|
+
@interceptor_registered = true
|
|
36
|
+
end
|
|
37
|
+
# An error line instead of the text Rails logs for an error the app didn't handle
|
|
38
|
+
unless ::ActionDispatch::DebugExceptions <= DebugExceptionsLogging
|
|
39
|
+
::ActionDispatch::DebugExceptions.prepend(DebugExceptionsLogging)
|
|
40
|
+
end
|
|
41
|
+
# The size of a body Rails can't parse, instead of the body
|
|
42
|
+
::ActionDispatch::Request.prepend(ParseErrorLogging) unless ::ActionDispatch::Request <= ParseErrorLogging
|
|
27
43
|
|
|
28
44
|
true
|
|
29
45
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module LogStruct
|
|
5
|
+
module Integrations
|
|
6
|
+
# A Rack middleware around the app: subclasses define call, and pass the request on to @app
|
|
7
|
+
class RackMiddleware
|
|
8
|
+
extend T::Sig
|
|
9
|
+
extend T::Helpers
|
|
10
|
+
|
|
11
|
+
abstract!
|
|
12
|
+
|
|
13
|
+
sig { params(app: T.untyped).void }
|
|
14
|
+
def initialize(app)
|
|
15
|
+
@app = app
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { abstract.params(env: T.untyped).returns(T.untyped) }
|
|
19
|
+
def call(env)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module LogStruct
|
|
5
|
+
module Integrations
|
|
6
|
+
# Rails' own log subscribers, which LogStruct's lines replace
|
|
7
|
+
module RailsLogSubscribers
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
# Detaches a framework's log subscriber (ActiveStorage::LogSubscriber, :active_storage). Up to Rails 8.1 it's an
|
|
11
|
+
# ActiveSupport::LogSubscriber attached to the framework's notifications; from Rails 8.2 it's an
|
|
12
|
+
# ActiveSupport::EventReporter::LogSubscriber, subscribed to Rails.event when its file loads.
|
|
13
|
+
sig { params(subscriber_class: T.untyped, namespace: Symbol).void }
|
|
14
|
+
def self.detach(subscriber_class, namespace)
|
|
15
|
+
if subscriber_class.respond_to?(:detach_from)
|
|
16
|
+
subscriber_class.detach_from(namespace)
|
|
17
|
+
else
|
|
18
|
+
unsubscribe_from_events(subscriber_class)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Unsubscribes a Rails 8.2 log subscriber from Rails.event. Does nothing for one that isn't subscribed there.
|
|
23
|
+
sig { params(subscriber_class: T.untyped).void }
|
|
24
|
+
def self.unsubscribe_from_events(subscriber_class)
|
|
25
|
+
return unless ::ActiveSupport.respond_to?(:event_reporter)
|
|
26
|
+
|
|
27
|
+
reporter = ::ActiveSupport.event_reporter
|
|
28
|
+
reporter.unsubscribe(subscriber_class) if reporter.respond_to?(:unsubscribe)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "../rack_middleware"
|
|
5
|
+
|
|
4
6
|
module LogStruct
|
|
5
7
|
module Integrations
|
|
6
8
|
module RequestContext
|
|
7
9
|
# Middleware that captures request_id and stores it in SemanticLogger's
|
|
8
10
|
# named_tags so all logs during the request include the request_id.
|
|
9
|
-
class Middleware
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
sig { params(app: T.untyped).void }
|
|
13
|
-
def initialize(app)
|
|
14
|
-
@app = app
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
sig { params(env: T.untyped).returns(T.untyped) }
|
|
11
|
+
class Middleware < RackMiddleware
|
|
12
|
+
sig { override.params(env: T.untyped).returns(T.untyped) }
|
|
18
13
|
def call(env)
|
|
19
14
|
request = ::ActionDispatch::Request.new(env)
|
|
20
15
|
request_id = request.request_id
|