Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 03:00 UTC
logging: Add traceID field to access logs when tracing is active (#5507)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Dave Henderson committed
Apr 26, 2023 at 22:46 UTC
f0e39817746903bb24948893348ae1cab67f1e46
7 files changed
+236
-53
go.mod
+3
-1
@@ -23,6 +23,7 @@ require (
23
github.com/smallstep/truststore v0.12.1
24
github.com/spf13/cobra v1.6.1
25
github.com/spf13/pflag v1.0.5
26
+ github.com/stretchr/testify v1.8.2
27
github.com/tailscale/tscert v0.0.0-20230124224810-c6dc1f4049b2
28
github.com/yuin/goldmark v1.5.4
29
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
@@ -50,6 +51,7 @@ require (
51
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
52
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
53
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
54
+ github.com/pmezard/go-difflib v1.0.0 // indirect
55
github.com/quic-go/qpack v0.4.0 // indirect
56
github.com/quic-go/qtls-go1-19 v0.2.1 // indirect
57
github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
@@ -128,7 +130,7 @@ require (
130
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
131
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect
132
go.opentelemetry.io/otel/metric v0.37.0 // indirect
131
- go.opentelemetry.io/otel/trace v1.14.0 // indirect
133
+ go.opentelemetry.io/otel/trace v1.14.0
134
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
135
go.step.sm/cli-utils v0.7.5 // indirect
136
go.step.sm/crypto v0.23.2
go.sum
+1
@@ -659,6 +659,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
659
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
660
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
661
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
662
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
663
github.com/tailscale/tscert v0.0.0-20230124224810-c6dc1f4049b2 h1:TrgfmCXwtWyFw85UkRGXt9qZRzdzt3nWt2Rerdecn0w=
664
github.com/tailscale/tscert v0.0.0-20230124224810-c6dc1f4049b2/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
665
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
modules/caddyhttp/logging.go
+19
-3
@@ -20,6 +20,7 @@ import (
20
"net/http"
21
"strings"
22
23
+ "github.com/caddyserver/caddy/v2"
24
"go.uber.org/zap"
25
"go.uber.org/zap/zapcore"
26
)
@@ -139,6 +140,21 @@ func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
140
return
141
}
142
142
-// Variable name used to indicate that this request
143
-// should be omitted from the access logs
144
-const SkipLogVar = "skip_log"
143
+// ExtraLogFields is a list of extra fields to log with every request.
144
+type ExtraLogFields struct {
145
+ fields []zapcore.Field
146
+}
147
+
148
+// Add adds a field to the list of extra fields to log.
149
+func (e *ExtraLogFields) Add(field zap.Field) {
150
+ e.fields = append(e.fields, field)
151
+}
152
+
153
+const (
154
+ // Variable name used to indicate that this request
155
+ // should be omitted from the access logs
156
+ SkipLogVar string = "skip_log"
157
+
158
+ // For adding additional fields to the access logs
159
+ ExtraLogFieldsCtxKey caddy.CtxKey = "extra_log_fields"
160
+)
modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+2
-7
@@ -254,9 +254,7 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
254
// if we didn't get a split result here.
255
// See https://github.com/caddyserver/caddy/issues/3718
256
if pathInfo == "" {
257
- if remainder, ok := repl.GetString("http.matchers.file.remainder"); ok {
258
- pathInfo = remainder
259
- }
257
+ pathInfo, _ = repl.GetString("http.matchers.file.remainder")
258
}
259
260
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
@@ -286,10 +284,7 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
284
reqHost = r.Host
285
}
286
289
- authUser := ""
290
- if val, ok := repl.Get("http.auth.user.id"); ok {
291
- authUser = val.(string)
292
- }
287
+ authUser, _ := repl.GetString("http.auth.user.id")
288
289
// Some variables are unused but cleared explicitly to prevent
290
// the parent environment from interfering.
modules/caddyhttp/server.go
+55
-40
@@ -282,46 +282,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
282
// capture the original version of the request
283
accLog := s.accessLogger.With(loggableReq)
284
285
- defer func() {
286
- // this request may be flagged as omitted from the logs
287
- if skipLog, ok := GetVar(r.Context(), SkipLogVar).(bool); ok && skipLog {
288
- return
289
- }
290
-
291
- repl.Set("http.response.status", wrec.Status()) // will be 0 if no response is written by us (Go will write 200 to client)
292
- repl.Set("http.response.size", wrec.Size())
293
- repl.Set("http.response.duration", duration)
294
- repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666)
295
-
296
- logger := accLog
297
- if s.Logs != nil {
298
- logger = s.Logs.wrapLogger(logger, r.Host)
299
- }
300
-
301
- log := logger.Info
302
- if wrec.Status() >= 400 {
303
- log = logger.Error
304
- }
305
-
306
- userID, _ := repl.GetString("http.auth.user.id")
307
-
308
- reqBodyLength := 0
309
- if bodyReader != nil {
310
- reqBodyLength = bodyReader.Length
311
- }
312
-
313
- log("handled request",
314
- zap.Int("bytes_read", reqBodyLength),
315
- zap.String("user_id", userID),
316
- zap.Duration("duration", duration),
317
- zap.Int("size", wrec.Size()),
318
- zap.Int("status", wrec.Status()),
319
- zap.Object("resp_headers", LoggableHTTPHeader{
320
- Header: wrec.Header(),
321
- ShouldLogCredentials: shouldLogCredentials,
322
- }),
323
- )
324
- }()
285
+ defer s.logRequest(accLog, r, wrec, &duration, repl, bodyReader, shouldLogCredentials)
286
}
287
288
start := time.Now()
@@ -703,6 +664,57 @@ func (s *Server) shouldLogRequest(r *http.Request) bool {
664
return !s.Logs.SkipUnmappedHosts
665
}
666
667
+// logRequest logs the request to access logs, unless skipped.
668
+func (s *Server) logRequest(
669
+ accLog *zap.Logger, r *http.Request, wrec ResponseRecorder, duration *time.Duration,
670
+ repl *caddy.Replacer, bodyReader *lengthReader, shouldLogCredentials bool,
671
+) {
672
+ // this request may be flagged as omitted from the logs
673
+ if skipLog, ok := GetVar(r.Context(), SkipLogVar).(bool); ok && skipLog {
674
+ return
675
+ }
676
+
677
+ repl.Set("http.response.status", wrec.Status()) // will be 0 if no response is written by us (Go will write 200 to client)
678
+ repl.Set("http.response.size", wrec.Size())
679
+ repl.Set("http.response.duration", duration)
680
+ repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666)
681
+
682
+ logger := accLog
683
+ if s.Logs != nil {
684
+ logger = s.Logs.wrapLogger(logger, r.Host)
685
+ }
686
+
687
+ log := logger.Info
688
+ if wrec.Status() >= 400 {
689
+ log = logger.Error
690
+ }
691
+
692
+ userID, _ := repl.GetString("http.auth.user.id")
693
+
694
+ reqBodyLength := 0
695
+ if bodyReader != nil {
696
+ reqBodyLength = bodyReader.Length
697
+ }
698
+
699
+ extra := r.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields)
700
+
701
+ fieldCount := 6
702
+ fields := make([]zapcore.Field, 0, fieldCount+len(extra.fields))
703
+ fields = append(fields,
704
+ zap.Int("bytes_read", reqBodyLength),
705
+ zap.String("user_id", userID),
706
+ zap.Duration("duration", *duration),
707
+ zap.Int("size", wrec.Size()),
708
+ zap.Int("status", wrec.Status()),
709
+ zap.Object("resp_headers", LoggableHTTPHeader{
710
+ Header: wrec.Header(),
711
+ ShouldLogCredentials: shouldLogCredentials,
712
+ }))
713
+ fields = append(fields, extra.fields...)
714
+
715
+ log("handled request", fields...)
716
+}
717
+
718
// protocol returns true if the protocol proto is configured/enabled.
719
func (s *Server) protocol(proto string) bool {
720
for _, p := range s.Protocols {
@@ -738,6 +750,9 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter
750
751
var url2 url.URL // avoid letting this escape to the heap
752
ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
753
+
754
+ ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields))
755
+
756
r = r.WithContext(ctx)
757
758
// once the pointer to the request won't change
modules/caddyhttp/server_test.go
new
+146
@@ -0,0 +1,146 @@
1
+package caddyhttp
2
+
3
+import (
4
+ "bytes"
5
+ "context"
6
+ "io"
7
+ "net/http"
8
+ "net/http/httptest"
9
+ "testing"
10
+ "time"
11
+
12
+ "github.com/stretchr/testify/assert"
13
+ "go.uber.org/zap"
14
+ "go.uber.org/zap/zapcore"
15
+)
16
+
17
+type writeFunc func(p []byte) (int, error)
18
+
19
+type nopSyncer writeFunc
20
+
21
+func (n nopSyncer) Write(p []byte) (int, error) {
22
+ return n(p)
23
+}
24
+
25
+func (n nopSyncer) Sync() error {
26
+ return nil
27
+}
28
+
29
+// testLogger returns a logger and a buffer to which the logger writes. The
30
+// buffer can be read for asserting log output.
31
+func testLogger(wf writeFunc) *zap.Logger {
32
+ ws := nopSyncer(wf)
33
+ encoderCfg := zapcore.EncoderConfig{
34
+ MessageKey: "msg",
35
+ LevelKey: "level",
36
+ NameKey: "logger",
37
+ EncodeLevel: zapcore.LowercaseLevelEncoder,
38
+ EncodeTime: zapcore.ISO8601TimeEncoder,
39
+ EncodeDuration: zapcore.StringDurationEncoder,
40
+ }
41
+ core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), ws, zap.DebugLevel)
42
+
43
+ return zap.New(core)
44
+}
45
+
46
+func TestServer_LogRequest(t *testing.T) {
47
+ s := &Server{}
48
+
49
+ ctx := context.Background()
50
+ ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields))
51
+ req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
52
+ rec := httptest.NewRecorder()
53
+ wrec := NewResponseRecorder(rec, nil, nil)
54
+
55
+ duration := 50 * time.Millisecond
56
+ repl := NewTestReplacer(req)
57
+ bodyReader := &lengthReader{Source: req.Body}
58
+ shouldLogCredentials := false
59
+
60
+ buf := bytes.Buffer{}
61
+ accLog := testLogger(buf.Write)
62
+ s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, shouldLogCredentials)
63
+
64
+ assert.JSONEq(t, `{
65
+ "msg":"handled request", "level":"info", "bytes_read":0,
66
+ "duration":"50ms", "resp_headers": {}, "size":0,
67
+ "status":0, "user_id":""
68
+ }`, buf.String())
69
+}
70
+
71
+func TestServer_LogRequest_WithTraceID(t *testing.T) {
72
+ s := &Server{}
73
+
74
+ extra := new(ExtraLogFields)
75
+ ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra)
76
+ extra.Add(zap.String("traceID", "1234567890abcdef"))
77
+
78
+ req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
79
+ rec := httptest.NewRecorder()
80
+ wrec := NewResponseRecorder(rec, nil, nil)
81
+
82
+ duration := 50 * time.Millisecond
83
+ repl := NewTestReplacer(req)
84
+ bodyReader := &lengthReader{Source: req.Body}
85
+ shouldLogCredentials := false
86
+
87
+ buf := bytes.Buffer{}
88
+ accLog := testLogger(buf.Write)
89
+ s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, shouldLogCredentials)
90
+
91
+ assert.JSONEq(t, `{
92
+ "msg":"handled request", "level":"info", "bytes_read":0,
93
+ "duration":"50ms", "resp_headers": {}, "size":0,
94
+ "status":0, "user_id":"",
95
+ "traceID":"1234567890abcdef"
96
+ }`, buf.String())
97
+}
98
+
99
+func BenchmarkServer_LogRequest(b *testing.B) {
100
+ s := &Server{}
101
+
102
+ extra := new(ExtraLogFields)
103
+ ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra)
104
+
105
+ req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
106
+ rec := httptest.NewRecorder()
107
+ wrec := NewResponseRecorder(rec, nil, nil)
108
+
109
+ duration := 50 * time.Millisecond
110
+ repl := NewTestReplacer(req)
111
+ bodyReader := &lengthReader{Source: req.Body}
112
+
113
+ buf := io.Discard
114
+ accLog := testLogger(buf.Write)
115
+
116
+ b.ResetTimer()
117
+
118
+ for i := 0; i < b.N; i++ {
119
+ s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, false)
120
+ }
121
+}
122
+
123
+func BenchmarkServer_LogRequest_WithTraceID(b *testing.B) {
124
+ s := &Server{}
125
+
126
+ extra := new(ExtraLogFields)
127
+ ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra)
128
+ extra.Add(zap.String("traceID", "1234567890abcdef"))
129
+
130
+ req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
131
+ rec := httptest.NewRecorder()
132
+ wrec := NewResponseRecorder(rec, nil, nil)
133
+
134
+ duration := 50 * time.Millisecond
135
+ repl := NewTestReplacer(req)
136
+ bodyReader := &lengthReader{Source: req.Body}
137
+
138
+ buf := io.Discard
139
+ accLog := testLogger(buf.Write)
140
+
141
+ b.ResetTimer()
142
+
143
+ for i := 0; i < b.N; i++ {
144
+ s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, false)
145
+ }
146
+}
modules/caddyhttp/tracing/tracer.go
+10
-2
@@ -14,6 +14,7 @@ import (
14
"go.opentelemetry.io/otel/sdk/resource"
15
sdktrace "go.opentelemetry.io/otel/sdk/trace"
16
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
17
+ "go.opentelemetry.io/otel/trace"
18
"go.uber.org/zap"
19
)
20
@@ -81,8 +82,15 @@ func newOpenTelemetryWrapper(
82
83
// serveHTTP injects a tracing context and call the next handler.
84
func (ot *openTelemetryWrapper) serveHTTP(w http.ResponseWriter, r *http.Request) {
84
- ot.propagators.Inject(r.Context(), propagation.HeaderCarrier(r.Header))
85
- next := r.Context().Value(nextCallCtxKey).(*nextCall)
85
+ ctx := r.Context()
86
+ ot.propagators.Inject(ctx, propagation.HeaderCarrier(r.Header))
87
+ spanCtx := trace.SpanContextFromContext(ctx)
88
+ if spanCtx.IsValid() {
89
+ if extra, ok := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields); ok {
90
+ extra.Add(zap.String("traceID", spanCtx.TraceID().String()))
91
+ }
92
+ }
93
+ next := ctx.Value(nextCallCtxKey).(*nextCall)
94
next.err = next.next.ServeHTTP(w, r)
95
}
96