Compare commits
	
		
			2 Commits
		
	
	
		
			v0.1.0
			...
			65eed04b5e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 65eed04b5e | |||
| 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) | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | { | ||||||
|  |   "$schema": "https://docs.renovatebot.com/renovate-schema.json" | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user