Initial commit
This commit is contained in:
commit
a78752b496
9 changed files with 319 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
prometheus-borg-exporter
|
||||||
|
test
|
14
Makefile
Normal file
14
Makefile
Normal 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
39
README.md
Normal 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
15
go.mod
Normal 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
20
go.sum
Normal 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
50
info.go
Normal 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
44
list.go
Normal 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
92
main.go
Normal 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
43
metrics.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue