|
|
|
@ -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{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "flow_factor",
|
|
|
|
|
Help: "Current flow factor, as a unitless number",
|
|
|
|
|
})
|
|
|
|
|
opsPrintSpeed = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "printing_speed",
|
|
|
|
|
Help: "Current print speed, as a percentage",
|
|
|
|
|
})
|
|
|
|
|
opsZPosition = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "z_position",
|
|
|
|
|
Help: "Vertical depth, in mm, of the Z head",
|
|
|
|
|
})
|
|
|
|
|
opsBed = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "temp_bed",
|
|
|
|
|
Help: "Temperature, in celsius, of the print bed",
|
|
|
|
|
})
|
|
|
|
|
opsNozzle = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "temp_nozzle",
|
|
|
|
|
Help: "Temperature, in celsius, of the print nozzle",
|
|
|
|
|
})
|
|
|
|
|
opsProgress = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "progress",
|
|
|
|
|
Help: "Current print completeness, as a percentage",
|
|
|
|
|
})
|
|
|
|
|
opsDuration = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "duration",
|
|
|
|
|
Help: "Duration of current print job, as seconds since start",
|
|
|
|
|
})
|
|
|
|
|
opsRemaining = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "remaining",
|
|
|
|
|
Help: "Estimated remaining time of current print job, as seconds",
|
|
|
|
|
})
|
|
|
|
|
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",
|
|
|
|
|
}),
|
|
|
|
|
"printing_speed": prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "printing_speed",
|
|
|
|
|
Help: "Current print speed, as a percentage",
|
|
|
|
|
}),
|
|
|
|
|
"z_position": prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "z_position",
|
|
|
|
|
Help: "Vertical depth, in mm, of the Z head",
|
|
|
|
|
}),
|
|
|
|
|
"temp_bed": prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "temp_bed",
|
|
|
|
|
Help: "Temperature, in celsius, of the print bed",
|
|
|
|
|
}),
|
|
|
|
|
"temp_nozzle": prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "temp_nozzle",
|
|
|
|
|
Help: "Temperature, in celsius, of the print nozzle",
|
|
|
|
|
}),
|
|
|
|
|
"progress": prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "progress",
|
|
|
|
|
Help: "Current print completeness, as a percentage",
|
|
|
|
|
}),
|
|
|
|
|
"duration": prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "duration",
|
|
|
|
|
Help: "Duration of current print job, as seconds since start",
|
|
|
|
|
}),
|
|
|
|
|
"remaining": prometheus.NewGauge(prometheus.GaugeOpts{
|
|
|
|
|
Namespace: "prusa_connect",
|
|
|
|
|
Name: "remaining",
|
|
|
|
|
Help: "Estimated remaining time of current print job, as seconds",
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|