commit a78752b4967a00fe84725544d6f9881c0db96ab3 Author: Valentin Doreau Date: Mon Apr 1 17:02:29 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39eedc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +prometheus-borg-exporter +test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d368953 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +.PHONY: all build clean + +PROJECT_NAME = $(shell head -n1 go.mod | cut -d'/' -f2) + + +all: build + +# Build static executable +build: + CGO_ENABLED=0 go build + strip $(PROJECT_NAME) + +clean: + rm $(PROJECT_NAME) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c3b629 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Prometheus Borg(Backup) exporter + +Simple [Prometheus](https://prometheus.io/) exporter for [BorgBackup](https://www.borgbackup.org/). + +Rewritten in go from https://code.recycled.cloud/RecycledCloud/prometheus-borgbackup-exporter. +It has been rewritten mainly to enable building a static binary but also to support ipv6. +Additional features have also been added like `last_archive_time`. + +## Building + +To build, you'll need at least the [go toolchain](https://go.dev/doc/install) installed. +On Ubuntu you can generally have the latest version by running: + +```sh +snap install go --classic +``` + +Then run: + +```sh +go build +``` + +Note this executable is dynamically linked and not stripped. + +### Static executable + +To build a static executable, you'll need `make` installed. You can then simply run: + +```sh +make +``` + +## Useful links: + +- [Writing a go exporter](https://prometheus.io/docs/guides/go-application/) +- [Writing (Prometheus) exporters guidelines](https://prometheus.io/docs/instrumenting/writing_exporters/) +- [Prometheus's `client_golang` library](https://github.com/prometheus/client_golang) + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8506ea7 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module code.recycled.cloud/prometheus-borg-exporter + +go 1.22.1 + +require github.com/prometheus/client_golang v1.19.0 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/sys v0.16.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..827eb22 --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/info.go b/info.go new file mode 100644 index 0000000..fcce269 --- /dev/null +++ b/info.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "os/exec" + "time" +) + +type Info struct { + Cache struct { + Stats struct { + Total_chunks float64 + Total_csize float64 + Total_size float64 + Total_unique_chunks float64 + Unique_csize float64 + Unique_size float64 + } + } + Repository struct { + Last_modified string + } +} + +func (info *Info) LastmodUnix() float64 { + lastmod, err := time.Parse("2006-01-02T15:04:05.999999", info.Repository.Last_modified) + if err != nil { + return 0 + } + + return float64(lastmod.Unix()) +} + +func GetInfo(path string) (*Info, error) { + cmd := exec.Command("borg", "info", "--json", path) + cmd.Env = append(cmd.Env, "BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes") + + stdout, err := cmd.Output() + if err != nil { + return nil, err + } + + data := &Info{} + err = json.Unmarshal([]byte(stdout), data) + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/list.go b/list.go new file mode 100644 index 0000000..a50eb49 --- /dev/null +++ b/list.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "os/exec" + "time" +) + +type List struct { + Archives []struct { + Time string + } +} + +func (list *List) LastArchiveUnix() float64 { + if len(list.Archives) == 0 { + return 0 + } + + last, err := time.Parse("2006-01-02T15:04:05.999999", list.Archives[len(list.Archives)-1].Time) + if err != nil { + return 0 + } + + return float64(last.Unix()) +} + +func GetList(path string) (*List, error) { + cmd := exec.Command("borg", "list", "--json", path) + cmd.Env = append(cmd.Env, "BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes") + + stdout, err := cmd.Output() + if err != nil { + return nil, err + } + + data := &List{} + err = json.Unmarshal([]byte(stdout), data) + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..488b33d --- /dev/null +++ b/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const VERSION = "0.1.0-alpha.0" + +const LISTEN_ADDR = ":9403" +const INTERVAL = 30 * time.Minute + +var backupDir = flag.String("backup-dir", "/srv/backups", "Directory where the backups are located") +var version_flag = flag.Bool("version", false, "Shows the program version") + +func main() { + flag.Parse() + + if *version_flag { + fmt.Printf("%v\n", VERSION) + os.Exit(0) + } + + reg := prometheus.NewRegistry() + + log.Printf("Backup directory is: %v\n", *backupDir) + + m := NewMetrics(reg) + + go RecordMetrics(*m) + + http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg})) + + log.Printf("Listening on %v ...\n", LISTEN_ADDR) + + log.Fatal(http.ListenAndServe(LISTEN_ADDR, nil)) +} + +func RecordMetrics(m Metrics) { + for { + entries, err := os.ReadDir(*backupDir) + if err != nil { + log.Println(err) + os.Exit(1) + } + + for _, entry := range entries { + if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { + log.Printf(">> Ignoring %v\n", entry.Name()) + continue + } + + path := filepath.Join(*backupDir, entry.Name()) + + info, err := GetInfo(path) + if err != nil { + log.Printf(">> Could not get info about %v: %v\n", path, err) + continue + } + list, err := GetList(path) + if err != nil { + log.Printf(">> Could not get archive list from %v: %v\n", path, err) + continue + } + + stats := info.Cache.Stats + + log.Printf("> Got info for: %v\n", path) + m.ArchiveCount.With(prometheus.Labels{"repo_name": entry.Name()}).Set(float64(len(list.Archives))) + m.LastArchiveTime.With(prometheus.Labels{"repo_name": entry.Name()}).Set(list.LastArchiveUnix()) + m.LastModified.With(prometheus.Labels{"repo_name": entry.Name()}).Set(info.LastmodUnix()) + m.TotalChunks.With(prometheus.Labels{"repo_name": entry.Name()}).Set(stats.Total_chunks) + m.TotalCsize.With(prometheus.Labels{"repo_name": entry.Name()}).Set(stats.Total_csize) + m.TotalSize.With(prometheus.Labels{"repo_name": entry.Name()}).Set(stats.Total_size) + m.TotalUniqueChunks.With(prometheus.Labels{"repo_name": entry.Name()}).Set(stats.Total_unique_chunks) + m.UniqueCsize.With(prometheus.Labels{"repo_name": entry.Name()}).Set(stats.Unique_csize) + m.UniqueSize.With(prometheus.Labels{"repo_name": entry.Name()}).Set(stats.Unique_size) + } + + log.Printf("> Waiting %v\n", INTERVAL) + time.Sleep(INTERVAL) + } +} diff --git a/metrics.go b/metrics.go new file mode 100644 index 0000000..c82da12 --- /dev/null +++ b/metrics.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +type Metrics struct { + ArchiveCount prometheus.GaugeVec + LastArchiveTime prometheus.GaugeVec + LastModified prometheus.GaugeVec + TotalChunks prometheus.GaugeVec + TotalCsize prometheus.GaugeVec + TotalSize prometheus.GaugeVec + TotalUniqueChunks prometheus.GaugeVec + UniqueCsize prometheus.GaugeVec + UniqueSize prometheus.GaugeVec +} + +func NewMetrics(reg prometheus.Registerer) *Metrics { + metrics := &Metrics{ + ArchiveCount: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "archive_count", Help: "Number of archive for this repository"}, []string{"repo_name"}), + LastArchiveTime: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "last_archive", Help: "UNIX timestamp of the last archive"}, []string{"repo_name"}), + LastModified: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "last_modified", Help: "Last modified UNIX timestamp"}, []string{"repo_name"}), + TotalChunks: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "total_chunks", Help: "Number of chunks"}, []string{"repo_name"}), + TotalCsize: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "total_csize", Help: "Total compressed and encrypted size of all chunks"}, []string{"repo_name"}), + TotalSize: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "total_size", Help: "Total uncompressed size of all chunks"}, []string{"repo_name"}), + TotalUniqueChunks: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "total_unique_chunks", Help: "Number of unique chunks"}, []string{"repo_name"}), + UniqueCsize: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "unique_csize", Help: "Compressed and encrypted size of all chunks"}, []string{"repo_name"}), + UniqueSize: *prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "borg", Subsystem: "repository", Name: "unique_size", Help: "Uncompressed size of all chunks"}, []string{"repo_name"}), + } + + reg.MustRegister(metrics.ArchiveCount) + reg.MustRegister(metrics.LastArchiveTime) + reg.MustRegister(metrics.LastModified) + reg.MustRegister(metrics.TotalChunks) + reg.MustRegister(metrics.TotalCsize) + reg.MustRegister(metrics.TotalSize) + reg.MustRegister(metrics.TotalUniqueChunks) + reg.MustRegister(metrics.UniqueCsize) + reg.MustRegister(metrics.UniqueSize) + + return metrics +}