@marcel91200 / caddy Mirror · read-only / commits / d16ede35
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 14, 05:15 UTC

metrics: Fix hidden panic while observing with bad exemplars (#3733)

* metrics: Fixing panic while observing with bad exemplars Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Minor cleanup The server is already added to the context. So, we can simply use that to get the server name, which is a field on the server. * Add integration test for auto HTTP->HTTPS redirects A test like this would have caught the problem in the first place Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>

Dave Henderson committed Sep 17, 2020 at 23:46 UTC d16ede358a2ec049bda28bf37e79c9cfcaa64c29
6 files changed +62 -41
caddytest/integration/autohttps_test.go new
+22
@@ -0,0 +1,22 @@
1 +package integration
2 +
3 +import (
4 + "net/http"
5 + "testing"
6 +
7 + "github.com/caddyserver/caddy/v2/caddytest"
8 +)
9 +
10 +func TestAutoHTTPtoHTTPSRedirects(t *testing.T) {
11 + tester := caddytest.NewTester(t)
12 + tester.InitServer(`
13 + {
14 + http_port 9080
15 + https_port 9443
16 + }
17 + localhost:9443
18 + respond "Yahaha! You found me!"
19 + `, "caddyfile")
20 +
21 + tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
22 +}
modules/caddyhttp/app.go
+1
@@ -155,6 +155,7 @@ func (app *App) Provision(ctx caddy.Context) error {
155
156 // prepare each server
157 for srvName, srv := range app.Servers {
158 + srv.name = srvName
159 srv.tlsApp = app.tlsApp
160 srv.logger = app.logger.Named("log")
161 srv.errorLogger = app.logger.Named("log.error")
modules/caddyhttp/metrics.go
+18 -34
@@ -82,44 +82,38 @@ func initHTTPMetrics() {
82 }, httpLabels)
83 }
84
85 -type ctxKeyServerName struct{}
86 -
85 // serverNameFromContext extracts the current server name from the context.
88 -// Returns "UNKNOWN" if none is available (should probably never happen?)
86 +// Returns "UNKNOWN" if none is available (should probably never happen).
87 func serverNameFromContext(ctx context.Context) string {
90 - srvName, ok := ctx.Value(ctxKeyServerName{}).(string)
91 - if !ok {
88 + srv, ok := ctx.Value(ServerCtxKey).(*Server)
89 + if !ok || srv == nil || srv.name == "" {
90 return "UNKNOWN"
91 }
94 - return srvName
92 + return srv.name
93 }
94
95 type metricsInstrumentedHandler struct {
98 - labels prometheus.Labels
99 - statusLabels prometheus.Labels
100 - mh MiddlewareHandler
96 + handler string
97 + mh MiddlewareHandler
98 }
99
103 -func newMetricsInstrumentedHandler(server, handler string, mh MiddlewareHandler) *metricsInstrumentedHandler {
100 +func newMetricsInstrumentedHandler(handler string, mh MiddlewareHandler) *metricsInstrumentedHandler {
101 httpMetrics.init.Do(func() {
102 initHTTPMetrics()
103 })
104
108 - labels := prometheus.Labels{"server": server, "handler": handler}
109 - statusLabels := prometheus.Labels{"server": server, "handler": handler, "code": "", "method": ""}
110 - return &metricsInstrumentedHandler{labels, statusLabels, mh}
105 + return &metricsInstrumentedHandler{handler, mh}
106 }
107
108 func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
114 - inFlight := httpMetrics.requestInFlight.With(h.labels)
109 + server := serverNameFromContext(r.Context())
110 + labels := prometheus.Labels{"server": server, "handler": h.handler}
111 + statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": r.Method}
112 +
113 + inFlight := httpMetrics.requestInFlight.With(labels)
114 inFlight.Inc()
115 defer inFlight.Dec()
116
118 - statusLabels := prometheus.Labels{"method": r.Method}
119 - for k, v := range h.labels {
120 - statusLabels[k] = v
121 - }
122 -
117 start := time.Now()
118
119 // This is a _bit_ of a hack - it depends on the ShouldBufferFunc always
@@ -128,35 +122,25 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
122 writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
123 statusLabels["code"] = sanitizeCode(status)
124 ttfb := time.Since(start).Seconds()
131 - observeWithExemplar(statusLabels, httpMetrics.responseDuration, ttfb)
125 + httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
126 return false
127 })
128 wrec := NewResponseRecorder(w, nil, writeHeaderRecorder)
129 err := h.mh.ServeHTTP(wrec, r, next)
130 dur := time.Since(start).Seconds()
137 - httpMetrics.requestCount.With(h.labels).Inc()
131 + httpMetrics.requestCount.With(labels).Inc()
132 if err != nil {
139 - httpMetrics.requestErrors.With(h.labels).Inc()
133 + httpMetrics.requestErrors.With(labels).Inc()
134 return err
135 }
136
143 - observeWithExemplar(statusLabels, httpMetrics.requestDuration, dur)
144 - observeWithExemplar(statusLabels, httpMetrics.requestSize, float64(computeApproximateRequestSize(r)))
137 + httpMetrics.requestDuration.With(statusLabels).Observe(dur)
138 + httpMetrics.requestSize.With(statusLabels).Observe(float64(computeApproximateRequestSize(r)))
139 httpMetrics.responseSize.With(statusLabels).Observe(float64(wrec.Size()))
140
141 return nil
142 }
143
150 -func observeWithExemplar(l prometheus.Labels, o *prometheus.HistogramVec, value float64) {
151 - obs := o.With(l)
152 - if oe, ok := obs.(prometheus.ExemplarObserver); ok {
153 - oe.ObserveWithExemplar(value, l)
154 - return
155 - }
156 - // _should_ be a noop, but here just in case...
157 - obs.Observe(value)
158 -}
159 -
144 func sanitizeCode(code int) string {
145 if code == 0 {
146 return "200"
modules/caddyhttp/metrics_test.go
+16 -1
@@ -1,6 +1,7 @@
1 package caddyhttp
2
3 import (
4 + "context"
5 "errors"
6 "net/http"
7 "net/http/httptest"
@@ -9,6 +10,20 @@ import (
10 "github.com/prometheus/client_golang/prometheus/testutil"
11 )
12
13 +func TestServerNameFromContext(t *testing.T) {
14 + ctx := context.Background()
15 + expected := "UNKNOWN"
16 + if actual := serverNameFromContext(ctx); actual != expected {
17 + t.Errorf("Not equal: expected %q, but got %q", expected, actual)
18 + }
19 +
20 + in := "foo"
21 + ctx = context.WithValue(ctx, ServerCtxKey, &Server{name: in})
22 + if actual := serverNameFromContext(ctx); actual != in {
23 + t.Errorf("Not equal: expected %q, but got %q", in, actual)
24 + }
25 +}
26 +
27 func TestMetricsInstrumentedHandler(t *testing.T) {
28 handlerErr := errors.New("oh noes")
29 response := []byte("hello world!")
@@ -26,7 +41,7 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
41 return h.ServeHTTP(w, r)
42 })
43
29 - ih := newMetricsInstrumentedHandler("foo", "bar", mh)
44 + ih := newMetricsInstrumentedHandler("bar", mh)
45
46 r := httptest.NewRequest("GET", "/", nil)
47 w := httptest.NewRecorder()
modules/caddyhttp/routes.go
+3 -6
@@ -243,12 +243,9 @@ func wrapRoute(route Route) Middleware {
243 // pointer into its own stack frame to preserve it so it
244 // won't be overwritten in future loop iterations.
245 func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler) Middleware {
246 - // first, wrap the middleware with metrics instrumentation
247 - metricsHandler := newMetricsInstrumentedHandler(
248 - serverNameFromContext(ctx.Context),
249 - caddy.GetModuleName(mh),
250 - mh,
251 - )
246 + // wrap the middleware with metrics instrumentation
247 + metricsHandler := newMetricsInstrumentedHandler(caddy.GetModuleName(mh), mh)
248 +
249 return func(next Handler) Handler {
250 // copy the next handler (it's an interface, so it's
251 // just a very lightweight copy of a pointer); this
modules/caddyhttp/server.go
+2
@@ -122,6 +122,8 @@ type Server struct {
122 // ⚠️ Experimental feature; subject to change or removal.
123 AllowH2C bool `json:"allow_h2c,omitempty"`
124
125 + name string
126 +
127 primaryHandlerChain Handler
128 errorHandlerChain Handler
129 listenerWrappers []caddy.ListenerWrapper