put the scraping in own package

This commit is contained in:
Andre Kully 2021-12-23 16:36:04 +01:00
parent b0dd938b5d
commit e2639a5638
4 changed files with 247 additions and 162 deletions

View file

@ -22,6 +22,8 @@ linux: ## builds the linux version of the exporter
GOOS=linux GOARCH=amd64 go build $(GOFLAGS) -ldflags '$(LDFLAGS)' GOOS=linux GOARCH=amd64 go build $(GOFLAGS) -ldflags '$(LDFLAGS)'
mac: ## builds the macos version of the exporter mac: ## builds the macos version of the exporter
GOOS=darwin GOARCH=amd64 go build $(GOFLAGS) -ldflags '$(LDFLAGS)' GOOS=darwin GOARCH=amd64 go build $(GOFLAGS) -ldflags '$(LDFLAGS)'
mac-arm: ## builds the macos (m1) version of the exporter
GOOS=darwin GOARCH=arm64 go build $(GOFLAGS) -ldflags '$(LDFLAGS)'
arm64: arm64:
GOOS=linux GOARCH=arm64 go build $(GOFLAGS) -ldflags '$(LDFLAGS)' GOOS=linux GOARCH=arm64 go build $(GOFLAGS) -ldflags '$(LDFLAGS)'
arm: arm:

View file

@ -28,9 +28,31 @@ $ ./mystrom-exporter --help
``` ```
| Flag | Description | Default | | Flag | Description | Default |
| ---- | ----------- | ------- | | ---- | ----------- | ------- |
| switch.ip-address | IP address of the switch you try to monitor | `` |
| web.listen-address | Address to listen on | `:9452` | | web.listen-address | Address to listen on | `:9452` |
| web.metrics-path | Path under which to expose metrics | `/metrics` | | web.metrics-path | Path under which to expose exporters own metrics | `/metrics` |
| web.device-path | Path under which the metrics of the devices are fetched | `/device` |
## Prometheus configuration
A enhancement has been made to have only one exporter which can scrape multiple devices. This is configured in
Prometheus as follows assuming we have 4 mystrom devices and the exporter is running locally on the smae machine as
the Prometheus.
```yaml
- job_name: mystrom
scrape_interval: 30s
metrics_path: /device
honor_labels: true
static_configs:
- targets:
- '192.168.105.11'
- '192.168.105.12'
- '192.168.105.13'
- '192.168.105.14'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- target_label: __address__
replacement: 127.0.0.1:9452
```
## Supported architectures ## Supported architectures
Using the make file, you can easily build for the following architectures, those can also be considered the tested ones: Using the make file, you can easily build for the following architectures, those can also be considered the tested ones:
@ -40,6 +62,7 @@ Using the make file, you can easily build for the following architectures, those
| Linux | arm64 | | Linux | arm64 |
| Linux | arm | | Linux | arm |
| Mac | amd64 | | Mac | amd64 |
| Mac | arm64 |
Since go is cross compatible with windows, and mac arm as well, you should be able to build the binary for those as well, but they aren't tested. Since go is cross compatible with windows, and mac arm as well, you should be able to build the binary for those as well, but they aren't tested.
The docker image is only built & tested for amd64. The docker image is only built & tested for amd64.

217
main.go
View file

@ -1,18 +1,18 @@
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/log" "github.com/prometheus/common/log"
"mystrom-exporter/pkg/mystrom"
"mystrom-exporter/pkg/version" "mystrom-exporter/pkg/version"
) )
@ -29,151 +29,22 @@ var (
listenAddress = flag.String("web.listen-address", ":9452", listenAddress = flag.String("web.listen-address", ":9452",
"Address to listen on") "Address to listen on")
metricsPath = flag.String("web.metrics-path", "/metrics", metricsPath = flag.String("web.metrics-path", "/metrics",
"Path under which to expose metrics") "Path under which to expose exporters own metrics")
switchIP = flag.String("switch.ip-address", "", devicePath = flag.String("web.device-path", "/device",
"IP address of the switch you try to monitor") "Path under which the metrics of the devices are fetched")
showVersion = flag.Bool("version", false, showVersion = flag.Bool("version", false,
"Show version information.") "Show version information.")
up = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "up"),
"Was the last myStrom query successful.",
nil, nil,
) )
myStromPower = prometheus.NewDesc( var (
prometheus.BuildFQName(namespace, "", "report_power"), mystromDurationCounterVec *prometheus.CounterVec
"The current power consumed by devices attached to the switch", mystromRequestsCounterVec *prometheus.CounterVec
nil, nil,
) )
myStromRelay = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "report_relay"),
"The current state of the relay (wether or not the relay is currently turned on)",
nil, nil,
)
myStromTemperature = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "report_temperatur"),
"The currently measured temperature by the switch. (Might initially be wrong, but will automatically correct itself over the span of a few hours)",
nil, nil,
)
myStromWattPerSec = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "report_watt_per_sec"),
"The average of energy consumed per second from last call this request",
nil, nil,
)
)
type Exporter struct {
myStromSwitchIp string
}
func NewExporter(myStromSwitchIp string) *Exporter {
return &Exporter{
myStromSwitchIp: myStromSwitchIp,
}
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- up
ch <- myStromPower
ch <- myStromRelay
ch <- myStromTemperature
ch <- myStromWattPerSec
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
up, prometheus.GaugeValue, 1,
)
e.FetchSwitchMetrics(e.myStromSwitchIp, ch)
}
func (e *Exporter) FetchSwitchMetrics(switchIP string, ch chan<- prometheus.Metric) {
report, err := FetchReport(switchIP)
if err != nil {
log.Infof("Error occured, while fetching metrics: %s", err)
ch <- prometheus.MustNewConstMetric(
up, prometheus.GaugeValue, 0,
)
return
}
ch <- prometheus.MustNewConstMetric(
myStromPower, prometheus.GaugeValue, report.Power,
)
if report.Relay {
ch <- prometheus.MustNewConstMetric(
myStromRelay, prometheus.GaugeValue, 1,
)
} else {
ch <- prometheus.MustNewConstMetric(
myStromRelay, prometheus.GaugeValue, 0,
)
}
ch <- prometheus.MustNewConstMetric(
myStromWattPerSec, prometheus.GaugeValue, report.WattPerSec,
)
ch <- prometheus.MustNewConstMetric(
myStromTemperature, prometheus.GaugeValue, report.Temperature,
)
}
func FetchReport(switchIP string) (*switchReport, error) {
log.Infof("Trying to connect to switch at: %s", switchIP)
url := "http://" + switchIP + "/report"
switchClient := http.Client{
Timeout: time.Second * 5, // 3 second timeout, might need to be increased
}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "myStrom-exporter")
res, getErr := switchClient.Do(req)
if getErr != nil {
log.Infof("Error while trying to connect to switch: %s", getErr)
return nil, getErr
}
if res.Body != nil {
defer res.Body.Close()
}
body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
log.Infof("Error while reading body: %s", readErr)
return nil, readErr
}
report := switchReport{}
err = json.Unmarshal(body, &report)
if err != nil {
log.Infof("Error while unmarshaling report: %s", err)
return nil, err
}
return &report, nil
}
func main() { func main() {
flag.Parse() flag.Parse()
// Show version information // -- show version information
if *showVersion { if *showVersion {
v, err := version.Print("mystrom_exporter") v, err := version.Print("mystrom_exporter")
if err != nil { if err != nil {
@ -184,13 +55,18 @@ func main() {
os.Exit(0) os.Exit(0)
} }
if *switchIP == "" { // -- create a new registry for the exporter telemetry
flag.Usage() telemetryRegistry := prometheus.NewRegistry()
fmt.Println("\nNo switch.ip-address provided") telemetryRegistry.MustRegister(prometheus.NewGoCollector())
os.Exit(1) telemetryRegistry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
}
// make the build information is available through a metric mystromDurationCounterVec = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "mystrom_request_duration_seconds_total",
Help: "Total duration of mystrom successful requests by target in seconds",
}, []string{"target"})
telemetryRegistry.MustRegister(mystromDurationCounterVec)
// -- make the build information is available through a metric
buildInfo := prometheus.NewGaugeVec( buildInfo := prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "scripts", Namespace: "scripts",
@ -200,12 +76,19 @@ func main() {
[]string{"version", "revision", "branch", "goversion", "builddate", "builduser"}, []string{"version", "revision", "branch", "goversion", "builddate", "builduser"},
) )
buildInfo.WithLabelValues(version.Version, version.Revision, version.Branch, version.GoVersion, version.BuildDate, version.BuildUser).Set(1) buildInfo.WithLabelValues(version.Version, version.Revision, version.Branch, version.GoVersion, version.BuildDate, version.BuildUser).Set(1)
telemetryRegistry.MustRegister(buildInfo)
exporter := NewExporter(*switchIP) exporter := mystrom.NewExporter()
prometheus.MustRegister(exporter, buildInfo) // prometheus.MustRegister(exporter)
http.Handle(*metricsPath, promhttp.Handler()) router := http.NewServeMux()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { router.Handle(*metricsPath, promhttp.HandlerFor(telemetryRegistry, promhttp.HandlerOpts{}))
router.Handle(*devicePath,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
scrapeHandler(exporter, w, r)
}),
)
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<html> w.Write([]byte(`<html>
<head><title>myStrom switch report Exporter</title></head> <head><title>myStrom switch report Exporter</title></head>
<body> <body>
@ -214,12 +97,36 @@ func main() {
</body> </body>
</html>`)) </html>`))
}) })
_, err := FetchReport(*switchIP)
if err != nil {
log.Fatalf("Switch at address %s couldn't be reached. Ensure it is reachable before starting the exporter", *switchIP)
}
log.Infoln("Listening on address " + *listenAddress) log.Infoln("Listening on address " + *listenAddress)
log.Fatal(http.ListenAndServe(*listenAddress, nil)) log.Fatal(http.ListenAndServe(*listenAddress, router))
}
func scrapeHandler(e *mystrom.Exporter, w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("target")
if target == "" {
http.Error(w, "'target' parameter must be specified", http.StatusBadRequest)
return
}
log.Infof("got scrape request for target '%v'", target)
start := time.Now()
gatherer, err := e.Scrape(target)
duration := time.Since(start).Seconds()
if err != nil {
if strings.Contains(fmt.Sprintf("%v", err), "unable to connect with target") {
} else if strings.Contains(fmt.Sprintf("%v", err), "i/o timeout") {
}
http.Error(
w,
fmt.Sprintf("failed to scrape target '%v': %v", target, err),
http.StatusInternalServerError,
)
log.Error(err)
return
}
mystromDurationCounterVec.WithLabelValues(target).Add(duration)
promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{}).ServeHTTP(w, r)
} }

153
pkg/mystrom/mystrom.go Normal file
View file

@ -0,0 +1,153 @@
package mystrom
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
)
const namespace = "mystrom"
type switchReport struct {
Power float64 `json:"power"`
WattPerSec float64 `json:"Ws"`
Relay bool `json:relay`
Temperature float64 `json:"temperature`
}
type Exporter struct {
myStromSwitchIp string
}
var (
up = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "up"),
"Was the last myStrom query successful.",
nil, nil,
)
myStromPower = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "report_power"),
"The current power consumed by devices attached to the switch",
nil, nil,
)
myStromRelay = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "report_relay"),
"The current state of the relay (wether or not the relay is currently turned on)",
nil, nil,
)
myStromTemperature = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "report_temperatur"),
"The currently measured temperature by the switch. (Might initially be wrong, but will automatically correct itself over the span of a few hours)",
nil, nil,
)
myStromWattPerSec = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "report_watt_per_sec"),
"The average of energy consumed per second from last call this request",
nil, nil,
)
)
func NewExporter() *Exporter {
return &Exporter{
// myStromSwitchIp: myStromSwitchIp,
}
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- up
ch <- myStromPower
ch <- myStromRelay
ch <- myStromTemperature
ch <- myStromWattPerSec
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
up, prometheus.GaugeValue, 1,
)
e.FetchSwitchMetrics(e.myStromSwitchIp, ch)
}
func (e *Exporter) FetchSwitchMetrics(switchIP string, ch chan<- prometheus.Metric) {
url := "http://" + switchIP + "/report"
switchClient := http.Client{
Timeout: time.Second * 5, // 3 second timeout, might need to be increased
}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
ch <- prometheus.MustNewConstMetric(
up, prometheus.GaugeValue, 0,
)
}
req.Header.Set("User-Agent", "myStrom-exporter")
res, getErr := switchClient.Do(req)
if getErr != nil {
ch <- prometheus.MustNewConstMetric(
up, prometheus.GaugeValue, 0,
)
}
if res.Body != nil {
defer res.Body.Close()
}
body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
ch <- prometheus.MustNewConstMetric(
up, prometheus.GaugeValue, 0,
)
}
report := switchReport{}
err = json.Unmarshal(body, &report)
if err != nil {
fmt.Println(err)
ch <- prometheus.MustNewConstMetric(
up, prometheus.GaugeValue, 0,
)
return
}
ch <- prometheus.MustNewConstMetric(
myStromPower, prometheus.GaugeValue, report.Power,
)
if report.Relay {
ch <- prometheus.MustNewConstMetric(
myStromRelay, prometheus.GaugeValue, 1,
)
} else {
ch <- prometheus.MustNewConstMetric(
myStromRelay, prometheus.GaugeValue, 0,
)
}
ch <- prometheus.MustNewConstMetric(
myStromWattPerSec, prometheus.GaugeValue, report.WattPerSec,
)
ch <- prometheus.MustNewConstMetric(
myStromTemperature, prometheus.GaugeValue, report.Temperature,
)
}
func (e *Exporter) Scrape(targetAddress string) (prometheus.Gatherer, error) {
reg := prometheus.NewRegistry()
return reg, nil
}