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"
|
"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)
|
||||||
|
Loading…
Reference in New Issue
Block a user