jnfilter @ bacd46b83243654cdfad83c49decd1554612a535

diff --git a/Makefile b/Makefile
index 69b82d9ae26675346d7810a090d1e10a0be7e7b3..874f6d3768512bba18e5c4a56548a79b7d83f2e5 100644
--- a/Makefile
+++ b/Makefile
@@ -21,6 +21,5 @@
 compress_into_oblivion: build
 	upx --best --ultra-brute $(OUT)
 
-run: sass tmpl
-	$(GO_RUN) $(SERVER)
-
+run:
+	$(GO_RUN) .
diff --git a/ext.go b/ext.go
new file mode 100644
index 0000000000000000000000000000000000000000..f6a0af801b29f29d970fd0f51ec9ee9302c4d12d
--- /dev/null
+++ b/ext.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+	"io"
+	"net/http"
+)
+
+type ResponseWriter interface {
+	http.ResponseWriter
+	Status() int
+}
+
+type responseWriter struct {
+	http.ResponseWriter
+	pendingStatus int
+	status        int
+	size          int
+}
+
+func NewResponseWriter(rw http.ResponseWriter) ResponseWriter {
+	return &responseWriter{
+		ResponseWriter: rw,
+	}
+}
+
+func (rw *responseWriter) WriteHeader(s int) {
+	if rw.Written() {
+		return
+	}
+
+	rw.pendingStatus = s
+
+	if rw.Written() {
+		return
+	}
+
+	rw.status = s
+	rw.ResponseWriter.WriteHeader(s)
+}
+
+func (rw *responseWriter) Write(b []byte) (int, error) {
+	if !rw.Written() {
+		// The status will be StatusOK if WriteHeader has not been called yet
+		rw.WriteHeader(http.StatusOK)
+	}
+	size, err := rw.ResponseWriter.Write(b)
+	rw.size += size
+	return size, err
+}
+
+func (rw *responseWriter) ReadFrom(r io.Reader) (n int64, err error) {
+	if !rw.Written() {
+		// The status will be StatusOK if WriteHeader has not been called yet
+		rw.WriteHeader(http.StatusOK)
+	}
+	n, err = io.Copy(rw.ResponseWriter, r)
+	rw.size += int(n)
+	return
+}
+
+func (rw *responseWriter) Unwrap() http.ResponseWriter {
+	return rw.ResponseWriter
+}
+
+func (rw *responseWriter) Status() int {
+	if rw.Written() {
+		return rw.status
+	}
+
+	return rw.pendingStatus
+}
+
+func (rw *responseWriter) Written() bool {
+	return rw.status != 0
+}
diff --git a/go.mod b/go.mod
index 298fa2b78ae0fa9364584e43746e23dd1ddd7f4a..b0cf6819a2871557cb4afd2ad2ebf40cd82a5638 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,17 @@ module git.sr.ht/~gabrielgio/jnfilter
 
 go 1.21.7
 
-require github.com/beevik/etree v1.3.0
+require (
+	github.com/beevik/etree v1.3.0
+	github.com/prometheus/client_golang v1.19.0
+)
+
+require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/prometheus/client_model v0.5.0 // indirect
+	github.com/prometheus/common v0.48.0 // indirect
+	github.com/prometheus/procfs v0.12.0 // indirect
+	golang.org/x/sys v0.16.0 // indirect
+	google.golang.org/protobuf v1.32.0 // indirect
+)
diff --git a/go.sum b/go.sum
index 999dbadd7a37dec8bf5043a8c3502e20d7d92194..925af14a2643f38d754d4e2fa4be5f292de734c4 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,20 @@
 github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU=
 github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
+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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
+github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
diff --git a/main.go b/main.go
index 945b30bb390b009e354cc89194b378ece5dd2be5..b9bed96fefc2d2eb2286a5c0c529520ede26575b 100644
--- a/main.go
+++ b/main.go
@@ -8,28 +8,52 @@ 	"fmt"
 	"io"
 	"net/http"
 	"regexp"
+	"strconv"
 	"strings"
+	"time"
 
 	"github.com/beevik/etree"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
 )
 
 type ErrorRequestHandler func(w http.ResponseWriter, r *http.Request) error
 
-var RegexCollection = map[string]string{
-	"nerdcast":     "NerdCast [0-9]+[a-c]* -",
-	"empreendedor": "Empreendedor [0-9]+ -",
-	"mamicas":      "Caneca de Mamicas [0-9]+ -",
-	"english":      "Speak English [0-9]+ -",
-	"nerdcash":     "NerdCash [0-9]+ -",
-	"bunker":       "Lá do Bunker [0-9]+ -",
-	"tech":         "NerdTech [0-9]+ -",
-	"genera":       "Generacast [0-9]+ -",
-}
-
 const (
 	FeedUrl = "https://api.jovemnerd.com.br/feed-nerdcast/"
 )
 
+var (
+	RegexCollection = map[string]string{
+		"nerdcast":     "NerdCast [0-9]+[a-c]* -",
+		"empreendedor": "Empreendedor [0-9]+ -",
+		"mamicas":      "Caneca de Mamicas [0-9]+ -",
+		"english":      "Speak English [0-9]+ -",
+		"nerdcash":     "NerdCash [0-9]+ -",
+		"bunker":       "Lá do Bunker [0-9]+ -",
+		"tech":         "NerdTech [0-9]+ -",
+		"genera":       "Generacast [0-9]+ -",
+	}
+
+	feedRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{
+		Name:    "feed_request",
+		Help:    "How long jovemnerd takes to answer",
+		Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
+	}, []string{"status_code"})
+
+	httpRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{
+		Name:    "http_request",
+		Help:    "How long the application takes to complete the request",
+		Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
+	}, []string{"status_code", "user_agent"})
+
+	seriesCount = promauto.NewCounterVec(prometheus.CounterOpts{
+		Name: "serie_count",
+		Help: "How often a serie is called",
+	}, []string{"serie"})
+)
+
 func getSeries(r *http.Request) []string {
 	query := r.URL.Query().Get("q")
 
@@ -59,13 +83,24 @@ 	return false
 }
 
 func fetchXML(_ context.Context) ([]byte, error) {
+	t := time.Now()
+	c := http.StatusInternalServerError
+
+	defer func() {
+		since := time.Since(t).Seconds()
+		code := strconv.Itoa(c)
+		feedRequest.WithLabelValues(code).Observe(since)
+	}()
+
 	res, err := http.Get(FeedUrl)
 	if err != nil {
 		return nil, err
 	}
 	defer res.Body.Close()
 
-	if res.StatusCode == http.StatusOK {
+	c = res.StatusCode
+
+	if c == http.StatusOK {
 		return io.ReadAll(res.Body)
 	}
 
@@ -109,7 +144,7 @@
 	return doc.WriteToBytes()
 }
 
-func wrap(next ErrorRequestHandler) http.HandlerFunc {
+func handleError(next ErrorRequestHandler) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		if err := next(w, r); err != nil {
 			w.WriteHeader(http.StatusInternalServerError)
@@ -117,6 +152,30 @@ 		}
 	}
 }
 
+func observe(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		t := time.Now()
+
+		next(w, r)
+
+		rw := w.(*responseWriter)
+		since := time.Since(t).Seconds()
+		code := strconv.Itoa(rw.Status())
+		userAgent := r.Header.Get("user-agent")
+		httpRequest.WithLabelValues(code, userAgent).Observe(float64(since))
+
+		for _, s := range getSeries(r) {
+			seriesCount.WithLabelValues(s).Inc()
+		}
+	}
+}
+
+func wrap(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		next(NewResponseWriter(w), r)
+	}
+}
+
 func titles(w http.ResponseWriter, r *http.Request) error {
 	xml, err := fetchXML(r.Context())
 	if err != nil {
@@ -173,8 +232,9 @@
 	flag.Parse()
 
 	mux := http.NewServeMux()
-	mux.HandleFunc("/titles", wrap(titles))
-	mux.HandleFunc("/", wrap(podcast))
+	mux.Handle("/metrics", promhttp.Handler())
+	mux.HandleFunc("/titles", wrap(handleError(titles)))
+	mux.HandleFunc("/", wrap(observe(handleError(podcast))))
 
 	server := http.Server{
 		Handler: mux,