-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This parses the transaction files and generates two metrics.
- Loading branch information
Showing
11 changed files
with
383 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
name: Build and Test | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
|
||
- name: Set up Go | ||
uses: actions/setup-go@v4 | ||
with: | ||
go-version: 1.23 | ||
|
||
- name: Install dependencies | ||
run: go mod tidy | ||
|
||
- name: Build | ||
run: make build | ||
|
||
- name: Test | ||
run: make test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1 @@ | ||
# Generated by Cargo | ||
# will have compiled files and executables | ||
debug/ | ||
target/ | ||
|
||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | ||
Cargo.lock | ||
|
||
# These are backup files generated by rustfmt | ||
**/*.rs.bk | ||
|
||
# MSVC Windows builds of rustc generate these, which store debugging information | ||
*.pdb | ||
|
||
# RustRover | ||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can | ||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore | ||
# and can be added to the global gitignore or merged into this file. For a more nuclear | ||
# option (not recommended) you can uncomment the following to ignore the entire idea folder. | ||
#.idea/ | ||
borgbackuptransactions_exporter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# Contributing | ||
|
||
We welcome contributions! Please follow these guidelines: | ||
1. Fork the repository and create a feature branch. | ||
2. Submit a pull request with a clear description of your changes. | ||
3. Ensure all tests pass before submitting. | ||
|
||
**Do not add features which require extra credentials on borg, they will be rejected.** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
build: | ||
go build -o borgbackuptransactions_exporter . | ||
|
||
test: | ||
go test ./... | ||
|
||
clean: | ||
rm -f borgbackuptransactions_exporter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Borg transactions prometheus exporter | ||
|
||
A Prometheus exporter for monitoring BorgBackup repositories without their passphrases. | ||
This reports the transaction happening on monitored repos, as this can be parsed without the passphrase. | ||
|
||
This is appropriate for monitoring on a server in which you do not want to store credentials. | ||
You should add other monitoring tools to check consistency with other exporters. | ||
|
||
## Features | ||
- Metrics for the last transaction timestamp and number for each repository. | ||
- Configurable via a JSON file. | ||
|
||
## Usage | ||
|
||
1. Create a `config.json` file: | ||
```json | ||
{ | ||
"repos": ["/path/to/repo1", "/path/to/repo2"], | ||
"ip": "0.0.0.0", | ||
"port": 8080, | ||
"endpoint": "/metrics" | ||
} | ||
``` | ||
2. Run the exporter: | ||
```bash | ||
./borgbackuptransactions_exporter --config=config.json | ||
``` | ||
|
||
### Metrics exposed | ||
- `borgbackup_last_transaction_timestamp` | ||
- `borgbackup_last_transaction_number` | ||
|
||
### How to scrape | ||
|
||
``` | ||
scrape_configs: | ||
- job_name: "borgbackuptransactions_exporter" | ||
static_configs: | ||
- targets: ["localhost:8080"] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"repos": [ | ||
"testdata" | ||
], | ||
"ip": "127.0.0.1", | ||
"port": 9999 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
module tlaas.be/borg-backup-transactions-exporter | ||
|
||
go 1.23.3 | ||
|
||
require github.com/prometheus/client_golang v1.20.5 | ||
|
||
require ( | ||
github.com/beorn7/perks v1.0.1 // indirect | ||
github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||
github.com/klauspost/compress v1.17.9 // indirect | ||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | ||
github.com/prometheus/client_model v0.6.1 // indirect | ||
github.com/prometheus/common v0.55.0 // indirect | ||
github.com/prometheus/procfs v0.15.1 // indirect | ||
golang.org/x/sys v0.22.0 // indirect | ||
google.golang.org/protobuf v1.34.2 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | ||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||
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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= | ||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= | ||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= | ||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= | ||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= | ||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= | ||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= | ||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= | ||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= | ||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | ||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= | ||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= | ||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | ||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | ||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= | ||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= | ||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
package main | ||
|
||
import ( | ||
"bufio" | ||
"context" | ||
"encoding/json" | ||
"flag" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"os" | ||
"os/signal" | ||
"path/filepath" | ||
"strconv" | ||
"strings" | ||
"sync" | ||
"syscall" | ||
"time" | ||
|
||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/prometheus/client_golang/prometheus/promhttp" | ||
) | ||
|
||
type Config struct { | ||
Repos []string `json:"repos"` | ||
IP string `json:"ip"` | ||
Port int `json:"port"` | ||
Endpoint string `json:"endpoint"` | ||
TickerInterval int `json:"ticker_interval"` | ||
} | ||
|
||
const ( | ||
defaultIP = "0.0.0.0" | ||
defaultPort = 8080 | ||
defaultEndpoint = "/metrics" | ||
defaultTickerInterval = 60 // in seconds | ||
) | ||
|
||
var ( | ||
lastTransactionTimestamp = prometheus.NewGaugeVec( | ||
prometheus.GaugeOpts{ | ||
Name: "borgbackup_last_transaction_timestamp", | ||
Help: "Unix timestamp of the last transaction in the BorgBackup repository", | ||
}, | ||
[]string{"repo"}, | ||
) | ||
lastTransactionNumber = prometheus.NewGaugeVec( | ||
prometheus.GaugeOpts{ | ||
Name: "borgbackup_last_transaction_number", | ||
Help: "Number of the last transaction in the BorgBackup repository", | ||
}, | ||
[]string{"repo"}, | ||
) | ||
) | ||
|
||
func init() { | ||
prometheus.MustRegister(lastTransactionTimestamp) | ||
prometheus.MustRegister(lastTransactionNumber) | ||
} | ||
|
||
func main() { | ||
configPath := flag.String("config", "config.json", "Path to the configuration file") | ||
flag.Parse() | ||
|
||
config, err := loadConfig(*configPath) | ||
if err != nil { | ||
log.Fatalf("Failed to load configuration: %v", err) | ||
} | ||
|
||
applyDefaults(config) | ||
|
||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
sigChan := make(chan os.Signal, 1) | ||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) | ||
|
||
var wg sync.WaitGroup | ||
|
||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
updateMetricsLoop(ctx, config.Repos, time.Duration(config.TickerInterval)*time.Second) | ||
}() | ||
|
||
serverAddr := fmt.Sprintf("%s:%d", config.IP, config.Port) | ||
http.Handle(config.Endpoint, promhttp.Handler()) | ||
server := &http.Server{Addr: serverAddr} | ||
|
||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
log.Printf("Starting Prometheus exporter on %s%s\n", serverAddr, config.Endpoint) | ||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||
log.Printf("HTTP server error: %v", err) | ||
} | ||
}() | ||
|
||
<-sigChan | ||
log.Println("Received termination signal. Shutting down...") | ||
|
||
cancel() | ||
|
||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer shutdownCancel() | ||
if err := server.Shutdown(shutdownCtx); err != nil { | ||
log.Printf("Error shutting down server: %v", err) | ||
} | ||
|
||
wg.Wait() | ||
log.Println("Exporter stopped.") | ||
} | ||
|
||
func loadConfig(filename string) (*Config, error) { | ||
file, err := os.Open(filename) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer file.Close() | ||
|
||
var config Config | ||
decoder := json.NewDecoder(file) | ||
if err := decoder.Decode(&config); err != nil { | ||
return nil, err | ||
} | ||
|
||
return &config, nil | ||
} | ||
|
||
func applyDefaults(config *Config) { | ||
if config.IP == "" { | ||
config.IP = defaultIP | ||
} | ||
if config.Port == 0 { | ||
config.Port = defaultPort | ||
} | ||
if config.Endpoint == "" { | ||
config.Endpoint = defaultEndpoint | ||
} | ||
if config.TickerInterval == 0 { | ||
config.TickerInterval = defaultTickerInterval | ||
} | ||
} | ||
|
||
func updateMetricsLoop(ctx context.Context, repos []string, interval time.Duration) { | ||
ticker := time.NewTicker(interval) | ||
defer ticker.Stop() | ||
|
||
// Perform the first update immediately | ||
for _, repo := range repos { | ||
updateRepoMetrics(repo) | ||
} | ||
|
||
for { | ||
select { | ||
case <-ctx.Done(): | ||
log.Println("Stopping metrics update loop.") | ||
return | ||
case <-ticker.C: | ||
for _, repo := range repos { | ||
updateRepoMetrics(repo) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func updateRepoMetrics(repo string) { | ||
transactionsFile := filepath.Join(repo, "transactions") | ||
file, err := os.Open(transactionsFile) | ||
if err != nil { | ||
log.Printf("Failed to open transactions file for repo %s: %v", repo, err) | ||
return | ||
} | ||
defer file.Close() | ||
|
||
var lastLine string | ||
scanner := bufio.NewScanner(file) | ||
for scanner.Scan() { | ||
lastLine = scanner.Text() | ||
} | ||
|
||
if err := scanner.Err(); err != nil { | ||
log.Printf("Error reading transactions file for repo %s: %v", repo, err) | ||
return | ||
} | ||
|
||
// instead of deferring as usual, close as soon as the | ||
file.Close() | ||
|
||
transactionNumber, timestamp, err := parseTransactionLine(lastLine) | ||
if err != nil { | ||
log.Printf("Failed to parse transactions file for repo %s: %v", repo, err) | ||
return | ||
} | ||
|
||
lastTransactionTimestamp.WithLabelValues(repo).Set(float64(timestamp)) | ||
lastTransactionNumber.WithLabelValues(repo).Set(float64(transactionNumber)) | ||
} | ||
|
||
func parseTransactionLine(line string) (int, int64, error) { | ||
parts := strings.Split(line, ",") | ||
if len(parts) < 2 { | ||
return 0, 0, fmt.Errorf("invalid line format: %s", line) | ||
} | ||
|
||
numberStr := strings.TrimPrefix(parts[0], "transaction ") | ||
transactionNumber, err := strconv.Atoi(strings.TrimSpace(numberStr)) | ||
if err != nil { | ||
return 0, 0, fmt.Errorf("failed to parse transaction number: %v", err) | ||
} | ||
|
||
timeStr := strings.TrimSpace(strings.Replace(parts[1], "UTC time", "", -1)) | ||
t, err := time.Parse("2006-01-02T15:04:05.000000", timeStr) | ||
if err != nil { | ||
return 0, 0, fmt.Errorf("failed to parse UTC time: %v", err) | ||
} | ||
|
||
return transactionNumber, t.Unix(), nil | ||
} |
Oops, something went wrong.