commit 8502019852557047d7ffcc5d8b52424c9693876c Author: Sergiusz Bazanski Date: Thu Aug 30 13:51:08 2018 +0100 gunwokod diff --git a/main.go b/main.go new file mode 100644 index 0000000..62f0904 --- /dev/null +++ b/main.go @@ -0,0 +1,226 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/golang/glog" + "golang.org/x/net/html" +) + +var ( + flagLuftdaten string + flagScrapeInterval int + flagListen string +) + +func init() { + flag.Set("logtostderr", "true") +} + +type sensorData struct { + // Whether the data is fresh. + stale bool + // PM2.5 in ug/m3 + pm25 float64 + // PM10 in ug/m3 + pm10 float64 + // Temperature in degrees centigrade + temperature float64 + // Humidity in percent + humidity float64 +} + +func getSensorData() (sensorData, error) { + data := sensorData{} + + resp, err := http.Get(flagLuftdaten) + if err != nil { + return data, fmt.Errorf("when GETing data: %v", err) + } + + root, err := html.Parse(resp.Body) + if err != nil { + return data, fmt.Errorf("when parsing HTML: %v", err) + } + + // Find html. + htmlN := root.FirstChild + if htmlN == nil || htmlN.Type != html.ElementNode || htmlN.Data != "html" { + return data, fmt.Errorf("unexpected root node") + } + // Find body. + bodyN := htmlN.FirstChild.NextSibling + if bodyN == nil || bodyN.Type != html.ElementNode || bodyN.Data != "body" { + return data, fmt.Errorf("unexpected html[1] node") + } + // Find div class=content + contentN := bodyN.FirstChild.NextSibling + if contentN == nil || contentN.Type != html.ElementNode || contentN.Data != "div" || contentN.Attr[0].Key != "class" || contentN.Attr[0].Val != "content" { + return data, fmt.Errorf("unexpected body[1] node") + } + // Find table. + var tableN *html.Node = nil + for c := contentN.FirstChild; c != nil; c = c.NextSibling { + if c.Type == html.ElementNode && c.Data == "table" { + tableN = c + break + } + } + if tableN == nil { + return data, fmt.Errorf("could not find table") + } + // Find tbody. + tbodyN := tableN.FirstChild + if tbodyN == nil || tbodyN.Type != html.ElementNode || tbodyN.Data != "tbody" { + return data, fmt.Errorf("unexpected table[0] node") + } + + got := make(map[string]bool) + // Iterate throught rows. + for row := tbodyN.FirstChild; row != nil; row = row.NextSibling { + td1 := row.FirstChild + if td1 == nil { + continue + } + td2 := td1.NextSibling + if td2 == nil { + continue + } + td3 := td2.NextSibling + if td3 == nil { + continue + } + if td1.Data != "td" || td2.Data != "td" || td3.Data != "td" { + continue + } + + if td2.FirstChild.Data == "PM2.5" { + // This is not an ASCII space. + parts := strings.Split(td3.FirstChild.Data, " ") + if len(parts) != 2 { + continue + } + pm25, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + continue + } + data.pm25 = pm25 + got["PM2.5"] = true + } + if td2.FirstChild.Data == "PM10" { + // This is not an ASCII space. + parts := strings.Split(td3.FirstChild.Data, " ") + if len(parts) != 2 { + continue + } + pm10, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + continue + } + data.pm10 = pm10 + got["PM10"] = true + } + if td2.FirstChild.Data == "Temperatur" { + // This is not an ASCII space. + parts := strings.Split(td3.FirstChild.Data, " ") + if len(parts) != 2 { + continue + } + temp, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + continue + } + data.temperature = temp + got["Temperature"] = true + } + if td2.FirstChild.Data == "rel. Luftfeuchte" { + // This is not an ASCII space. + parts := strings.Split(td3.FirstChild.Data, " ") + if len(parts) != 2 { + continue + } + hum, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + continue + } + data.humidity = hum + got["Humidity"] = true + } + } + + for _, want := range []string{"PM2.5", "PM10", "Temperature", "Humidity"} { + if !got[want] { + return data, fmt.Errorf("could not find %v in HTML", want) + } + } + return data, nil +} + +func scraper(req chan int, resp chan sensorData) error { + data := sensorData{ + stale: true, + } + + feed := func() { + newData, err := getSensorData() + if err != nil { + glog.Error("Could not get sensor data: %v", err) + data.stale = true + } else { + data = newData + glog.Errorf("Got sensor data: %f/%f/%f/%f", data.pm25, data.pm10, data.temperature, data.humidity) + } + } + + feed() + + ticker := time.NewTicker(time.Duration(flagScrapeInterval) * time.Second) + for { + select { + case <-req: + resp <- data + case <-ticker.C: + feed() + } + } +} + +func main() { + flag.StringVar(&flagLuftdaten, "luftdaten", "http://127.0.0.1:2140/values", "Luftdaten sensor endpoint") + flag.IntVar(&flagScrapeInterval, "scrape_interval", 60, "Scrape interval of Luftdaten, in seconds") + flag.StringVar(&flagListen, "listen", "0.0.0.0:2141", "Address to listen at for Prometheus requests") + flag.Parse() + + glog.Info("Starting scraper...") + req := make(chan int, 1) + res := make(chan sensorData, 1) + go scraper(req, res) + + http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { + req <- 0 + data := <-res + if data.stale { + fmt.Fprintf(w, "# data is stale :(\n") + return + } + + fmt.Fprintf(w, "# HELP pm25 The current PM2.5 concentration in micrograms per cubic meter\n") + fmt.Fprintf(w, "# TYPE pm25 gauge\n") + fmt.Fprintf(w, "pm25 %f\n", data.pm25) + fmt.Fprintf(w, "# HELP pm10 The current PM10 concentration in micrograms per cubic meter\n") + fmt.Fprintf(w, "# TYPE pm10 gauge\n") + fmt.Fprintf(w, "pm10 %f\n", data.pm10) + fmt.Fprintf(w, "# HELP temperature The current temperature in degrees centigrate\n") + fmt.Fprintf(w, "# TYPE temperature gauge\n") + fmt.Fprintf(w, "temperature %f\n", data.temperature) + fmt.Fprintf(w, "# HELP humidity The current relative humidity in percent\n") + fmt.Fprintf(w, "# TYPE humidity gauge\n") + fmt.Fprintf(w, "humidity %f\n", data.humidity) + }) + http.ListenAndServe(flagListen, nil) +}