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