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,