diff --git a/Makefile b/Makefile index bcf7c79..365589e 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/README.md b/README.md index 1d28069..52a4561 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/main.go b/main.go index 39daae0..fd58ca8 100644 --- a/main.go +++ b/main.go @@ -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(` - myStrom switch report Exporter - -

myStrom Exporter

-

Metrics

- - `)) + myStrom switch report Exporter + +

myStrom Exporter

+

Metrics

+ + `)) }) + 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) + } diff --git a/pkg/mystrom/mystrom.go b/pkg/mystrom/mystrom.go new file mode 100644 index 0000000..8b00aea --- /dev/null +++ b/pkg/mystrom/mystrom.go @@ -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 +}