upgrade in prep for PC 4.4.0

This commit is contained in:
David 2022-08-06 14:30:42 -04:00
parent 016465aaa9
commit 4d7e15d719
2 changed files with 255 additions and 87 deletions

21
README.md Normal file
View File

@ -0,0 +1,21 @@
# Prusa Connect Prometheus Exporter
Version v0.1.0 of this exporter supports the pre-4.4.0 changes to the Prusa Connect API (non-API-key version).
Current master branch supports the new, 4.4.0-beta2 release of Prusa Connect as well as the older API.
## Usage
`./prusa-connect-exporter -h`
* `apikey`
Prusa Connect API key (see Main Menu -> Settings -> Network on the printer).
If no key is provided, exporter assumes the older API layout when querying the printer.
* `hostname`
Hostname the Prusa Connect API is available at (assumes http) (default "localhost")
* `interval`
How often, in seconds, to query the API (default 2)
* `path`
Local path to export metrics on (default "metrics")
* `port`
Local port to export metrics on (default "2112")

275
main.go
View File

@ -15,7 +15,8 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
) )
type Metrics struct { // original /api/telemetry type struct
type MetricsV1 struct {
TempNozzle int `json:"temp_nozzle"` TempNozzle int `json:"temp_nozzle"`
TempBed int `json:"temp_bed"` TempBed int `json:"temp_bed"`
Material string `json:"material"` Material string `json:"material"`
@ -29,68 +30,135 @@ type Metrics struct {
ProjectName string `json:"project_name"` ProjectName string `json:"project_name"`
} }
// new 4.4.0+ /api/printer struct
type MetricsV2 struct {
Telemetry Telemetry `json:"telemetry"`
Temperature Temperature `json:"temperature"`
State State `json:"state"`
}
type Telemetry struct {
TempBed float64 `json:"temp-bed"`
TempNozzle float64 `json:"temp-nozzle"`
PrintSpeed int `json:"print-speed"`
ZHeight float64 `json:"z-height"`
Material string `json:"material"`
}
type Temperature struct {
Tool0 Tempatures `json:"tool0"`
Bed Tempatures `json:"bed"`
}
type Tempatures struct {
Actual float64 `json:"actual"`
Target float64 `json:"target"`
Display float64 `json:"display"`
Offset float64 `json:"offset"`
}
type State struct {
Text string `json:"text"`
Flags Flags `json:"flags"`
}
type Flags struct {
Operational bool `json:"operational"`
Paused bool `json:"paused"`
Printing bool `json:"printing"`
Cancelling bool `json:"cancelling"`
Pausing bool `json:"pausing"`
SdReady bool `json:"sdReady"`
Error bool `json:"error"`
ClosedOnError bool `json:"closedOnError"`
Ready bool `json:"ready"`
Busy bool `json:"busy"`
}
// /api/job struct
type JobV2 struct {
State string `json:"state"`
Job Job `json:"job"`
Progress Progress `json:"progress"`
}
type File struct {
Name string `json:"name"`
Path string `json:"path"`
Display string `json:"display"`
}
type Job struct {
EstimatedPrintTime int `json:"estimatedPrintTime"`
File File `json:"file"`
}
type Progress struct {
Completion float64 `json:"completion"`
PrintTime int `json:"printTime"`
PrintTimeLeft int `json:"printTimeLeft"`
}
type Config struct { type Config struct {
QueryHostname string QueryHostname string
QueryInterval int QueryInterval int
Port string Port string
Path string Path string
APIKey string
} }
var ( var (
opsFlowFactor = prometheus.NewGauge(prometheus.GaugeOpts{ errCount = 0
gauges = map[string]prometheus.Gauge{
"flow_factor": prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prusa_connect", Namespace: "prusa_connect",
Name: "flow_factor", Name: "flow_factor",
Help: "Current flow factor, as a unitless number", Help: "Current flow factor, as a unitless number",
}) }),
opsPrintSpeed = prometheus.NewGauge(prometheus.GaugeOpts{ "printing_speed": prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prusa_connect", Namespace: "prusa_connect",
Name: "printing_speed", Name: "printing_speed",
Help: "Current print speed, as a percentage", Help: "Current print speed, as a percentage",
}) }),
opsZPosition = prometheus.NewGauge(prometheus.GaugeOpts{ "z_position": prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prusa_connect", Namespace: "prusa_connect",
Name: "z_position", Name: "z_position",
Help: "Vertical depth, in mm, of the Z head", Help: "Vertical depth, in mm, of the Z head",
}) }),
opsBed = prometheus.NewGauge(prometheus.GaugeOpts{ "temp_bed": prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prusa_connect", Namespace: "prusa_connect",
Name: "temp_bed", Name: "temp_bed",
Help: "Temperature, in celsius, of the print bed", Help: "Temperature, in celsius, of the print bed",
}) }),
opsNozzle = prometheus.NewGauge(prometheus.GaugeOpts{ "temp_nozzle": prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prusa_connect", Namespace: "prusa_connect",
Name: "temp_nozzle", Name: "temp_nozzle",
Help: "Temperature, in celsius, of the print nozzle", Help: "Temperature, in celsius, of the print nozzle",
}) }),
opsProgress = prometheus.NewGauge(prometheus.GaugeOpts{ "progress": prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prusa_connect", Namespace: "prusa_connect",
Name: "progress", Name: "progress",
Help: "Current print completeness, as a percentage", Help: "Current print completeness, as a percentage",
}) }),
opsDuration = prometheus.NewGauge(prometheus.GaugeOpts{ "duration": prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prusa_connect", Namespace: "prusa_connect",
Name: "duration", Name: "duration",
Help: "Duration of current print job, as seconds since start", Help: "Duration of current print job, as seconds since start",
}) }),
opsRemaining = prometheus.NewGauge(prometheus.GaugeOpts{ "remaining": prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prusa_connect", Namespace: "prusa_connect",
Name: "remaining", Name: "remaining",
Help: "Estimated remaining time of current print job, as seconds", Help: "Estimated remaining time of current print job, as seconds",
}) }),
errCount = 0 }
) )
func errLog(err error) { func errLog(err error) {
errCount++ errCount++
// reset metrics // reset metrics
opsFlowFactor.Set(0) for _, g := range gauges {
opsPrintSpeed.Set(0) g.Set(0)
opsZPosition.Set(0) }
opsBed.Set(0)
opsNozzle.Set(0)
opsProgress.Set(0)
opsDuration.Set(0)
opsRemaining.Set(0)
if errCount == 5 { if errCount == 5 {
log.Printf("suppressing further error logging") log.Printf("suppressing further error logging")
return return
@ -109,42 +177,124 @@ func recordMetrics(ctx context.Context, config Config) {
return return
default: default:
time.Sleep(time.Second * time.Duration(config.QueryInterval)) time.Sleep(time.Second * time.Duration(config.QueryInterval))
res, err := http.Get(config.QueryHostname + "/api/telemetry") if config.APIKey == "" {
if err != nil { parseMetricsV1(ctx, config)
errLog(err)
break break
} }
b, err := io.ReadAll(res.Body) parseMetricsV2(ctx, config)
if err != nil {
errLog(err)
break
}
t := &Metrics{}
err = json.Unmarshal(b, t)
if err != nil {
errLog(err)
break
}
if errCount != 0 {
log.Printf("connection established")
}
errCount = 0
opsFlowFactor.Set(float64(t.FlowFactor))
opsPrintSpeed.Set(float64(t.PrintingSpeed))
opsZPosition.Set(t.ZPosition)
opsBed.Set(float64(t.TempBed))
opsNozzle.Set(float64(t.TempNozzle))
opsProgress.Set(float64(t.Progress))
opsDuration.Set(float64(parseDuration(t.PrintDur) / time.Second))
estimatedTimeRemaining, err := strconv.ParseFloat(t.TimeEst, 64)
if err == nil {
opsRemaining.Set(float64(estimatedTimeRemaining))
}
} }
} }
}() }()
} }
func parseMetricsV1(ctx context.Context, config Config) {
t, err := getTelemetry(config.QueryHostname)
if err != nil {
errLog(err)
return
}
if errCount != 0 {
errCount = 0
log.Printf("connection established")
}
gauges["flow_factor"].Set(float64(t.FlowFactor))
gauges["printing_speed"].Set(float64(t.PrintingSpeed))
gauges["z_position"].Set(t.ZPosition)
gauges["temp_bed"].Set(float64(t.TempBed))
gauges["temp_nozzle"].Set(float64(t.TempNozzle))
gauges["progress"].Set(float64(t.Progress))
gauges["duration"].Set(float64(parseDuration(t.PrintDur) / time.Second))
estimatedTimeRemaining, err := strconv.ParseFloat(t.TimeEst, 64)
if err == nil {
gauges["remaining"].Set(float64(estimatedTimeRemaining))
}
}
func parseMetricsV2(ctx context.Context, config Config) {
t, err := getPrinter(config.Port, config.APIKey)
if err != nil {
errLog(err)
return
}
j, err := getJob(config.Port, config.APIKey)
if err != nil {
errLog(err)
return
}
if errCount != 0 {
errCount = 0
log.Printf("connection established")
}
gauges["printing_speed"].Set(float64(t.Telemetry.PrintSpeed))
gauges["z_position"].Set(t.Telemetry.ZHeight)
gauges["temp_bed"].Set(float64(t.Telemetry.TempBed))
gauges["temp_nozzle"].Set(float64(t.Telemetry.TempNozzle))
gauges["progress"].Set(float64(j.Progress.Completion) * 100)
gauges["duration"].Set(float64(j.Progress.PrintTime))
gauges["remaining"].Set(float64(j.Progress.PrintTimeLeft))
}
func getJob(hostname, apiKey string) (*JobV2, error) {
r, err := http.NewRequest(http.MethodGet, hostname+"/api/job", nil)
if err != nil {
return &JobV2{}, err
}
r.Header.Set("X-Api-Key", apiKey)
res, err := http.DefaultClient.Do(r)
if err != nil {
return &JobV2{}, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return &JobV2{}, err
}
j := &JobV2{}
err = json.Unmarshal(b, j)
if err != nil {
return &JobV2{}, err
}
return j, nil
}
func getPrinter(hostname, apiKey string) (*MetricsV2, error) {
r, err := http.NewRequest(http.MethodGet, hostname+"/api/printer", nil)
if err != nil {
return &MetricsV2{}, err
}
r.Header.Set("X-Api-Key", apiKey)
res, err := http.DefaultClient.Do(r)
if err != nil {
return &MetricsV2{}, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return &MetricsV2{}, err
}
j := &MetricsV2{}
err = json.Unmarshal(b, j)
if err != nil {
return &MetricsV2{}, err
}
return j, nil
}
func getTelemetry(hostname string) (*MetricsV1, error) {
res, err := http.Get(hostname + "/api/telemetry")
if err != nil {
return &MetricsV1{}, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return &MetricsV1{}, err
}
t := &MetricsV1{}
err = json.Unmarshal(b, t)
if err != nil {
return &MetricsV1{}, err
}
return t, nil
}
// taking a guess on proper parsing here... // taking a guess on proper parsing here...
func parseDuration(d string) time.Duration { func parseDuration(d string) time.Duration {
dur := 0 dur := 0
@ -187,6 +337,8 @@ func main() {
flag.StringVar(&port, "port", "2112", "Local port to export metrics on") flag.StringVar(&port, "port", "2112", "Local port to export metrics on")
var path string var path string
flag.StringVar(&path, "path", "metrics", "Local path to export metrics on") flag.StringVar(&path, "path", "metrics", "Local path to export metrics on")
var apiKey string
flag.StringVar(&apiKey, "apikey", "", "Prusa Connect API key (see Main Menu -> Settings -> Network on the printer)")
flag.Parse() flag.Parse()
if queryInterval < 1 { if queryInterval < 1 {
@ -198,18 +350,13 @@ func main() {
QueryInterval: queryInterval, QueryInterval: queryInterval,
Port: port, Port: port,
Path: path, Path: path,
APIKey: apiKey,
} }
r := prometheus.NewRegistry() r := prometheus.NewRegistry()
r.MustRegister(opsNozzle) for _, g := range gauges {
r.MustRegister(opsBed) r.MustRegister(g)
r.MustRegister(opsZPosition) }
r.MustRegister(opsPrintSpeed)
r.MustRegister(opsFlowFactor)
r.MustRegister(opsProgress)
r.MustRegister(opsDuration)
r.MustRegister(opsRemaining)
recordMetrics(context.Background(), config) recordMetrics(context.Background(), config)
log.Printf("starting exporter on :%v", config.Port) log.Printf("starting exporter on :%v", config.Port)