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)'
mac: ## builds the macos version of the exporter
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:
GOOS=linux GOARCH=arm64 go build $(GOFLAGS) -ldflags '$(LDFLAGS)'
arm:

View file

@ -28,9 +28,31 @@ $ ./mystrom-exporter --help
```
| Flag | Description | Default |
| ---- | ----------- | ------- |
| switch.ip-address | IP address of the switch you try to monitor | `` |
| 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
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 | arm |
| 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.
The docker image is only built & tested for amd64.

227
main.go
View file

@ -1,18 +1,18 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/log"
"mystrom-exporter/pkg/mystrom"
"mystrom-exporter/pkg/version"
)
@ -29,151 +29,22 @@ var (
listenAddress = flag.String("web.listen-address", ":9452",
"Address to listen on")
metricsPath = flag.String("web.metrics-path", "/metrics",
"Path under which to expose metrics")
switchIP = flag.String("switch.ip-address", "",
"IP address of the switch you try to monitor")
"Path under which to expose exporters own metrics")
devicePath = flag.String("web.device-path", "/device",
"Path under which the metrics of the devices are fetched")
showVersion = flag.Bool("version", false,
"Show version information.")
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,
)
)
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
}
var (
mystromDurationCounterVec *prometheus.CounterVec
mystromRequestsCounterVec *prometheus.CounterVec
)
func main() {
flag.Parse()
// Show version information
// -- show version information
if *showVersion {
v, err := version.Print("mystrom_exporter")
if err != nil {
@ -184,13 +55,18 @@ func main() {
os.Exit(0)
}
if *switchIP == "" {
flag.Usage()
fmt.Println("\nNo switch.ip-address provided")
os.Exit(1)
}
// -- create a new registry for the exporter telemetry
telemetryRegistry := prometheus.NewRegistry()
telemetryRegistry.MustRegister(prometheus.NewGoCollector())
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(
prometheus.GaugeOpts{
Namespace: "scripts",
@ -200,26 +76,57 @@ func main() {
[]string{"version", "revision", "branch", "goversion", "builddate", "builduser"},
)
buildInfo.WithLabelValues(version.Version, version.Revision, version.Branch, version.GoVersion, version.BuildDate, version.BuildUser).Set(1)
telemetryRegistry.MustRegister(buildInfo)
exporter := NewExporter(*switchIP)
prometheus.MustRegister(exporter, buildInfo)
exporter := mystrom.NewExporter()
// prometheus.MustRegister(exporter)
http.Handle(*metricsPath, promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
router := http.NewServeMux()
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>
<head><title>myStrom switch report Exporter</title></head>
<body>
<h1>myStrom Exporter</h1>
<p><a href='` + *metricsPath + `'>Metrics</a></p>
</body>
</html>`))
<head><title>myStrom switch report Exporter</title></head>
<body>
<h1>myStrom Exporter</h1>
<p><a href='` + *metricsPath + `'>Metrics</a></p>
</body>
</html>`))
})
log.Infoln("Listening on address " + *listenAddress)
log.Fatal(http.ListenAndServe(*listenAddress, router))
}
_, 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)
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.Infoln("Listening on address " + *listenAddress)
log.Fatal(http.ListenAndServe(*listenAddress, nil))
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
}