gunwokod
commit
8502019852
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue