Skip to content
This repository was archived by the owner on Apr 30, 2025. It is now read-only.

Commit a34e690

Browse files
maxmoehlameowlia
authored andcommitted
feat: log local TCP address
When troubleshooting issues related to keepalive it is often useful to be able to correlate multiple requests made over the same connection. A first step was to introduce the information on whether the request was made on a re-used connection or not which was done with a7dbf84. This commit adds the local address of the TCP connection which adds the capability to distinguish between multiple, parallel keep-alive connections by looking at the client port of gorouter for the backend connection.
1 parent b0d88bb commit a34e690

File tree

9 files changed

+117
-10
lines changed

9 files changed

+117
-10
lines changed

accesslog/schema/access_log_record.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ type AccessLogRecord struct {
110110
LogAttemptsDetails bool
111111
FailedAttempts int
112112
RoundTripSuccessful bool
113+
ExtraFields []string
113114
record []byte
114115

115116
// See the handlers.RequestInfo struct for details on these timings.
@@ -124,6 +125,8 @@ type AccessLogRecord struct {
124125
TlsHandshakeFinishedAt time.Time
125126
AppRequestFinishedAt time.Time
126127
FinishedAt time.Time
128+
129+
LocalAddress string
127130
}
128131

129132
func (r *AccessLogRecord) formatStartedAt() string {
@@ -319,6 +322,17 @@ func (r *AccessLogRecord) makeRecord(performTruncate bool) []byte {
319322
b.WriteDashOrFloatValue(r.successfulAttemptTime())
320323
}
321324

325+
// We have to consider the impact of iterating over a list. This technically allows to repeat
326+
// some of the fields but it allows us to iterate over the list only once instead of once per
327+
// field when we perform a [slices.Contains] check.
328+
for _, field := range r.ExtraFields {
329+
switch field {
330+
case "local_address":
331+
b.WriteString(`local_address:`)
332+
b.WriteDashOrStringValue(r.LocalAddress)
333+
}
334+
}
335+
322336
b.AppendSpaces(false)
323337
// #nosec G104 - ignore errors from writing the access log as it will only cause more errors to log this error
324338
b.WriteString(`x_cf_routererror:`)

accesslog/schema/access_log_record_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,43 @@ var _ = Describe("AccessLogRecord", func() {
314314
Eventually(r).Should(Say(`x_cf_routererror:"-"`))
315315
})
316316
})
317+
318+
Context("when extra_fields is set to [local_address]", func() {
319+
Context("and the local address is empty", func() {
320+
It("makes a record with the local address set to -", func() {
321+
record.ExtraFields = []string{"local_address"}
322+
323+
r := BufferReader(bytes.NewBufferString(record.LogMessage()))
324+
Eventually(r).Should(Say(`local_address:"-"`))
325+
})
326+
})
327+
Context("and the local address contains an address", func() {
328+
It("makes a record with the local address set to that address", func() {
329+
record.ExtraFields = []string{"local_address"}
330+
record.LocalAddress = "10.0.0.1:34823"
331+
332+
r := BufferReader(bytes.NewBufferString(record.LogMessage()))
333+
Eventually(r).Should(Say(`local_address:"10.0.0.1:34823"`))
334+
})
335+
})
336+
})
337+
338+
Context("when extra_fields is set to [foobarbazz]", func() {
339+
It("ignores it", func() {
340+
record.ExtraFields = []string{"foobarbazz"}
341+
record.LocalAddress = "10.0.0.1:34823"
342+
343+
r := BufferReader(bytes.NewBufferString(record.LogMessage()))
344+
Consistently(r).ShouldNot(Say("foobarbazz"))
345+
})
346+
It("does not log local_address", func() {
347+
record.ExtraFields = []string{"foobarbazz"}
348+
record.LocalAddress = "10.0.0.1:34823"
349+
350+
r := BufferReader(bytes.NewBufferString(record.LogMessage()))
351+
Consistently(r).ShouldNot(Say(`local_address:"10.0.0.1:34823"`))
352+
})
353+
})
317354
})
318355

319356
Describe("WriteTo", func() {

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ type LoggingConfig struct {
186186
RedactQueryParams string `yaml:"redact_query_params"`
187187
EnableAttemptsDetails bool `yaml:"enable_attempts_details"`
188188
Format FormatConfig `yaml:"format"`
189+
ExtraAccessLogFields []string `yaml:"extra_access_log_fields"`
189190

190191
// This field is populated by the `Process` function.
191192
JobName string `yaml:"-"`

handlers/access_log.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"io"
55
"log/slog"
66
"net/http"
7+
"slices"
78
"sync/atomic"
89
"time"
910

@@ -20,6 +21,7 @@ type accessLog struct {
2021
accessLogger accesslog.AccessLogger
2122
extraHeadersToLog []string
2223
logAttemptsDetails bool
24+
extraFields []string
2325
logger *slog.Logger
2426
}
2527

@@ -29,12 +31,14 @@ func NewAccessLog(
2931
accessLogger accesslog.AccessLogger,
3032
extraHeadersToLog []string,
3133
logAttemptsDetails bool,
34+
extraFields []string,
3235
logger *slog.Logger,
3336
) negroni.Handler {
3437
return &accessLog{
3538
accessLogger: accessLogger,
3639
extraHeadersToLog: extraHeadersToLog,
3740
logAttemptsDetails: logAttemptsDetails,
41+
extraFields: deduplicate(extraFields),
3842
logger: logger,
3943
}
4044
}
@@ -46,6 +50,7 @@ func (a *accessLog) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http
4650
Request: r,
4751
ExtraHeadersToLog: a.extraHeadersToLog,
4852
LogAttemptsDetails: a.logAttemptsDetails,
53+
ExtraFields: a.extraFields,
4954
}
5055

5156
requestBodyCounter := &countingReadCloser{delegate: r.Body}
@@ -82,6 +87,8 @@ func (a *accessLog) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http
8287
alr.AppRequestFinishedAt = reqInfo.AppRequestFinishedAt
8388
alr.FinishedAt = reqInfo.FinishedAt
8489

90+
alr.LocalAddress = reqInfo.LocalAddress
91+
8592
a.accessLogger.Log(*alr)
8693
}
8794

@@ -105,3 +112,19 @@ func (crc *countingReadCloser) GetCount() int {
105112
func (crc *countingReadCloser) Close() error {
106113
return crc.delegate.Close()
107114
}
115+
116+
func deduplicate[S ~[]E, E comparable](s S) S {
117+
// costs some memory and requires an allocation but reduces complexity from O(n^2)
118+
// to O(n) where n = len(s)
119+
m := make(map[E]struct{}, len(s))
120+
return slices.DeleteFunc(s, func(s E) bool {
121+
_, ok := m[s]
122+
if ok {
123+
return true
124+
}
125+
126+
m[s] = struct{}{}
127+
128+
return false
129+
})
130+
}

handlers/access_log_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ var _ = Describe("AccessLog", func() {
8181
handler = negroni.New()
8282
handler.Use(handlers.NewRequestInfo())
8383
handler.Use(handlers.NewProxyWriter(logger.Logger))
84-
handler.Use(handlers.NewAccessLog(accessLogger, extraHeadersToLog, false, logger.Logger))
84+
handler.Use(handlers.NewAccessLog(accessLogger, extraHeadersToLog, false, nil, logger.Logger))
8585
handler.Use(nextHandler)
8686

8787
reqChan = make(chan *http.Request, 1)
@@ -116,6 +116,24 @@ var _ = Describe("AccessLog", func() {
116116
Expect(alr.RouterError).To(BeEmpty())
117117
})
118118

119+
Context("when duplicate extraFields are set", func() {
120+
BeforeEach(func() {
121+
handler = negroni.New()
122+
handler.Use(handlers.NewRequestInfo())
123+
handler.Use(handlers.NewProxyWriter(logger.Logger))
124+
handler.Use(handlers.NewAccessLog(accessLogger, extraHeadersToLog, false, []string{"local_address", "local_address"}, logger.Logger))
125+
handler.Use(nextHandler)
126+
})
127+
It("only logs them once", func() {
128+
handler.ServeHTTP(resp, req)
129+
Expect(accessLogger.LogCallCount()).To(Equal(1))
130+
131+
alr := accessLogger.LogArgsForCall(0)
132+
133+
Expect(alr.ExtraFields).To(Equal([]string{"local_address"}))
134+
})
135+
})
136+
119137
Context("when there are backend request headers on the context", func() {
120138
BeforeEach(func() {
121139
extraHeadersHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -146,7 +164,7 @@ var _ = Describe("AccessLog", func() {
146164
BeforeEach(func() {
147165
handler = negroni.New()
148166
handler.UseFunc(testProxyWriterHandler)
149-
handler.Use(handlers.NewAccessLog(accessLogger, extraHeadersToLog, false, logger.Logger))
167+
handler.Use(handlers.NewAccessLog(accessLogger, extraHeadersToLog, false, nil, logger.Logger))
150168
handler.Use(nextHandler)
151169
})
152170
It("calls Panic on the logger", func() {

handlers/requestinfo.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ type RequestInfo struct {
7171
ShouldRouteToInternalRouteService bool
7272
FailedAttempts int
7373

74+
LocalAddress string
75+
7476
// RoundTripSuccessful will be set once a request has successfully reached a backend instance.
7577
RoundTripSuccessful bool
7678

proxy/proxy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func NewProxy(
173173
n.Use(handlers.NewHTTPLatencyPrometheus(p.promRegistry))
174174
}
175175
}
176-
n.Use(handlers.NewAccessLog(accessLogger, headersToLog, cfg.Logging.EnableAttemptsDetails, logger))
176+
n.Use(handlers.NewAccessLog(accessLogger, headersToLog, cfg.Logging.EnableAttemptsDetails, cfg.Logging.ExtraAccessLogFields, logger))
177177
n.Use(handlers.NewQueryParam(logger))
178178
n.Use(handlers.NewReporter(reporter, logger))
179179
n.Use(handlers.NewHTTPRewriteHandler(cfg.HTTPRewrite, headersToAlwaysRemove))

proxy/round_tripper/proxy_round_tripper.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ func (rt *roundTripper) RoundTrip(originalRequest *http.Request) (*http.Response
191191
slog.Float64("dns-lookup-time", trace.DnsTime()),
192192
slog.Float64("dial-time", trace.DialTime()),
193193
slog.Float64("tls-handshake-time", trace.TlsTime()),
194+
slog.String("local-address", trace.LocalAddr()),
194195
)
195196

196197
if err != nil {
@@ -255,6 +256,7 @@ func (rt *roundTripper) RoundTrip(originalRequest *http.Request) (*http.Response
255256
slog.Float64("dns-lookup-time", trace.DnsTime()),
256257
slog.Float64("dial-time", trace.DialTime()),
257258
slog.Float64("tls-handshake-time", trace.TlsTime()),
259+
slog.String("local-address", trace.LocalAddr()),
258260
)
259261

260262
if err != nil {
@@ -347,6 +349,7 @@ func (rt *roundTripper) RoundTrip(originalRequest *http.Request) (*http.Response
347349
reqInfo.DialFinishedAt = trace.DialDone()
348350
reqInfo.TlsHandshakeStartedAt = trace.TlsStart()
349351
reqInfo.TlsHandshakeFinishedAt = trace.TlsDone()
352+
reqInfo.LocalAddress = trace.LocalAddr()
350353

351354
if res != nil && endpoint.PrivateInstanceId != "" && !requestSentToRouteService(request) {
352355
setupStickySession(

proxy/round_tripper/trace.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import (
1111
// requestTracer holds trace data of a single request.
1212
type requestTracer struct {
1313
gotConn atomic.Bool
14-
connInfo atomic.Pointer[httptrace.GotConnInfo]
14+
connReused atomic.Bool
1515
wroteHeaders atomic.Bool
16+
localAddr atomic.Pointer[string]
1617

1718
// all times are stored as returned by time.Time{}.UnixNano()
1819
dnsStart atomic.Int64
@@ -26,8 +27,9 @@ type requestTracer struct {
2627
// Reset the trace data. Helpful when performing the same request again.
2728
func (t *requestTracer) Reset() {
2829
t.gotConn.Store(false)
29-
t.connInfo.Store(nil)
30+
t.connReused.Store(false)
3031
t.wroteHeaders.Store(false)
32+
t.localAddr.Store(nil)
3133
t.dnsStart.Store(0)
3234
t.dnsDone.Store(0)
3335
t.dialStart.Store(0)
@@ -49,11 +51,15 @@ func (t *requestTracer) WroteHeaders() bool {
4951
// ConnReused returns true if the traced request used an idle connection.
5052
// it returns false if no idle connection was used or if the information was unavailable.
5153
func (t *requestTracer) ConnReused() bool {
52-
info := t.connInfo.Load()
53-
if info != nil {
54-
return info.Reused
54+
return t.connReused.Load()
55+
}
56+
57+
func (t *requestTracer) LocalAddr() string {
58+
p := t.localAddr.Load()
59+
if p == nil {
60+
return ""
5561
}
56-
return false
62+
return *p
5763
}
5864

5965
func (t *requestTracer) DnsStart() time.Time {
@@ -121,7 +127,10 @@ func traceRequest(req *http.Request) (*http.Request, *requestTracer) {
121127
r2 := req.WithContext(httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
122128
GotConn: func(info httptrace.GotConnInfo) {
123129
t.gotConn.Store(true)
124-
t.connInfo.Store(&info)
130+
t.connReused.Store(info.Reused)
131+
la := info.Conn.LocalAddr().String()
132+
t.localAddr.Store(&la)
133+
125134
// FIXME: due to https://github.com/golang/go/issues/31259 this breaks our acceptance tests and is dangerous
126135
// disabled for now even though this will reduce the number of requests we can retry
127136
// if !info.Reused {

0 commit comments

Comments
 (0)