Initial commit

This commit is contained in:
Valentin Doreau 2024-04-01 17:02:29 +02:00
commit a78752b496
Signed by: vdoreau
GPG key ID: F3E456CF9A14098B
9 changed files with 319 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
prometheus-borg-exporter
test

14
Makefile Normal file
View file

@ -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)

39
README.md Normal file
View file

@ -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)

15
go.mod Normal file
View file

@ -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
)

20
go.sum Normal file
View file

@ -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=

50
info.go Normal file
View file

@ -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
}

44
list.go Normal file
View file

@ -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
}

92
main.go Normal file
View file

@ -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)
}
}

43
metrics.go Normal file
View file

@ -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
}