GamendWeb.LogFilters (gamend_web v1.0.1216)

Copy Markdown View Source

Primary :logger filters for crash reports that are noise, not defects.

A primary filter (rather than a per-handler one) because these reports are worthless everywhere: console, file log and the admin Logs buffer alike. Filtering once at the source also keeps them from consuming the admin buffer, which is a fixed-size ring — a steady stream of noise evicts the real entries long before anyone reads them.

The TLS client alerts

When a client closes a TLS connection by sending a user_canceled alert (Safari and iOS do this routinely on navigation away), Erlang's :ssl sends the owning process {:ssl_error, socket, {:tls_alert, {:user_canceled, _}}}. Thousand Island turns that straight into {:stop, reason, state}:

def handle_info({msg, raw_socket, reason}, {socket, state})
    when msg in [:tcp_error, :ssl_error] do
  {:stop, reason, {socket, state}}
end

The reason is neither :normal nor :shutdown, so OTP logs a full gen_server terminate crash report with the entire socket struct inlined — one per closing client. Note this path ignores Thousand Island's silent_terminate_on_error option, which only covers the handler-return error path, so there is no configuration knob that suppresses it.

Why an allowlist rather than "drop all TLS alerts"

Most TLS alerts are worth seeing. handshake_failure, unknown_ca and certificate_expired on a server that terminates TLS itself (this one binds 443 directly — no proxy) are how a broken or expired certificate announces itself. Dropping every :tls_alert would silence the one class of TLS error that actually needs a human. Only alerts that mean "the peer went away or never spoke TLS" are listed.

The corruption alerts

Binding 443 straight to the internet puts every scanner, broken middlebox and half-open mobile connection in front of the TLS stack, and each one costs a full crash report:

  • bad_record_mac — a record failed its authentication check, in either direction (decryption_failed, record_type_mismatch). Bytes were altered, truncated or replayed in transit; nothing on this side can repair a network path that mangles packets.
  • unexpected_message carrying unsupported_record_type — the byte where a TLS content type belongs is not one of the four legal values (20, 21, 22, 23). Whatever connected is not speaking TLS at all.

Both are peer faults by construction. A genuine misconfiguration on this side fails during the handshake, as one of the alerts deliberately kept above.

The non-alert noise

Bandit.TransportError "Unable to obtain conn_data" is a socket that died between accept and first read: :inet.peername/1 answers :einval because there is no longer a connection to name. Scanners that connect and instantly reset produce a steady trickle of these.

Bandit's "Connection that looks like TLS received on a clear channel" is a client speaking TLS to port 80. This app serves 80 only to redirect and to answer ACME challenges, so that is the client's mistake, not a fault here.

Bandit's own protocol-error lines — malformed request lines, forbidden HTTP/2 headers, bodies cut short — are deliberately not handled here. They are switched off at the source, log_protocol_errors: false on both listeners in GamendWeb.HostRuntime, because every one of them is the peer's fault by construction and matching their message strings here would be a patch on top of a patch. The dead-socket rule below still applies to a host that builds its own endpoint config and leaves Bandit's default in place.

Summary

Functions

Drops crash reports and warnings caused by the peer rather than by this server.

The host-supplied GamendWeb.LogFilter modules, in configuration order.

Installs the filters. Idempotent: re-installing is a no-op, so a supervisor restart cannot stack duplicates.

Functions

filter_tls_alert(event, extra)

Drops crash reports and warnings caused by the peer rather than by this server.

Returns :stop to drop the event, or :ignore to leave it for the remaining filters and handlers — never the event itself, since this filter only ever rejects and must not short-circuit other filters by accepting.

extra is the filter config :logger was given at install. A :host_filters list there wins over the application env; install/0 passes none, so production reads the host's config live, and a caller (or a test) can pin its own list without touching global state.

host_filters()

The host-supplied GamendWeb.LogFilter modules, in configuration order.

Noise from a host's own code is the host's to describe; this is how it says so without patching core.

install()

Installs the filters. Idempotent: re-installing is a no-op, so a supervisor restart cannot stack duplicates.