Module: LogStruct::Integrations::RackErrorHandler::ErrorLines

Extended by:
T::Sig
Defined in:
lib/log_struct/integrations/rack_error_handler/error_lines.rb

Overview

The structured lines for errors raised while Rails handles a request. Each error is logged once: security errors from the app by DebugExceptions' interceptor, anything else that gets past ShowExceptions by the middleware. Rails still turns each error into its usual response.

Constant Summary collapse

CSRF_ERROR_NAMES =
T.let(
  ["ActionController::InvalidAuthenticityToken", "ActionController::InvalidCrossOriginRequest"].freeze,
  T::Array[String]
)

Class Method Summary collapse

Class Method Details

.enabled? ⇒ Boolean

Returns:

  • (Boolean)


21
22
23
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 21

def self.enabled?
  LogStruct.enabled? && LogStruct.config.integrations.enable_rack_error_handler
end

.intercept(request, error) ⇒ void

This method returns an undefined value.

For DebugExceptions.register_interceptor: CSRF and IP spoofing errors from the app, which ShowExceptions would otherwise turn into a response before any middleware saw them. Other errors are left to Rails' own logging and error reporting.

Parameters:

  • request (::ActionDispatch::Request)
  • error (Exception)


29
30
31
32
33
34
35
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 29

def self.intercept(request, error)
  return unless enabled?
  return unless security_error?(error)
  return if LoggedErrors.logged?(error)

  log_security_error(request, error)
end

.ip_spoof_error?(error) ⇒ Boolean

Parameters:

  • error (Exception)

Returns:

  • (Boolean)


154
155
156
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 154

def self.ip_spoof_error?(error)
  error.is_a?(::ActionDispatch::RemoteIp::IpSpoofAttackError)
end

.log_escaped(request, error) ⇒ void

This method returns an undefined value.

An error that got past ShowExceptions (or came from a middleware outside it)

Parameters:

  • request (::ActionDispatch::Request)
  • error (StandardError)


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 39

def self.log_escaped(request, error)
  return unless enabled?
  return if LoggedErrors.logged?(error)
  return log_security_error(request, error) if security_error?(error)

  LoggedErrors.mark(error)
  LogStruct.error(
    Log::Error.new(
      source: Source::Rails,
      error_class: error.class,
      message: ParamsErrors.messages(error).first,
      backtrace: error.backtrace,
      path: request.path,
      http_method: request.method,
      source_ip: source_ip(request),
      user_agent: request.user_agent,
      referer: StringScrubber.without_query(request.referer)
    )
  )
end

.log_security_error(request, error) ⇒ void

This method returns an undefined value.

A security line, and the error handled in the security_errors mode (reported by default)

Parameters:

  • request (::ActionDispatch::Request)
  • error (Exception)


97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 97

def self.log_security_error(request, error)
  LoggedErrors.mark(error)
  if ip_spoof_error?(error)
    LogStruct.warn(
      Log::Security::IPSpoof.new(
        path: request.path,
        http_method: request.method,
        user_agent: request.user_agent,
        referer: StringScrubber.without_query(request.referer),
        message: error.message,
        source_ip: request.ip,
        client_ip: request.get_header("HTTP_CLIENT_IP"),
        x_forwarded_for: request.x_forwarded_for,
        timestamp: Time.now
      )
    )
  else
    LogStruct.error(
      Log::Security::CSRFViolation.new(
        path: request.path,
        http_method: request.method,
        source_ip: source_ip(request),
        user_agent: request.user_agent,
        referer: StringScrubber.without_query(request.referer),
        message: error.message,
        timestamp: Time.now
      )
    )
  end
  reported = error.is_a?(StandardError) ? error : RuntimeError.new(error.message)
  LogStruct.handle_exception(reported, source: Source::Security, context: request_context(request), log: false)
end

.log_unhandled(request, wrapper) ⇒ void

This method returns an undefined value.

An error the app didn't handle, which DebugExceptions logs before ShowExceptions renders Rails' error page: at config.action_dispatch.debug_exception_log_level (fatal by default), with the trace Rails would log and the response's status. A security error already has its own line.

Parameters:

  • request (::ActionDispatch::Request)
  • wrapper (T.untyped)


64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 64

def self.log_unhandled(request, wrapper)
  error = T.cast(wrapper.exception, Exception)
  return if LoggedErrors.logged?(error)

  LoggedErrors.mark(error)
  # Without a backtrace cleaner, Rails' trace is Thread::Backtrace::Locations
  trace = Array(wrapper.exception_trace).map(&:to_s)
  message, cause_message = ParamsErrors.messages(error)
  line = Log::Error.new(
    source: Source::Rails,
    error_class: error.class,
    message: message,
    backtrace: trace.empty? ? error.backtrace : trace,
    path: request.path,
    http_method: request.method,
    source_ip: source_ip(request),
    user_agent: request.user_agent,
    referer: StringScrubber.without_query(request.referer),
    status: wrapper.status_code,
    cause_class: error.cause&.class&.name,
    cause_message: cause_message
  )
  case request.get_header("action_dispatch.debug_exception_log_level")
  when ::Logger::DEBUG then LogStruct.debug(line)
  when ::Logger::INFO then LogStruct.info(line)
  when ::Logger::WARN then LogStruct.warn(line)
  when ::Logger::ERROR then LogStruct.error(line)
  else LogStruct.fatal(line)
  end
end

.request_context(request) ⇒ Hash{Symbol => T.untyped}

Parameters:

  • request (::ActionDispatch::Request)

Returns:

  • (Hash{Symbol => T.untyped})


131
132
133
134
135
136
137
138
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 131

def self.request_context(request)
  {
    path: request.path,
    method: request.method,
    user_agent: request.user_agent,
    referer: StringScrubber.without_query(request.referer)
  }
end

.security_error?(error) ⇒ Boolean

Parameters:

  • error (Exception)

Returns:

  • (Boolean)


149
150
151
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 149

def self.security_error?(error)
  ip_spoof_error?(error) || CSRF_ERROR_NAMES.include?(error.class.name)
end

.source_ip(request) ⇒ String?

The client's IP, as Rails works it out, or the connection's when Rails finds a spoofed one

Parameters:

  • request (::ActionDispatch::Request)

Returns:

  • (String, nil)


142
143
144
145
146
# File 'lib/log_struct/integrations/rack_error_handler/error_lines.rb', line 142

def self.source_ip(request)
  request.remote_ip
rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError
  request.ip
end