From a78752b4967a00fe84725544d6f9881c0db96ab3 Mon Sep 17 00:00:00 2001 From: Valentin Doreau Date: Mon, 1 Apr 2024 17:02:29 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 ++ Makefile | 14 +++++++++ README.md | 39 +++++++++++++++++++++++ go.mod | 15 +++++++++ go.sum | 20 ++++++++++++ info.go | 50 +++++++++++++++++++++++++++++ list.go | 44 ++++++++++++++++++++++++++ main.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ metrics.go | 43 +++++++++++++++++++++++++ 9 files changed, 319 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 info.go create mode 100644 list.go create mode 100644 main.go create mode 100644 metrics.go 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 +}