From b29ad0afa15f8f34f9825cadc31ee97559ebcfd7 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Giovanini" Date: Fri, 22 Mar 2024 18:28:31 +0100 Subject: feat: Add metrics --- Makefile | 5 ++-- ext.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 15 ++++++++++- go.sum | 18 +++++++++++++ main.go | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------- 5 files changed, 184 insertions(+), 19 deletions(-) create mode 100644 ext.go diff --git a/Makefile b/Makefile index 69b82d9..874f6d3 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,5 @@ compress: build 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 0000000..f6a0af8 --- /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 298fa2b..b0cf681 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 999dbad..925af14 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 945b30b..b9bed96 100644 --- a/main.go +++ b/main.go @@ -8,28 +8,52 @@ import ( "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 @@ func match(title string, series []string) bool { } 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 @@ func filterBySeries(series []string, xml []byte, temper bool) ([]byte, error) { 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 wrap(next ErrorRequestHandler) http.HandlerFunc { } } +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 @@ func main() { 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, -- cgit v1.2.3