package main import ( "context" "errors" "flag" "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 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") var series []string for _, q := range strings.Split(query, ",") { if _, ok := RegexCollection[q]; ok { series = append(series, q) } } if len(series) > 0 { return series } return []string{"nerdcast"} } func match(title string, series []string) bool { for _, s := range series { if ok, err := regexp.MatchString(RegexCollection[s], title); err == nil && ok { return true } } 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() c = res.StatusCode if c == http.StatusOK { return io.ReadAll(res.Body) } return nil, errors.New("Invalid http code") } func appendTag(tag *etree.Element, ap string) { text := tag.Text() tag.SetText(text + ap) } func filterBySeries(series []string, xml []byte, temper bool) ([]byte, error) { doc := etree.NewDocument() err := doc.ReadFromBytes(xml) if err != nil { return nil, err } channel := doc.FindElement("//channel") if temper { tmp := strings.ToUpper(strings.Join(series, ",")) tmp = fmt.Sprintf(" [%s]", tmp) appendTag(channel.FindElement("title"), tmp) appendTag(channel.FindElement("description"), tmp) appendTag(channel.FindElement("link"), "?"+tmp) appendTag(channel.FindElement("author[namespace-prefix()='itunes']"), tmp) appendTag(channel.FindElement("subtitle[namespace-prefix()='itunes']"), tmp) appendTag(channel.FindElement("summary[namespace-prefix()='itunes']"), tmp) appendTag(channel.FindElement("author[namespace-prefix()='googleplay']"), tmp) } for _, tag := range channel.FindElements("item") { title := tag.FindElement("title").Text() if !match(title, series) { channel.RemoveChild(tag) } } return doc.WriteToBytes() } 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) } } } 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 { return err } doc := etree.NewDocument() err = doc.ReadFromBytes(xml) if err != nil { return err } series := getSeries(r) els := doc.FindElements("//channel/item") for _, e := range els { txt := e.FindElement("title").Text() + "\n" if match(txt, series) { _, err = w.Write([]byte(txt)) if err != nil { return err } } } return nil } func podcast(w http.ResponseWriter, r *http.Request) error { xml, err := fetchXML(r.Context()) if err != nil { return err } series := getSeries(r) filterdXML, err := filterBySeries(series, xml, true) if err != nil { return err } _, err = w.Write(filterdXML) if err != nil { return err } return nil } func main() { var ( addr = flag.String("addr", ":8080", "Server address") ) flag.Parse() mux := http.NewServeMux() mux.Handle("/metrics", promhttp.Handler()) mux.HandleFunc("/titles", wrap(handleError(titles))) mux.HandleFunc("/", wrap(observe(handleError(podcast)))) server := http.Server{ Handler: mux, Addr: *addr, } err := server.ListenAndServe() if err != nil { fmt.Printf("Server error: %s", err.Error()) } }