upgrade in prep for PC 4.4.0
This commit is contained in:
parent
016465aaa9
commit
4d7e15d719
21
README.md
Normal file
21
README.md
Normal 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
275
main.go
@ -15,7 +15,8 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
// original /api/telemetry type struct
|
||||
type MetricsV1 struct {
|
||||
TempNozzle int `json:"temp_nozzle"`
|
||||
TempBed int `json:"temp_bed"`
|
||||
Material string `json:"material"`
|
||||
@ -29,68 +30,135 @@ type Metrics struct {
|
||||
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 {
|
||||
QueryHostname string
|
||||
QueryInterval int
|
||||
Port string
|
||||
Path string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
var (
|
||||
opsFlowFactor = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
errCount = 0
|
||||
gauges = map[string]prometheus.Gauge{
|
||||
"flow_factor": prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "prusa_connect",
|
||||
Name: "flow_factor",
|
||||
Help: "Current flow factor, as a unitless number",
|
||||
})
|
||||
opsPrintSpeed = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
}),
|
||||
"printing_speed": prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "prusa_connect",
|
||||
Name: "printing_speed",
|
||||
Help: "Current print speed, as a percentage",
|
||||
})
|
||||
opsZPosition = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
}),
|
||||
"z_position": prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "prusa_connect",
|
||||
Name: "z_position",
|
||||
Help: "Vertical depth, in mm, of the Z head",
|
||||
})
|
||||
opsBed = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
}),
|
||||
"temp_bed": prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "prusa_connect",
|
||||
Name: "temp_bed",
|
||||
Help: "Temperature, in celsius, of the print bed",
|
||||
})
|
||||
opsNozzle = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
}),
|
||||
"temp_nozzle": prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "prusa_connect",
|
||||
Name: "temp_nozzle",
|
||||
Help: "Temperature, in celsius, of the print nozzle",
|
||||
})
|
||||
opsProgress = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
}),
|
||||
"progress": prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "prusa_connect",
|
||||
Name: "progress",
|
||||
Help: "Current print completeness, as a percentage",
|
||||
})
|
||||
opsDuration = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
}),
|
||||
"duration": prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "prusa_connect",
|
||||
Name: "duration",
|
||||
Help: "Duration of current print job, as seconds since start",
|
||||
})
|
||||
opsRemaining = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
}),
|
||||
"remaining": prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "prusa_connect",
|
||||
Name: "remaining",
|
||||
Help: "Estimated remaining time of current print job, as seconds",
|
||||
})
|
||||
errCount = 0
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func errLog(err error) {
|
||||
errCount++
|
||||
// reset metrics
|
||||
opsFlowFactor.Set(0)
|
||||
opsPrintSpeed.Set(0)
|
||||
opsZPosition.Set(0)
|
||||
opsBed.Set(0)
|
||||
opsNozzle.Set(0)
|
||||
opsProgress.Set(0)
|
||||
opsDuration.Set(0)
|
||||
opsRemaining.Set(0)
|
||||
for _, g := range gauges {
|
||||
g.Set(0)
|
||||
}
|
||||
if errCount == 5 {
|
||||
log.Printf("suppressing further error logging")
|
||||
return
|
||||
@ -109,42 +177,124 @@ func recordMetrics(ctx context.Context, config Config) {
|
||||
return
|
||||
default:
|
||||
time.Sleep(time.Second * time.Duration(config.QueryInterval))
|
||||
res, err := http.Get(config.QueryHostname + "/api/telemetry")
|
||||
if err != nil {
|
||||
errLog(err)
|
||||
if config.APIKey == "" {
|
||||
parseMetricsV1(ctx, config)
|
||||
break
|
||||
}
|
||||
b, err := io.ReadAll(res.Body)
|
||||
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))
|
||||
}
|
||||
parseMetricsV2(ctx, config)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
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...
|
||||
func parseDuration(d string) time.Duration {
|
||||
dur := 0
|
||||
@ -187,6 +337,8 @@ func main() {
|
||||
flag.StringVar(&port, "port", "2112", "Local port to export metrics on")
|
||||
var path string
|
||||
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()
|
||||
|
||||
if queryInterval < 1 {
|
||||
@ -198,18 +350,13 @@ func main() {
|
||||
QueryInterval: queryInterval,
|
||||
Port: port,
|
||||
Path: path,
|
||||
APIKey: apiKey,
|
||||
}
|
||||
|
||||
r := prometheus.NewRegistry()
|
||||
r.MustRegister(opsNozzle)
|
||||
r.MustRegister(opsBed)
|
||||
r.MustRegister(opsZPosition)
|
||||
r.MustRegister(opsPrintSpeed)
|
||||
r.MustRegister(opsFlowFactor)
|
||||
r.MustRegister(opsProgress)
|
||||
r.MustRegister(opsDuration)
|
||||
r.MustRegister(opsRemaining)
|
||||
|
||||
for _, g := range gauges {
|
||||
r.MustRegister(g)
|
||||
}
|
||||
recordMetrics(context.Background(), config)
|
||||
|
||||
log.Printf("starting exporter on :%v", config.Port)
|
||||
|
Loading…
Reference in New Issue
Block a user