Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f0cf0ad

Browse files
authored
feat: log additional known non-sensitive query param fields in the httpmw logger (#19532)
Blink helped here but it's suggestion was to have a set map of sensitive fields based on predefined constants in various files, such as the api token string names. For now we'll add additional query param logging for fields we know are safe/that we want to log, such as query pagination/limit fields and ID list counts which may help identify P99 DB query latencies. --------- Signed-off-by: Callum Styan <[email protected]>
1 parent d274f83 commit f0cf0ad

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

coderd/httpmw/loggermw/logger.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"net/url"
8+
"strconv"
9+
"strings"
710
"sync"
811
"time"
912

@@ -15,6 +18,59 @@ import (
1518
"github.com/coder/coder/v2/coderd/tracing"
1619
)
1720

21+
var (
22+
safeParams = []string{"page", "limit", "offset"}
23+
countParams = []string{"ids", "template_ids"}
24+
)
25+
26+
func safeQueryParams(params url.Values) []slog.Field {
27+
if len(params) == 0 {
28+
return nil
29+
}
30+
31+
fields := make([]slog.Field, 0, len(params))
32+
for key, values := range params {
33+
// Check if this parameter should be included
34+
for _, pattern := range safeParams {
35+
if strings.EqualFold(key, pattern) {
36+
// Prepend query parameters in the log line to ensure we don't have issues with collisions
37+
// in case any other internal logging fields already log fields with similar names
38+
fieldName := "query_" + key
39+
40+
// Log the actual values for non-sensitive parameters
41+
if len(values) == 1 {
42+
fields = append(fields, slog.F(fieldName, values[0]))
43+
continue
44+
}
45+
fields = append(fields, slog.F(fieldName, values))
46+
}
47+
}
48+
// Some query params we just want to log the count of the params length
49+
for _, pattern := range countParams {
50+
if !strings.EqualFold(key, pattern) {
51+
continue
52+
}
53+
count := 0
54+
55+
// Prepend query parameters in the log line to ensure we don't have issues with collisions
56+
// in case any other internal logging fields already log fields with similar names
57+
fieldName := "query_" + key
58+
59+
// Count comma-separated values for CSV format
60+
for _, v := range values {
61+
if strings.Contains(v, ",") {
62+
count += len(strings.Split(v, ","))
63+
continue
64+
}
65+
count++
66+
}
67+
// For logging we always want strings
68+
fields = append(fields, slog.F(fieldName+"_count", strconv.Itoa(count)))
69+
}
70+
}
71+
return fields
72+
}
73+
1874
func Logger(log slog.Logger) func(next http.Handler) http.Handler {
1975
return func(next http.Handler) http.Handler {
2076
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@@ -39,6 +95,11 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler {
3995
slog.F("start", start),
4096
)
4197

98+
// Add safe query parameters to the log
99+
if queryFields := safeQueryParams(r.URL.Query()); len(queryFields) > 0 {
100+
httplog = httplog.With(queryFields...)
101+
}
102+
42103
logContext := NewRequestLogger(httplog, r.Method, start)
43104

44105
ctx := WithRequestLogger(r.Context(), logContext)

coderd/httpmw/loggermw/logger_internal_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"net/http"
66
"net/http/httptest"
7+
"net/url"
78
"slices"
89
"strings"
910
"sync"
@@ -292,6 +293,76 @@ func TestRequestLogger_RouteParamsLogging(t *testing.T) {
292293
}
293294
}
294295

296+
func TestSafeQueryParams(t *testing.T) {
297+
t.Parallel()
298+
299+
tests := []struct {
300+
name string
301+
params url.Values
302+
expected map[string]interface{}
303+
}{
304+
{
305+
name: "safe parameters",
306+
params: url.Values{
307+
"page": []string{"1"},
308+
"limit": []string{"10"},
309+
"filter": []string{"active"},
310+
"sort": []string{"name"},
311+
"offset": []string{"2"},
312+
"ids": []string{"some-id,another-id", "second-param"},
313+
"template_ids": []string{"some-id,another-id", "second-param"},
314+
},
315+
expected: map[string]interface{}{
316+
"query_page": "1",
317+
"query_limit": "10",
318+
"query_offset": "2",
319+
"query_ids_count": "3",
320+
"query_template_ids_count": "3",
321+
},
322+
},
323+
{
324+
name: "unknown/sensitive parameters",
325+
params: url.Values{
326+
"token": []string{"secret-token"},
327+
"api_key": []string{"secret-key"},
328+
"coder_signed_app_token": []string{"jwt-token"},
329+
"coder_application_connect_api_key": []string{"encrypted-key"},
330+
"client_secret": []string{"oauth-secret"},
331+
"code": []string{"auth-code"},
332+
},
333+
expected: map[string]interface{}{},
334+
},
335+
{
336+
name: "mixed parameters",
337+
params: url.Values{
338+
"page": []string{"1"},
339+
"token": []string{"secret"},
340+
"filter": []string{"active"},
341+
},
342+
expected: map[string]interface{}{
343+
"query_page": "1",
344+
},
345+
},
346+
}
347+
348+
for _, tt := range tests {
349+
tt := tt
350+
t.Run(tt.name, func(t *testing.T) {
351+
t.Parallel()
352+
353+
fields := safeQueryParams(tt.params)
354+
355+
// Convert fields to map for easier comparison
356+
result := make(map[string]interface{})
357+
for _, field := range fields {
358+
result[field.Name] = field.Value
359+
}
360+
361+
require.Equal(t, tt.expected, result)
362+
})
363+
}
364+
}
365+
295366
type fakeSink struct {
296367
entries []slog.SinkEntry
297368
newEntries chan slog.SinkEntry

0 commit comments

Comments
 (0)