Log the client address next to the peer address (closes #270)
check / check (push) Waiting to run

The access log, the rate-limit rejection lines, the CSRF warning and
the receiver's "webhook request received" line now carry clientIP,
the address the rate limiters key on, next to remoteIP, the
connecting peer. Logging works it out once per request from the same
code the rate limiters use and stores it on the request context for
the other lines. The CSRF and receiver lines name the peer as
remoteIP instead of remote_addr. The README documents the field and
that it is only as trustworthy as TRUSTED_PROXIES.

Model: opus-5-5
This commit is contained in:
clawbot
2026-10-02 14:10:28 +00:00
committed by sneak
parent e8379272ae
commit a0788ba1ef
10 changed files with 506 additions and 66 deletions
+44 -10
View File
@@ -3,6 +3,7 @@
package middleware
import (
"context"
"log/slog"
"net"
"net/http"
@@ -69,18 +70,19 @@ const (
// url, useragent, referer 3*(512+11) = 1569
// request_id 128+11 = 139
// method 32+11 = 43
// fixed portion = 336
// fixed portion = 405
// ----
// 2087
// 2156
//
// The 512 is logfield.MaxBytes; the 11 is the truncation marker,
// charged on top of each budget rather than inside it.
//
// The fixed portion is the JSON punctuation, the field names, the
// level and the message, both timestamps at their longest, an IPv6
// remoteIP with a zone, a three-digit status and a full-width int64
// latency. Stated at 2560 so the figure carries headroom rather
// than sitting on the arithmetic.
// level and the message, both timestamps at their longest, remoteIP
// and clientIP each charged as an IPv6 address with a zone, a
// three-digit status and a full-width int64 latency. Stated at 2560
// so the figure carries headroom rather than sitting on the
// arithmetic.
//
// The tty text handler in internal/logger is covered by the same
// figure. logfield.EncodedBytes charges every rune at least what
@@ -88,8 +90,8 @@ const (
// bytes strconv.Quote spends on a non-printable rune at or above
// U+10000, which is four more than the JSON handler ever spends —
// so each budget bounds the encoded field under either handler.
// The text handler's fixed portion is 286, the smaller of the two,
// which puts its worst case at 2037.
// The text handler's fixed portion is 351, the smaller of the two,
// which puts its worst case at 2102.
//
// It is also the ceiling on every OTHER line this service writes
// THROUGH SLOG that carries text an UNAUTHENTICATED client
@@ -215,6 +217,28 @@ func ipFromHostPort(hp string) string {
return h
}
// RemoteIP returns the address of the connecting peer, without its
// port. Behind a reverse proxy it is the proxy. Every log line that
// names the client logs it as remoteIP, next to clientIP.
func RemoteIP(r *http.Request) string {
return ipFromHostPort(r.RemoteAddr)
}
// clientIPKey is the request context key under which Logging stores
// the value ClientIP returns.
type clientIPKey struct{}
// ClientIP returns the address the request is attributed to, which
// Logging works out once per request with clientAddr in ratelimit.go
// and logs as clientIP. The other lines that name the client read it
// from here, so all of them agree. It is empty for a request Logging
// has not seen.
func ClientIP(r *http.Request) string {
ip, _ := r.Context().Value(clientIPKey{}).(string)
return ip
}
type loggingResponseWriter struct {
http.ResponseWriter
@@ -316,6 +340,13 @@ func (s *Middleware) Logging() func(http.Handler) http.Handler {
lrw := newLoggingResponseWriter(w)
ctx := r.Context()
// When RemoteAddr is not an address, the peer's own
// text is all the request can be attributed to.
clientIP := RemoteIP(r)
if addr, ok := s.clientAddr(r); ok {
clientIP = addr.String()
}
defer func() {
latency := time.Since(start)
requestID := ""
@@ -350,13 +381,16 @@ func (s *Middleware) Logging() func(http.Handler) http.Handler {
r.Referer(), logfield.MaxBytes,
),
"proto", r.Proto,
"remoteIP", ipFromHostPort(r.RemoteAddr),
"remoteIP", RemoteIP(r),
"clientIP", clientIP,
"status", lrw.statusCode,
"latency_ms", latency.Milliseconds(),
)
}()
next.ServeHTTP(lrw, r)
next.ServeHTTP(lrw, r.WithContext(
context.WithValue(ctx, clientIPKey{}, clientIP),
))
})
}
}