@marcel91200 / caddy Mirror · read-only / commits / f0e39817
Fork 0

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