// Package prometheus writes node_exporter textfile-compatible metrics.
//
// See mercemay.top/src/portr/ for context.
package prometheus
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/mercemay/portr/internal/check"
)
// Writer writes a .prom file suitable for the textfile collector.
type Writer struct {
Path string
}
// New returns a Writer that will output to path (parent dir must exist).
func New(path string) *Writer { return &Writer{Path: path} }
// Name satisfies output.Named.
func (*Writer) Name() string { return "prometheus" }
// Write renders metrics to w (used by tests). The Path-based Flush is
// the usual entry point from the CLI.
func (*Writer) Write(w io.Writer, r check.Report) error {
fmt.Fprintln(w, "# HELP portr_target_open 1 if reachable, 0 if not.")
fmt.Fprintln(w, "# TYPE portr_target_open gauge")
for _, res := range r.Results {
val := 0
if res.Open {
val = 1
}
fmt.Fprintf(w, "portr_target_open{host=%q,port=\"%d\",name=%q} %d\n",
res.Target.Host, res.Target.Port, res.Target.Name, val)
}
fmt.Fprintln(w, "# HELP portr_target_latency_seconds Last observed latency.")
fmt.Fprintln(w, "# TYPE portr_target_latency_seconds gauge")
for _, res := range r.Results {
fmt.Fprintf(w, "portr_target_latency_seconds{host=%q,port=\"%d\"} %f\n",
res.Target.Host, res.Target.Port, res.Latency.Seconds())
}
return nil
}
// Flush writes to Writer.Path atomically (temp file + rename).
func (p *Writer) Flush(r check.Report) error {
dir, base := filepath.Split(p.Path)
if !strings.HasSuffix(base, ".prom") {
return fmt.Errorf("prometheus: path must end in .prom (got %q)", base)
}
tmp, err := os.CreateTemp(dir, base+".tmp-*")
if err != nil {
return err
}
if err := p.Write(tmp, r); err != nil {
_ = tmp.Close()
_ = os.Remove(tmp.Name())
return err
}
if err := tmp.Close(); err != nil {
_ = os.Remove(tmp.Name())
return err
}
return os.Rename(tmp.Name(), p.Path)
}