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) }