jnfilter @ bacd46b83243654cdfad83c49decd1554612a535

  1diff --git a/Makefile b/Makefile
  2index 69b82d9ae26675346d7810a090d1e10a0be7e7b3..874f6d3768512bba18e5c4a56548a79b7d83f2e5 100644
  3--- a/Makefile
  4+++ b/Makefile
  5@@ -21,6 +21,5 @@
  6 compress_into_oblivion: build
  7 	upx --best --ultra-brute $(OUT)
  8 
  9-run: sass tmpl
 10-	$(GO_RUN) $(SERVER)
 11-
 12+run:
 13+	$(GO_RUN) .
 14diff --git a/ext.go b/ext.go
 15new file mode 100644
 16index 0000000000000000000000000000000000000000..f6a0af801b29f29d970fd0f51ec9ee9302c4d12d
 17--- /dev/null
 18+++ b/ext.go
 19@@ -0,0 +1,75 @@
 20+package main
 21+
 22+import (
 23+	"io"
 24+	"net/http"
 25+)
 26+
 27+type ResponseWriter interface {
 28+	http.ResponseWriter
 29+	Status() int
 30+}
 31+
 32+type responseWriter struct {
 33+	http.ResponseWriter
 34+	pendingStatus int
 35+	status        int
 36+	size          int
 37+}
 38+
 39+func NewResponseWriter(rw http.ResponseWriter) ResponseWriter {
 40+	return &responseWriter{
 41+		ResponseWriter: rw,
 42+	}
 43+}
 44+
 45+func (rw *responseWriter) WriteHeader(s int) {
 46+	if rw.Written() {
 47+		return
 48+	}
 49+
 50+	rw.pendingStatus = s
 51+
 52+	if rw.Written() {
 53+		return
 54+	}
 55+
 56+	rw.status = s
 57+	rw.ResponseWriter.WriteHeader(s)
 58+}
 59+
 60+func (rw *responseWriter) Write(b []byte) (int, error) {
 61+	if !rw.Written() {
 62+		// The status will be StatusOK if WriteHeader has not been called yet
 63+		rw.WriteHeader(http.StatusOK)
 64+	}
 65+	size, err := rw.ResponseWriter.Write(b)
 66+	rw.size += size
 67+	return size, err
 68+}
 69+
 70+func (rw *responseWriter) ReadFrom(r io.Reader) (n int64, err error) {
 71+	if !rw.Written() {
 72+		// The status will be StatusOK if WriteHeader has not been called yet
 73+		rw.WriteHeader(http.StatusOK)
 74+	}
 75+	n, err = io.Copy(rw.ResponseWriter, r)
 76+	rw.size += int(n)
 77+	return
 78+}
 79+
 80+func (rw *responseWriter) Unwrap() http.ResponseWriter {
 81+	return rw.ResponseWriter
 82+}
 83+
 84+func (rw *responseWriter) Status() int {
 85+	if rw.Written() {
 86+		return rw.status
 87+	}
 88+
 89+	return rw.pendingStatus
 90+}
 91+
 92+func (rw *responseWriter) Written() bool {
 93+	return rw.status != 0
 94+}
 95diff --git a/go.mod b/go.mod
 96index 298fa2b78ae0fa9364584e43746e23dd1ddd7f4a..b0cf6819a2871557cb4afd2ad2ebf40cd82a5638 100644
 97--- a/go.mod
 98+++ b/go.mod
 99@@ -2,4 +2,17 @@ module git.sr.ht/~gabrielgio/jnfilter
100 
101 go 1.21.7
102 
103-require github.com/beevik/etree v1.3.0
104+require (
105+	github.com/beevik/etree v1.3.0
106+	github.com/prometheus/client_golang v1.19.0
107+)
108+
109+require (
110+	github.com/beorn7/perks v1.0.1 // indirect
111+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
112+	github.com/prometheus/client_model v0.5.0 // indirect
113+	github.com/prometheus/common v0.48.0 // indirect
114+	github.com/prometheus/procfs v0.12.0 // indirect
115+	golang.org/x/sys v0.16.0 // indirect
116+	google.golang.org/protobuf v1.32.0 // indirect
117+)
118diff --git a/go.sum b/go.sum
119index 999dbadd7a37dec8bf5043a8c3502e20d7d92194..925af14a2643f38d754d4e2fa4be5f292de734c4 100644
120--- a/go.sum
121+++ b/go.sum
122@@ -1,2 +1,20 @@
123 github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU=
124 github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
125+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
126+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
127+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
128+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
129+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
130+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
131+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
132+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
133+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
134+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
135+github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
136+github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
137+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
138+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
139+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
140+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
141+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
142+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
143diff --git a/main.go b/main.go
144index 945b30bb390b009e354cc89194b378ece5dd2be5..b9bed96fefc2d2eb2286a5c0c529520ede26575b 100644
145--- a/main.go
146+++ b/main.go
147@@ -8,28 +8,52 @@ 	"fmt"
148 	"io"
149 	"net/http"
150 	"regexp"
151+	"strconv"
152 	"strings"
153+	"time"
154 
155 	"github.com/beevik/etree"
156+	"github.com/prometheus/client_golang/prometheus"
157+	"github.com/prometheus/client_golang/prometheus/promauto"
158+	"github.com/prometheus/client_golang/prometheus/promhttp"
159 )
160 
161 type ErrorRequestHandler func(w http.ResponseWriter, r *http.Request) error
162 
163-var RegexCollection = map[string]string{
164-	"nerdcast":     "NerdCast [0-9]+[a-c]* -",
165-	"empreendedor": "Empreendedor [0-9]+ -",
166-	"mamicas":      "Caneca de Mamicas [0-9]+ -",
167-	"english":      "Speak English [0-9]+ -",
168-	"nerdcash":     "NerdCash [0-9]+ -",
169-	"bunker":       "Lá do Bunker [0-9]+ -",
170-	"tech":         "NerdTech [0-9]+ -",
171-	"genera":       "Generacast [0-9]+ -",
172-}
173-
174 const (
175 	FeedUrl = "https://api.jovemnerd.com.br/feed-nerdcast/"
176 )
177 
178+var (
179+	RegexCollection = map[string]string{
180+		"nerdcast":     "NerdCast [0-9]+[a-c]* -",
181+		"empreendedor": "Empreendedor [0-9]+ -",
182+		"mamicas":      "Caneca de Mamicas [0-9]+ -",
183+		"english":      "Speak English [0-9]+ -",
184+		"nerdcash":     "NerdCash [0-9]+ -",
185+		"bunker":       "Lá do Bunker [0-9]+ -",
186+		"tech":         "NerdTech [0-9]+ -",
187+		"genera":       "Generacast [0-9]+ -",
188+	}
189+
190+	feedRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{
191+		Name:    "feed_request",
192+		Help:    "How long jovemnerd takes to answer",
193+		Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
194+	}, []string{"status_code"})
195+
196+	httpRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{
197+		Name:    "http_request",
198+		Help:    "How long the application takes to complete the request",
199+		Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
200+	}, []string{"status_code", "user_agent"})
201+
202+	seriesCount = promauto.NewCounterVec(prometheus.CounterOpts{
203+		Name: "serie_count",
204+		Help: "How often a serie is called",
205+	}, []string{"serie"})
206+)
207+
208 func getSeries(r *http.Request) []string {
209 	query := r.URL.Query().Get("q")
210 
211@@ -59,13 +83,24 @@ 	return false
212 }
213 
214 func fetchXML(_ context.Context) ([]byte, error) {
215+	t := time.Now()
216+	c := http.StatusInternalServerError
217+
218+	defer func() {
219+		since := time.Since(t).Seconds()
220+		code := strconv.Itoa(c)
221+		feedRequest.WithLabelValues(code).Observe(since)
222+	}()
223+
224 	res, err := http.Get(FeedUrl)
225 	if err != nil {
226 		return nil, err
227 	}
228 	defer res.Body.Close()
229 
230-	if res.StatusCode == http.StatusOK {
231+	c = res.StatusCode
232+
233+	if c == http.StatusOK {
234 		return io.ReadAll(res.Body)
235 	}
236 
237@@ -109,7 +144,7 @@
238 	return doc.WriteToBytes()
239 }
240 
241-func wrap(next ErrorRequestHandler) http.HandlerFunc {
242+func handleError(next ErrorRequestHandler) http.HandlerFunc {
243 	return func(w http.ResponseWriter, r *http.Request) {
244 		if err := next(w, r); err != nil {
245 			w.WriteHeader(http.StatusInternalServerError)
246@@ -117,6 +152,30 @@ 		}
247 	}
248 }
249 
250+func observe(next http.HandlerFunc) http.HandlerFunc {
251+	return func(w http.ResponseWriter, r *http.Request) {
252+		t := time.Now()
253+
254+		next(w, r)
255+
256+		rw := w.(*responseWriter)
257+		since := time.Since(t).Seconds()
258+		code := strconv.Itoa(rw.Status())
259+		userAgent := r.Header.Get("user-agent")
260+		httpRequest.WithLabelValues(code, userAgent).Observe(float64(since))
261+
262+		for _, s := range getSeries(r) {
263+			seriesCount.WithLabelValues(s).Inc()
264+		}
265+	}
266+}
267+
268+func wrap(next http.HandlerFunc) http.HandlerFunc {
269+	return func(w http.ResponseWriter, r *http.Request) {
270+		next(NewResponseWriter(w), r)
271+	}
272+}
273+
274 func titles(w http.ResponseWriter, r *http.Request) error {
275 	xml, err := fetchXML(r.Context())
276 	if err != nil {
277@@ -173,8 +232,9 @@
278 	flag.Parse()
279 
280 	mux := http.NewServeMux()
281-	mux.HandleFunc("/titles", wrap(titles))
282-	mux.HandleFunc("/", wrap(podcast))
283+	mux.Handle("/metrics", promhttp.Handler())
284+	mux.HandleFunc("/titles", wrap(handleError(titles)))
285+	mux.HandleFunc("/", wrap(observe(handleError(podcast))))
286 
287 	server := http.Server{
288 		Handler: mux,