mirror of
https://github.com/coder/coder.git
synced 2026-11-01 04:33:58 +08:00
## What Adds `client_session_id` correlation to coderd's HTTP request handling, per the [Connection log collection and correlation RFC](https://www.notion.so/coderhq/Connection-log-collection-and-correlation-36ed579be5928025a56cd11fe58661fb). Clients attach a per-session correlation ID to every API request via W3C baggage using the `client_session_id` key. This change makes coderd's tracing middleware read that baggage member and: - add `client_session_id` to the **per-request log context** so all logs for a request (and the handlers it calls) can be correlated by a single ID, and - set `client_session_id` as a **span attribute** when tracing is enabled. Per RFC requirement 6.1, the value is added to the log context **even when tracing is disabled** (the middleware previously returned early when no tracer provider was configured, so baggage was never read). The `client_session_id` is validated as a 32-character hexadecimal string (a 16-byte value, per RFC requirement 1) to guard against logging arbitrary client-controlled baggage values. ## Scope This is `DEVEX-659` and is intentionally limited to the coderd tracing middleware. It is the first piece of a stack: the web terminal client change (`DEVEX-663`) that generates and sends the `client_session_id` will be stacked on top of this PR. No client currently sends `client_session_id` baggage, so this change is a no-op until the client work lands. ## Testing - `coderd/tracing`: new unit tests cover `validSessionID`, baggage extraction (`sessionIDFromHeaders`), and the middleware end to end, asserting `client_session_id` lands on the log context with tracing enabled **and** disabled, is exposed as a span attribute when tracing is enabled, and that absent/malformed baggage is ignored. - Existing `Test_Middleware` route-matching behavior is unchanged. <details> <summary>Design notes / decision log</summary> - **Where the value is read:** the existing `tracing.Middleware` runs high in the coderd middleware stack (`coderd/coderd.go`), before request-id and request-logger middleware, and already matches the `/api`, `/api/**`, app proxy, and external-auth routes. Reading baggage here means the `client_session_id` is on the context before the request logger and handlers run, so it flows into all downstream `slog` calls that use the request context. This mirrors the existing `request_id` pattern in `httpmw.AttachRequestID` (`slog.With(ctx, ...)` + span attribute). - **Works when tracing is off:** the middleware now gates only on the route matcher, extracts baggage and adds `client_session_id` to the log context for all matched routes, and only then branches on whether a tracer is configured. When a tracer is present, `client_session_id` is additionally set as a span attribute. - **Explicit baggage propagator:** extraction uses `propagation.Baggage{}` directly rather than the global text map propagator, so it does not depend on the global propagator being configured (also makes it deterministic in tests). - **Validation:** only a 32-char hex string is accepted (lower or upper case). Malformed values are dropped rather than logged, preventing log/attribute pollution from arbitrary client-supplied baggage. - **Out of scope for this PR (tracked elsewhere):** client generation/sending of `client_session_id` (`DEVEX-663`, web terminal), the equivalent agent-side middleware (RFC 6.2), `connection_logs.client_session_id` (RFC 12), and additional connection state-change logging (RFC 7-13). </details> --- _Opened by Coder Agents on behalf of @aqandrew._
91 lines
2.1 KiB
Go
91 lines
2.1 KiB
Go
package tracing
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestValidSessionID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
id string
|
|
valid bool
|
|
}{
|
|
{"LowerHex", "0123456789abcdef0123456789abcdef", true},
|
|
{"UpperHex", "0123456789ABCDEF0123456789ABCDEF", false},
|
|
{"MixedCase", "0123456789abcdef0123456789ABCDEF", false},
|
|
{"Empty", "", false},
|
|
{"TooShort", "0123456789abcdef0123456789abcde", false},
|
|
{"TooLong", "0123456789abcdef0123456789abcdef0", false},
|
|
{"NonHex", "0123456789abcdef0123456789abcdeg", false},
|
|
{"Uuid", "0123456789ab-cdef-0123456789abcde", false},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, c.valid, validSessionID(c.id))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionIDFromHeaders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const validID = "0123456789abcdef0123456789abcdef"
|
|
|
|
cases := []struct {
|
|
name string
|
|
baggage string
|
|
want string
|
|
}{
|
|
{"Valid", SessionIDBaggageKey + "=" + validID, validID},
|
|
{"WithOtherMembers", "foo=bar," + SessionIDBaggageKey + "=" + validID + ",baz=qux", validID},
|
|
{"Missing", "foo=bar", ""},
|
|
{"NoHeader", "", ""},
|
|
{"Malformed", SessionIDBaggageKey + "=not-a-hex-value", ""},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := http.Header{}
|
|
if c.baggage != "" {
|
|
h.Set("baggage", c.baggage)
|
|
}
|
|
require.Equal(t, c.want, sessionIDFromHeaders(h))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionIDFromQueryString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const validID = "0123456789abcdef0123456789abcdef"
|
|
|
|
cases := []struct {
|
|
name string
|
|
query url.Values
|
|
want string
|
|
}{
|
|
{"Valid", url.Values{SessionIDBaggageKey: {validID}}, validID},
|
|
{"Missing", url.Values{"foo": {"bar"}}, ""},
|
|
{"Empty", url.Values{}, ""},
|
|
{"Malformed", url.Values{SessionIDBaggageKey: {"not-a-hex-value"}}, ""},
|
|
{"Uppercase", url.Values{SessionIDBaggageKey: {"0123456789ABCDEF0123456789ABCDEF"}}, ""},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, c.want, sessionIDFromQueryString(c.query))
|
|
})
|
|
}
|
|
}
|