summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--admin.go45
-rw-r--r--caddyconfig/httpcaddyfile/directives.go1
-rw-r--r--go.mod1
-rw-r--r--go.sum32
-rw-r--r--metrics.go32
-rw-r--r--modules/caddyhttp/app.go2
-rw-r--r--modules/caddyhttp/metrics.go195
-rw-r--r--modules/caddyhttp/metrics_test.go66
-rw-r--r--modules/caddyhttp/routes.go12
-rw-r--r--modules/metrics/adminmetrics.go55
-rw-r--r--modules/metrics/metrics.go109
-rw-r--r--modules/standard/imports.go1
12 files changed, 518 insertions, 33 deletions
diff --git a/admin.go b/admin.go
index acf737e..4ce2099 100644
--- a/admin.go
+++ b/admin.go
@@ -35,6 +35,8 @@ import (
"sync"
"time"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
)
@@ -107,34 +109,53 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
mux: http.NewServeMux(),
}
+ addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
+ labels := prometheus.Labels{"path": pattern, "handler": handlerLabel}
+ h = promhttp.InstrumentHandlerCounter(
+ adminMetrics.requestCount.MustCurryWith(labels),
+ h,
+ )
+ muxWrap.mux.Handle(pattern, h)
+ }
// addRoute just calls muxWrap.mux.Handle after
// wrapping the handler with error handling
- addRoute := func(pattern string, h AdminHandler) {
+ addRoute := func(pattern string, handlerLabel string, h AdminHandler) {
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h.ServeHTTP(w, r)
+ if err != nil {
+ labels := prometheus.Labels{
+ "path": pattern,
+ "handler": handlerLabel,
+ "method": r.Method,
+ }
+ adminMetrics.requestErrors.With(labels).Inc()
+ }
muxWrap.handleError(w, r, err)
})
- muxWrap.mux.Handle(pattern, wrapper)
+ addRouteWithMetrics(pattern, handlerLabel, wrapper)
}
+ const handlerLabel = "admin"
+
// register standard config control endpoints
- addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig))
- addRoute("/id/", AdminHandlerFunc(handleConfigID))
- addRoute("/stop", AdminHandlerFunc(handleStop))
+ addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
+ addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
+ addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
// register debugging endpoints
- muxWrap.mux.HandleFunc("/debug/pprof/", pprof.Index)
- muxWrap.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
- muxWrap.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
- muxWrap.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
- muxWrap.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
- muxWrap.mux.Handle("/debug/vars", expvar.Handler())
+ addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
+ addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline))
+ addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile))
+ addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol))
+ addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace))
+ addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler())
// register third-party module endpoints
for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter)
+ handlerLabel := m.ID.Name()
for _, route := range router.Routes() {
- addRoute(route.Pattern, route.Handler)
+ addRoute(route.Pattern, handlerLabel, route.Handler)
}
}
diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go
index d6af97d..9e17eeb 100644
--- a/caddyconfig/httpcaddyfile/directives.go
+++ b/caddyconfig/httpcaddyfile/directives.go
@@ -63,6 +63,7 @@ var directiveOrder = []string{
// handlers that typically respond to requests
"respond",
+ "metrics",
"reverse_proxy",
"php_fastcgi",
"file_server",
diff --git a/go.mod b/go.mod
index fa5913a..7f9cea0 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
github.com/mholt/acmez v0.1.1-0.20200811184240-dc9c5f05ed1e
github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/naoina/toml v0.1.1
+ github.com/prometheus/client_golang v1.7.1
github.com/smallstep/certificates v0.15.1
github.com/smallstep/cli v0.15.0
github.com/smallstep/nosql v0.3.0
diff --git a/go.sum b/go.sum
index 381988e..545c2e1 100644
--- a/go.sum
+++ b/go.sum
@@ -74,6 +74,7 @@ github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P
github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U=
@@ -85,6 +86,7 @@ github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
@@ -140,9 +142,7 @@ github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.8.0/go.mod h1:3l45GVGkyrnYNl9HoIjnp2NnNWvh6hLAqD8yTfGjnw8=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
-github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -196,7 +196,6 @@ github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
@@ -209,9 +208,7 @@ github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
@@ -291,7 +288,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
@@ -309,6 +305,7 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o=
@@ -380,6 +377,7 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/acmez v0.1.1-0.20200811184240-dc9c5f05ed1e h1:F5E9CM0APrZtTfowJfvUBmrCjuHLfslZyFlNTEB7UV4=
github.com/mholt/acmez v0.1.1-0.20200811184240-dc9c5f05ed1e/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
@@ -425,7 +423,6 @@ github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FW
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
@@ -434,7 +431,6 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
@@ -458,20 +454,28 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
+github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
+github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@@ -527,11 +531,9 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE=
-github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 h1:kSImCuenAkXtCaBeQ1UhmzzJGRhSm8sVH7I3sHE2Qdg=
github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
-github.com/smallstep/certificates v0.15.0 h1:riIt3fBWuenlvHoqkVuUOxQbhtxFQMO1ziSr7usb1J0=
github.com/smallstep/certificates v0.15.0/go.mod h1:awyVXYIVn4J9ANSSnDuA3FjKia7+QixaqorGW8CKGq8=
github.com/smallstep/certificates v0.15.1 h1:Es2IGgU4D0RE4OKys/VB1KvAuGuZwkxR7D9A0DeM/Ow=
github.com/smallstep/certificates v0.15.1/go.mod h1:hDmNejfuVTDDe1NBIKrhL9xjELRJb2ZFM+bDXwSW9xI=
@@ -627,7 +629,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.step.sm/crypto v0.0.0-20200805202904-ec18b6df3cf0/go.mod h1:8VYxmvSKt5yOTBx3MGsD2Gk4F1Es/3FIxrjnfeYWE8U=
-go.step.sm/crypto v0.1.1 h1:xg3kUS30hEnwgbxtKwq9a4MJaeiU616HSug60LU9B2E=
go.step.sm/crypto v0.1.1/go.mod h1:cIoSWTfTQ5xqvwTeZH9ZXZzi6jdMepjK4A/TDWMUvw8=
go.step.sm/crypto v0.2.0 h1:Rx+XqrNO4ZGHWlT9QCXls2L9pvcNiI23zEpAq0fctvY=
go.step.sm/crypto v0.2.0/go.mod h1:YNLnHj4JgABFoRkUq8brkscIB9THdiJUFoDxLQw1tww=
@@ -769,10 +770,10 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -864,9 +865,7 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171 h1:xes2Q2k+d/+YNXVw0FpZkIDJiaux4OVrRKXRAzH6A0U=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 h1:LCO0fg4kb6WwkXQXRQQgUYsFeFb5taTX5WAx5O/Vt28=
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
@@ -889,9 +888,7 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc h1:TnonUr8u3himcMY0vSh23jFOXA+cnucl1gB6EQTReBI=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
@@ -903,7 +900,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
diff --git a/metrics.go b/metrics.go
new file mode 100644
index 0000000..a7ff3b7
--- /dev/null
+++ b/metrics.go
@@ -0,0 +1,32 @@
+package caddy
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+)
+
+// define and register the metrics used in this package.
+func init() {
+ prometheus.MustRegister(prometheus.NewBuildInfoCollector())
+
+ const ns, sub = "caddy", "admin"
+
+ adminMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "http_requests_total",
+ Help: "Counter of requests made to the Admin API's HTTP endpoints.",
+ }, []string{"handler", "path", "code", "method"})
+ adminMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "http_request_errors_total",
+ Help: "Number of requests resulting in middleware errors.",
+ }, []string{"handler", "path", "method"})
+}
+
+// adminMetrics is a collection of metrics that can be tracked for the admin API.
+var adminMetrics = struct {
+ requestCount *prometheus.CounterVec
+ requestErrors *prometheus.CounterVec
+}{}
diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go
index 375ca4d..c8bbc95 100644
--- a/modules/caddyhttp/app.go
+++ b/modules/caddyhttp/app.go
@@ -226,6 +226,8 @@ func (app *App) Provision(ctx caddy.Context) error {
// route handler so that important security checks are done, etc.
primaryRoute := emptyHandler
if srv.Routes != nil {
+ // inject the server name for observability purposes
+ ctx.Context = contextWithServerName(ctx.Context, srvName)
err := srv.Routes.ProvisionHandlers(ctx)
if err != nil {
return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err)
diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go
new file mode 100644
index 0000000..dbf1033
--- /dev/null
+++ b/modules/caddyhttp/metrics.go
@@ -0,0 +1,195 @@
+package caddyhttp
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+)
+
+var httpMetrics = struct {
+ init sync.Once
+ requestInFlight *prometheus.GaugeVec
+ requestCount *prometheus.CounterVec
+ requestErrors *prometheus.CounterVec
+ requestDuration *prometheus.HistogramVec
+ requestSize *prometheus.HistogramVec
+ responseSize *prometheus.HistogramVec
+ responseDuration *prometheus.HistogramVec
+}{
+ init: sync.Once{},
+}
+
+func initHTTPMetrics() {
+ const ns, sub = "caddy", "http"
+
+ basicLabels := []string{"server", "handler"}
+ httpMetrics.requestInFlight = promauto.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "requests_in_flight",
+ Help: "Number of requests currently handled by this server.",
+ }, basicLabels)
+ httpMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "request_errors_total",
+ Help: "Number of requests resulting in middleware errors.",
+ }, basicLabels)
+ httpMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "requests_total",
+ Help: "Counter of HTTP(S) requests made.",
+ }, basicLabels)
+
+ // TODO: allow these to be customized in the config
+ durationBuckets := prometheus.DefBuckets
+ sizeBuckets := prometheus.ExponentialBuckets(256, 4, 8)
+
+ httpLabels := []string{"server", "handler", "code", "method"}
+ httpMetrics.requestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "request_duration_seconds",
+ Help: "Histogram of round-trip request durations.",
+ Buckets: durationBuckets,
+ }, httpLabels)
+ httpMetrics.requestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "request_size_bytes",
+ Help: "Total size of the request. Includes body",
+ Buckets: sizeBuckets,
+ }, httpLabels)
+ httpMetrics.responseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "response_size_bytes",
+ Help: "Size of the returned response.",
+ Buckets: sizeBuckets,
+ }, httpLabels)
+ httpMetrics.responseDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: ns,
+ Subsystem: sub,
+ Name: "response_duration_seconds",
+ Help: "Histogram of times to first byte in response bodies.",
+ Buckets: durationBuckets,
+ }, httpLabels)
+}
+
+type ctxKeyServerName struct{}
+
+// serverNameFromContext extracts the current server name from the context.
+// Returns "UNKNOWN" if none is available (should probably never happen?)
+func serverNameFromContext(ctx context.Context) string {
+ srvName, ok := ctx.Value(ctxKeyServerName{}).(string)
+ if !ok {
+ return "UNKNOWN"
+ }
+ return srvName
+}
+
+func contextWithServerName(ctx context.Context, serverName string) context.Context {
+ return context.WithValue(ctx, ctxKeyServerName{}, serverName)
+}
+
+type metricsInstrumentedHandler struct {
+ labels prometheus.Labels
+ statusLabels prometheus.Labels
+ mh MiddlewareHandler
+}
+
+func newMetricsInstrumentedHandler(server, handler string, mh MiddlewareHandler) *metricsInstrumentedHandler {
+ httpMetrics.init.Do(func() {
+ initHTTPMetrics()
+ })
+
+ labels := prometheus.Labels{"server": server, "handler": handler}
+ statusLabels := prometheus.Labels{"server": server, "handler": handler, "code": "", "method": ""}
+ return &metricsInstrumentedHandler{labels, statusLabels, mh}
+}
+
+func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
+ inFlight := httpMetrics.requestInFlight.With(h.labels)
+ inFlight.Inc()
+ defer inFlight.Dec()
+
+ statusLabels := prometheus.Labels{"method": r.Method}
+ for k, v := range h.labels {
+ statusLabels[k] = v
+ }
+
+ start := time.Now()
+
+ // This is a _bit_ of a hack - it depends on the ShouldBufferFunc always
+ // being called when the headers are written.
+ // Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader.
+ writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
+ statusLabels["code"] = sanitizeCode(status)
+ ttfb := time.Since(start).Seconds()
+ observeWithExemplar(statusLabels, httpMetrics.responseDuration, ttfb)
+ return false
+ })
+ wrec := NewResponseRecorder(w, nil, writeHeaderRecorder)
+ err := h.mh.ServeHTTP(wrec, r, next)
+ dur := time.Since(start).Seconds()
+ httpMetrics.requestCount.With(h.labels).Inc()
+ if err != nil {
+ httpMetrics.requestErrors.With(h.labels).Inc()
+ return err
+ }
+
+ observeWithExemplar(statusLabels, httpMetrics.requestDuration, dur)
+ observeWithExemplar(statusLabels, httpMetrics.requestSize, float64(computeApproximateRequestSize(r)))
+ httpMetrics.responseSize.With(statusLabels).Observe(float64(wrec.Size()))
+
+ return nil
+}
+
+func observeWithExemplar(l prometheus.Labels, o *prometheus.HistogramVec, value float64) {
+ obs := o.With(l)
+ if oe, ok := obs.(prometheus.ExemplarObserver); ok {
+ oe.ObserveWithExemplar(value, l)
+ return
+ }
+ // _should_ be a noop, but here just in case...
+ obs.Observe(value)
+}
+
+func sanitizeCode(code int) string {
+ if code == 0 {
+ return "200"
+ }
+ return strconv.Itoa(code)
+
+}
+
+// taken from https://github.com/prometheus/client_golang/blob/6007b2b5cae01203111de55f753e76d8dac1f529/prometheus/promhttp/instrument_server.go#L298
+func computeApproximateRequestSize(r *http.Request) int {
+ s := 0
+ if r.URL != nil {
+ s += len(r.URL.String())
+ }
+
+ s += len(r.Method)
+ s += len(r.Proto)
+ for name, values := range r.Header {
+ s += len(name)
+ for _, value := range values {
+ s += len(value)
+ }
+ }
+ s += len(r.Host)
+
+ // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
+
+ if r.ContentLength != -1 {
+ s += int(r.ContentLength)
+ }
+ return s
+}
diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go
new file mode 100644
index 0000000..5c3bc1d
--- /dev/null
+++ b/modules/caddyhttp/metrics_test.go
@@ -0,0 +1,66 @@
+package caddyhttp
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/prometheus/client_golang/prometheus/testutil"
+)
+
+func TestServerNameFromContext(t *testing.T) {
+ ctx := context.Background()
+ expected := "UNKNOWN"
+ if actual := serverNameFromContext(ctx); actual != expected {
+ t.Errorf("Not equal: expected %q, but got %q", expected, actual)
+ }
+
+ in := "foo"
+ ctx = contextWithServerName(ctx, in)
+ if actual := serverNameFromContext(ctx); actual != in {
+ t.Errorf("Not equal: expected %q, but got %q", in, actual)
+ }
+}
+
+func TestMetricsInstrumentedHandler(t *testing.T) {
+ handlerErr := errors.New("oh noes")
+ response := []byte("hello world!")
+ h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+ if actual := testutil.ToFloat64(httpMetrics.requestInFlight); actual != 1.0 {
+ t.Errorf("Not same: expected %#v, but got %#v", 1.0, actual)
+ }
+ if handlerErr == nil {
+ w.Write(response)
+ }
+ return handlerErr
+ })
+
+ mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
+ return h.ServeHTTP(w, r)
+ })
+
+ ih := newMetricsInstrumentedHandler("foo", "bar", mh)
+
+ r := httptest.NewRequest("GET", "/", nil)
+ w := httptest.NewRecorder()
+
+ if actual := ih.ServeHTTP(w, r, h); actual != handlerErr {
+ t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
+ }
+ if actual := testutil.ToFloat64(httpMetrics.requestInFlight); actual != 0.0 {
+ t.Errorf("Not same: expected %#v, but got %#v", 0.0, actual)
+ }
+
+ handlerErr = nil
+ if err := ih.ServeHTTP(w, r, h); err != nil {
+ t.Errorf("Received unexpected error: %w", err)
+ }
+}
+
+type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
+
+func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {
+ return f(w, r, h)
+}
diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go
index 10e0c9d..be23d39 100644
--- a/modules/caddyhttp/routes.go
+++ b/modules/caddyhttp/routes.go
@@ -157,7 +157,7 @@ func (routes RouteList) ProvisionHandlers(ctx caddy.Context) error {
// pre-compile the middleware handler chain
for _, midhandler := range routes[i].Handlers {
- routes[i].middleware = append(routes[i].middleware, wrapMiddleware(midhandler))
+ routes[i].middleware = append(routes[i].middleware, wrapMiddleware(ctx, midhandler))
}
}
return nil
@@ -242,7 +242,13 @@ func wrapRoute(route Route) Middleware {
// we need to pull this particular MiddlewareHandler
// pointer into its own stack frame to preserve it so it
// won't be overwritten in future loop iterations.
-func wrapMiddleware(mh MiddlewareHandler) Middleware {
+func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler) Middleware {
+ // first, wrap the middleware with metrics instrumentation
+ metricsHandler := newMetricsInstrumentedHandler(
+ serverNameFromContext(ctx.Context),
+ caddy.GetModuleName(mh),
+ mh,
+ )
return func(next Handler) Handler {
// copy the next handler (it's an interface, so it's
// just a very lightweight copy of a pointer); this
@@ -253,7 +259,7 @@ func wrapMiddleware(mh MiddlewareHandler) Middleware {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
// TODO: This is where request tracing could be implemented
// TODO: see what the std lib gives us in terms of stack tracing too
- return mh.ServeHTTP(w, r, nextCopy)
+ return metricsHandler.ServeHTTP(w, r, nextCopy)
})
}
}
diff --git a/modules/metrics/adminmetrics.go b/modules/metrics/adminmetrics.go
new file mode 100644
index 0000000..f6a55e8
--- /dev/null
+++ b/modules/metrics/adminmetrics.go
@@ -0,0 +1,55 @@
+// Copyright 2020 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics
+
+import (
+ "net/http"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(AdminMetrics{})
+}
+
+// AdminMetrics is a module that serves a metrics endpoint so that any gathered
+// metrics can be exposed for scraping. This module is not configurable, and
+// is permanently mounted to the admin API endpoint at "/metrics".
+// See the Metrics module for a configurable endpoint that is usable if the
+// Admin API is disabled.
+type AdminMetrics struct{}
+
+// CaddyModule returns the Caddy module information.
+func (AdminMetrics) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "admin.api.metrics",
+ New: func() caddy.Module { return new(AdminMetrics) },
+ }
+}
+
+// Routes returns a route for the /metrics endpoint.
+func (m *AdminMetrics) Routes() []caddy.AdminRoute {
+ metricsHandler := createMetricsHandler(nil)
+ h := caddy.AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+ metricsHandler.ServeHTTP(w, r)
+ return nil
+ })
+ return []caddy.AdminRoute{{Pattern: "/metrics", Handler: h}}
+}
+
+// Interface guards
+var (
+ _ caddy.AdminRouter = (*AdminMetrics)(nil)
+)
diff --git a/modules/metrics/metrics.go b/modules/metrics/metrics.go
new file mode 100644
index 0000000..eab6342
--- /dev/null
+++ b/modules/metrics/metrics.go
@@ -0,0 +1,109 @@
+// Copyright 2020 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics
+
+import (
+ "net/http"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
+
+ "go.uber.org/zap"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+func init() {
+ caddy.RegisterModule(Metrics{})
+ httpcaddyfile.RegisterHandlerDirective("metrics", parseCaddyfile)
+}
+
+// Metrics is a module that serves a /metrics endpoint so that any gathered
+// metrics can be exposed for scraping. This module is configurable by end-users
+// unlike AdminMetrics.
+type Metrics struct {
+ metricsHandler http.Handler
+}
+
+// CaddyModule returns the Caddy module information.
+func (Metrics) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "http.handlers.metrics",
+ New: func() caddy.Module { return new(Metrics) },
+ }
+}
+
+type zapLogger struct {
+ zl *zap.Logger
+}
+
+func (l *zapLogger) Println(v ...interface{}) {
+ l.zl.Sugar().Error(v...)
+}
+
+// Provision sets up m.
+func (m *Metrics) Provision(ctx caddy.Context) error {
+ log := ctx.Logger(m)
+ m.metricsHandler = createMetricsHandler(&zapLogger{log})
+ return nil
+}
+
+func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
+ var m Metrics
+ err := m.UnmarshalCaddyfile(h.Dispenser)
+ return m, err
+}
+
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
+//
+// metrics <matcher>
+//
+func (m *Metrics) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.Next() {
+ args := d.RemainingArgs()
+ if len(args) > 0 {
+ return d.ArgErr()
+ }
+ }
+ return nil
+}
+
+func (m Metrics) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
+ m.metricsHandler.ServeHTTP(w, r)
+ return next.ServeHTTP(w, r)
+}
+
+// Interface guards
+var (
+ _ caddy.Provisioner = (*Metrics)(nil)
+ _ caddyhttp.MiddlewareHandler = (*Metrics)(nil)
+ _ caddyfile.Unmarshaler = (*Metrics)(nil)
+)
+
+func createMetricsHandler(logger promhttp.Logger) http.Handler {
+ return promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer,
+ promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
+ // will only log errors if logger is non-nil
+ ErrorLog: logger,
+
+ // Allow OpenMetrics format to be negotiated - largely compatible,
+ // except quantile/le label values always have a decimal.
+ EnableOpenMetrics: true,
+ }),
+ )
+}
diff --git a/modules/standard/imports.go b/modules/standard/imports.go
index b8ea7a8..bc2d955 100644
--- a/modules/standard/imports.go
+++ b/modules/standard/imports.go
@@ -11,4 +11,5 @@ import (
_ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek"
_ "github.com/caddyserver/caddy/v2/modules/filestorage"
_ "github.com/caddyserver/caddy/v2/modules/logging"
+ _ "github.com/caddyserver/caddy/v2/modules/metrics"
)