package main import ( "context" "encoding/json" "flag" "io" "log" "net/http" "strconv" "strings" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) type Metrics struct { TempNozzle int `json:"temp_nozzle"` TempBed int `json:"temp_bed"` Material string `json:"material"` ZPosition float64 `json:"pos_z_mm"` PrintingSpeed int `json:"printing_speed"` FlowFactor int `json:"flow_factor"` Progress int `json:"progress"` PrintDur string `json:"print_dur"` TimeEst string `json:"time_est"` TimeZone string `json:"time_zone"` ProjectName string `json:"project_name"` } type Config struct { QueryHostname string QueryInterval int Port string Path 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 ) func errLog(err error) { errCount++ // reset metrics opsFlowFactor.Set(0) opsPrintSpeed.Set(0) opsZPosition.Set(0) opsBed.Set(0) opsNozzle.Set(0) if errCount == 5 { log.Printf("suppressing further error logging") return } else if errCount > 5 { return } log.Printf("error retrieving telemetry: %v", err) } func recordMetrics(ctx context.Context, config Config) { go func() { for { select { case <-ctx.Done(): log.Print("exiting!") return default: time.Sleep(time.Second * time.Duration(config.QueryInterval)) res, err := http.Get(config.QueryHostname + "/api/telemetry") if err != nil { errLog(err) 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)) } } } }() } // taking a guess on proper parsing here... func parseDuration(d string) time.Duration { dur := 0 d = strings.TrimSpace(d) number := []rune{} for _, t := range d { switch t { case ' ': continue case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0': number = append(number, t) case 's': i, _ := strconv.Atoi(string(number)) dur += i * int(time.Second) number = []rune{} case 'm': i, _ := strconv.Atoi(string(number)) dur += (i * int(time.Minute)) number = []rune{} case 'h': i, _ := strconv.Atoi(string(number)) dur += (i * int(time.Hour)) number = []rune{} case 'd': i, _ := strconv.Atoi(string(number)) dur += (i * int(time.Hour) * 24) number = []rune{} } } return time.Duration(dur) } func main() { var hostname string flag.StringVar(&hostname, "hostname", "localhost", "Hostname the Prusa Connect API is available at (assumes http)") var queryInterval int flag.IntVar(&queryInterval, "interval", 2, "How often, in seconds, to query the API") var port string 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") flag.Parse() if queryInterval < 1 { log.Fatalf("query interval must be greater than 0; %d received", queryInterval) } config := Config{ QueryHostname: "http://" + hostname, QueryInterval: queryInterval, Port: port, Path: path, } 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) recordMetrics(context.Background(), config) log.Printf("starting exporter on :%v", config.Port) http.Handle("/"+config.Path, promhttp.HandlerFor(r, promhttp.HandlerOpts{})) http.ListenAndServe(":"+config.Port, nil) }