First commit

This commit is contained in:
tess1o
2024-06-14 11:06:26 +03:00
parent 82fea34eb9
commit a6ba48f2a3
11 changed files with 294 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# Stage 1: Build stage
FROM golang:1.22-alpine AS build
# Set the working directory
WORKDIR /app
# Copy and download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY main.go ./
# Build the Go application
RUN CGO_ENABLED=0 GOOS=linux go build -o ecoflow-exporter .
# Stage 2: Final stage
FROM alpine:edge
# Set the working directory
WORKDIR /app
# Copy the binary from the build stage
COPY --from=build /app/ecoflow-exporter .
# Set the timezone and install CA certificates
RUN apk --no-cache add ca-certificates tzdata
EXPOSE 2112
# Set the entrypoint command
ENTRYPOINT ["/app/ecoflow-exporter"]

View File

@@ -1 +1,41 @@
# go-ecoflow-exporter
# ⚡ EcoFlow to Prometheus exporter in Go via Rest API
# Caution
It's a beta version. It works fine, I need to provide more documentation
## About the project
This is an Ecoflow metrics exporter to Prometheus implemented in Go.\
It uses library https://github.com/tess1o/go-ecoflow to fetch the metrics from your Ecoflow devices via Ecoflow Rest
API. More details about the API are on their website: https://developer-eu.ecoflow.com/
Other known to me projects use MQTT protocol to scrap the metrics, this implementation uses Rest API.
## Compare to other exporters
This implementation is inspired by https://github.com/berezhinskiy/ecoflow_exporter, and it's fully
compatible with their grafana dashboard. This exporter by default uses the same prefix for the metrics (`ecoflow`)
Both exporters support all parameters returned by the API. MQTT and Rest API actually return the same set of parameters.
This implementation was tested on Delta 2 and River 2.
Some difference between this project and https://github.com/berezhinskiy/ecoflow_exporter:
1. This project requires `ACCESS_KEY` and `SECRET_KEY` that can be obtained on https://developer-eu.ecoflow.com/. For
me, it took less than 1 day until Ecoflow approved access to the API. The other project needs only ecoflow
credentials (login and password)
2. This project doesn't hardcode devices `Serial Numbers` and this exporter can export metrics from all linked devices.
Internally it uses Ecoflow API to fetch the list of linked devices and then for each device it exports the
metrics. https://github.com/berezhinskiy/ecoflow_exporter can export metrics for a single device only, and you have
to hardcode it's Serial Number in the env variables. If you have 5 devices, then you need to run 5 instances of the
exporter
3. The image size (not compressed!) of this exporter is only 21MB, `ghcr.io/berezhinskiy/ecoflow_exporter` is 142 MB
4. This implementation is extremely lightweight and barely consumes any RAM & CPU (it needs less than 10MB of RAM to
scrap metrics from 2 devices)
# TODO
1. Instructions how to get AccessToken and SecretToken
2. Instructions how to run exporter, grafana and prometheus
3. Instructions how to import grafana dashboard
4. Github workflow to automatically push new

7
docker-compose/.env Normal file
View File

@@ -0,0 +1,7 @@
ECOFLOW_ACCESS_KEY=your_access_key_from_ecoflow
ECOFLOW_SECRET_KEY=your_secret_key_from_ecoflow
METRIC_PREFIX=ecoflow
PROMETHEUS_INTERVAL=10
DEBUG_ENABLED=false
GRAFANA_USERNAME=grafana
GRAFANA_PASSWORD=grafana

View File

@@ -0,0 +1,43 @@
services:
prometheus:
image: prom/prometheus
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- 9090:9090
restart: unless-stopped
volumes:
- ./prometheus:/etc/prometheus
- prometheus_data:/prometheus
grafana:
image: grafana/grafana
container_name: grafana
ports:
- 3000:3000
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_USER: "${GRAFANA_USERNAME}"
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_PASSWORD}"
volumes:
- ./grafana:/etc/grafana/provisioning/datasources
- grafana_data:/var/lib/grafana
go_ecoflow_exporter:
build:
context: ./..
dockerfile: ./Dockerfile
container_name: go_ecoflow_exporter
ports:
- 2112:2112
restart: unless-stopped
environment:
ACCESS_KEY: ${ACCESS_KEY}
SECRET_KEY: ${SECRET_KEY}
METRIC_PREFIX: ${METRIC_PREFIX}
PROMETHEUS_INTERVAL: ${PROMETHEUS_INTERVAL}
volumes:
prometheus_data:
grafana_data:

View File

@@ -0,0 +1,41 @@
services:
prometheus:
image: prom/prometheus
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- 9090:9090
restart: unless-stopped
volumes:
- ./prometheus:/etc/prometheus
- prometheus_data:/prometheus
grafana:
image: grafana/grafana
container_name: grafana
ports:
- 3000:3000
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_USER: "${GRAFANA_USERNAME}"
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_PASSWORD}"
volumes:
- ./grafana:/etc/grafana/provisioning/datasources
- grafana_data:/var/lib/grafana
go_ecoflow_exporter:
image: tess1o/go-ecoflow-exporter:latest
container_name: go_ecoflow_exporter
ports:
- 2112:2112
restart: unless-stopped
environment:
ECOFLOW_ACCESS_KEY: ${ECOFLOW_ACCESS_KEY}
ECOFLOW_SECRET_KEY: ${ECOFLOW_SECRET_KEY}
METRIC_PREFIX: ${METRIC_PREFIX}
PROMETHEUS_INTERVAL: ${PROMETHEUS_INTERVAL}
volumes:
prometheus_data:
grafana_data:

View File

@@ -0,0 +1,9 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
isDefault: true
access: proxy
editable: true

View File

@@ -0,0 +1,18 @@
global:
scrape_interval: 10s # Set the scrape interval to every 10 seconds. Default is every 1 minute.
scrape_timeout: 10s
evaluation_interval: 10s # Evaluate rules every 10 seconds. The default is every 1 minute.
scrape_configs:
- job_name: prometheus
static_configs:
- targets:
- localhost:9090
- job_name: ecoflow-exporter
static_configs:
- targets:
- go_ecoflow_exporter:2112

75
exporter.go Normal file
View File

@@ -0,0 +1,75 @@
package main
import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/tess1o/go-ecoflow"
"log/slog"
"net/http"
"os"
"strconv"
"time"
)
const (
defaultMetricPrefix = "ecoflow"
defaultInterval = 30
)
func main() {
setLoggerLevel()
accessKey := os.Getenv("ECOFLOW_ACCESS_KEY")
secretKey := os.Getenv("ECOFLOW_SECRET_KEY")
if accessKey == "" || secretKey == "" {
slog.Error("AccessKey and SecretKey are mandatory")
return
}
client := ecoflow.NewEcoflowClient(accessKey, secretKey)
metricPrefix := getStringOrDefault("METRIC_PREFIX", defaultMetricPrefix)
interval := getIntOrDefault("PROMETHEUS_INTERVAL", defaultInterval)
// configure prometheus scrap interval and metric prefix
config := ecoflow.PrometheusConfig{
Interval: time.Second * time.Duration(interval),
Prefix: metricPrefix,
}
client.RecordPrometheusMetrics(&config)
// start server with metrics
slog.Info("Starting server on port 2112. Metrics are available at http://localhost:2112/metrics")
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":2112", nil)
}
func setLoggerLevel() {
debugEnabled := os.Getenv("DEBUG_ENABLED")
if debugEnabled == "true" || debugEnabled == "1" {
slog.SetLogLoggerLevel(slog.LevelDebug)
} else {
slog.SetLogLoggerLevel(slog.LevelInfo)
}
}
func getStringOrDefault(key, def string) string {
val, exists := os.LookupEnv(key)
if exists {
return val
}
return def
}
func getIntOrDefault(key string, def int) int {
val, exists := os.LookupEnv(key)
if exists {
intVal, err := strconv.Atoi(val)
if err != nil {
return def
}
return intVal
}
return def
}

18
go.mod Normal file
View File

@@ -0,0 +1,18 @@
module go-ecoflow-usage
go 1.22
require (
github.com/prometheus/client_golang v1.19.1
github.com/tess1o/go-ecoflow v0.0.0-20240613201311-d00152d1ead3
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
golang.org/x/sys v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)

9
go.sum Normal file
View File

@@ -0,0 +1,9 @@
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/tess1o/go-ecoflow v0.0.0-20240613201311-d00152d1ead3/go.mod h1:HtMB5DptewhOD+SbAwLtfGlWPq504RUjDhRx+NqxBxs=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=