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