You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
219 lines
5.6 KiB
219 lines
5.6 KiB
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) |
|
opsProgress.Set(0) |
|
opsDuration.Set(0) |
|
opsRemaining.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) |
|
}
|
|
|