upgrade in prep for PC 4.4.0
This commit is contained in:
		
							
								
								
									
										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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user