@n1try I’ve made very basic progress but I actually stripped out some features
I’m not sure where to take this to next (my Golang skills ain’t great)
This works but is not 100% representative of the available features in v1
# handler.go
package prometheus
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
"os"
"strconv"
"time"
)
func init() {
caddy.RegisterModule(Metrics{})
httpcaddyfile.RegisterHandlerDirective("prometheus", parseCaddyfile)
}
func (Metrics) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.prometheus",
New: func() caddy.Module { return NewMetrics() },
}
}
func (m *Metrics) Provision(ctx caddy.Context) error {
m.handler = promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
ErrorHandling: promhttp.HTTPErrorOnError,
ErrorLog: log.New(os.Stderr, "", log.LstdFlags),
})
once.Do(func() {
m.start()
})
return nil
}
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (m Metrics) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
requestURI := r.RequestURI
statusStr := "0"
var statusInt int
start := time.Now()
rw := caddyhttp.NewResponseRecorder(w, nil, func(status int, header http.Header) bool {
statusInt = status
return false
})
err := next.ServeHTTP(rw, r)
if err == nil {
rw.WriteResponse()
statusStr = strconv.Itoa(rw.Status())
} else {
if handlerErr, ok := err.(caddyhttp.HandlerError); ok {
statusInt = handlerErr.StatusCode
}
statusStr = strconv.Itoa(statusInt)
}
d := time.Now().Sub(start)
requestCount.WithLabelValues(append([]string{requestURI, r.Method, statusStr})...).Inc()
responseLatency.WithLabelValues(append([]string{requestURI, r.Method, statusStr})...).Observe(d.Seconds())
requestDuration.WithLabelValues(append([]string{requestURI, r.Method, statusStr})...).Observe(time.Since(start).Seconds())
responseSize.WithLabelValues(append([]string{requestURI, r.Method, statusStr})...).Observe(float64(rw.Size()))
responseStatus.WithLabelValues(append([]string{requestURI, r.Method, statusStr})...).Inc()
return err
}
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var (
metrics *Metrics
err error
)
if metrics != nil {
return nil, h.Dispenser.Err("prometheus: can only have one metrics module per server")
}
metrics = NewMetrics()
return metrics, err
}
// Interface guards
var (
_ caddy.Provisioner = (*Metrics)(nil)
_ caddyhttp.MiddlewareHandler = (*Metrics)(nil)
)
# metrics.go
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
)
const namespace = "caddy"
var (
requestCount *prometheus.CounterVec
requestDuration *prometheus.HistogramVec
responseSize *prometheus.HistogramVec
responseStatus *prometheus.CounterVec
responseLatency *prometheus.HistogramVec
)
func (m Metrics) define(subsystem string) {
if subsystem == "" {
subsystem = "http"
}
if m.latencyBuckets == nil {
m.latencyBuckets = append(prometheus.DefBuckets, 15, 20, 30, 60, 120, 180, 240, 480, 960)
}
if m.sizeBuckets == nil {
m.sizeBuckets = []float64{0, 500, 1000, 2000, 3000, 4000, 5000, 10000, 20000, 30000, 50000, 1e5, 5e5, 1e6, 2e6, 3e6, 4e6, 5e6, 10e6}
}
requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "request_count_total",
Help: "Counter of HTTP(S) requests made.",
}, append([]string{"handler", "method", "status_code"}))
requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "request_duration_seconds",
Help: "Histogram of the time (in seconds) each request took.",
Buckets: m.latencyBuckets,
}, append([]string{"handler", "method", "status_code"}))
responseSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "response_size_bytes",
Help: "Size of the returns response in bytes.",
Buckets: m.sizeBuckets,
}, append([]string{"handler", "method", "status_code"}))
responseStatus = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "response_status_count_total",
Help: "Counter of response status codes.",
}, append([]string{"handler", "method", "status_code"}))
responseLatency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "response_latency_seconds",
Help: "Histogram of the time (in seconds) until the first write for each request.",
Buckets: m.latencyBuckets,
}, append([]string{"handler", "method", "status_code"}))
}
# setup.go
package prometheus
import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/prometheus/client_golang/prometheus"
"log"
"net/http"
"sync"
)
const (
defaultPath = "/metrics"
defaultAddr = "localhost:9180"
)
var once sync.Once
// Metrics holds the prometheus configuration.
type Metrics struct {
next caddyhttp.Handler
addr string // where to we listen
useCaddyAddr bool
hostname string
path string
latencyBuckets []float64
sizeBuckets []float64
// subsystem?
once sync.Once
handler http.Handler
}
func NewMetrics() *Metrics {
return &Metrics{
path: defaultPath,
addr: defaultAddr,
}
}
func (m *Metrics) start() error {
m.once.Do(func() {
m.define("")
prometheus.MustRegister(requestCount)
prometheus.MustRegister(requestDuration)
prometheus.MustRegister(responseLatency)
prometheus.MustRegister(responseSize)
prometheus.MustRegister(responseStatus)
if !m.useCaddyAddr {
http.Handle(m.path, m.handler)
go func() {
err := http.ListenAndServe(m.addr, nil)
if err != nil {
log.Printf("[ERROR] Starting handler: %v", err)
}
}()
}
})
return nil
}