Skip to content

Commit

Permalink
First implementation
Browse files Browse the repository at this point in the history
This parses the transaction files and generates two metrics.
  • Loading branch information
evrardjp committed Nov 30, 2024
1 parent 26512f0 commit 36dcfe5
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 21 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/build.yml
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
22 changes: 1 addition & 21 deletions .gitignore
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
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
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.**
8 changes: 8 additions & 0 deletions Makefile
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
40 changes: 40 additions & 0 deletions README
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"]
```
7 changes: 7 additions & 0 deletions example_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"repos": [
"testdata"
],
"ip": "127.0.0.1",
"port": 9999
}
17 changes: 17 additions & 0 deletions go.mod
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
)
24 changes: 24 additions & 0 deletions go.sum
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=
219 changes: 219 additions & 0 deletions main.go
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
}
Loading

0 comments on commit 36dcfe5

Please sign in to comment.