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,