Skip to content

Commit 9bbaf27

Browse files
committed
Prometheus metric for repository's last update
A new gauge metric `rest_server_repo_last_update_timestamp` was added to monitor each repository's last write access. This allows a basic monitoring for each repository's freshness. In order to have this metric available at startup, a basic preloading for Prometheus metrics has been implemented. This operates by scanning the file system for restic repositories and using their last modified time. Subsequently, each write access updates the last update time. If scanning each repository takes too long, it can be disabled through the `--prometheus-no-preload` flag. This might be related to the feature request in #176.
1 parent 2dd87ce commit 9bbaf27

File tree

6 files changed

+194
-40
lines changed

6 files changed

+194
-40
lines changed

README.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,25 @@ Usage:
3232
rest-server [flags]
3333

3434
Flags:
35-
--append-only enable append only mode
36-
--cpu-profile string write CPU profile to file
37-
--debug output debug messages
38-
-h, --help help for rest-server
39-
--htpasswd-file string location of .htpasswd file (default: "<data directory>/.htpasswd")
40-
--listen string listen address (default ":8000")
41-
--log filename write HTTP requests in the combined log format to the specified filename
42-
--max-size int the maximum size of the repository in bytes
43-
--no-auth disable .htpasswd authentication
44-
--no-verify-upload do not verify the integrity of uploaded data. DO NOT enable unless the rest-server runs on a very low-power device
45-
--path string data directory (default "/tmp/restic")
46-
--private-repos users can only access their private repo
47-
--prometheus enable Prometheus metrics
48-
--prometheus-no-auth disable auth for Prometheus /metrics endpoint
49-
--tls turn on TLS support
50-
--tls-cert string TLS certificate path
51-
--tls-key string TLS key path
52-
-v, --version version for rest-server
35+
--append-only enable append only mode
36+
--cpu-profile string write CPU profile to file
37+
--debug output debug messages
38+
-h, --help help for rest-server
39+
--htpasswd-file string location of .htpasswd file (default: "<data directory>/.htpasswd)"
40+
--listen string listen address (default ":8000")
41+
--log filename write HTTP requests in the combined log format to the specified filename
42+
--max-size int the maximum size of the repository in bytes
43+
--no-auth disable .htpasswd authentication
44+
--no-verify-upload do not verify the integrity of uploaded data. DO NOT enable unless the rest-server runs on a very low-power device
45+
--path string data directory (default "/tmp/restic")
46+
--private-repos users can only access their private repo
47+
--prometheus enable Prometheus metrics
48+
--prometheus-no-auth disable auth for Prometheus /metrics endpoint
49+
--prometheus-no-preload disable preloading Prometheus metrics during startup
50+
--tls turn on TLS support
51+
--tls-cert string TLS certificate path
52+
--tls-key string TLS key path
53+
-v, --version version for rest-server
5354
```
5455

5556
By default the server persists backup data in the OS temporary directory (`/tmp/restic` on Linux/BSD and others, in `%TEMP%\\restic` in Windows, etc). **If `rest-server` is launched using the default path, all backups will be lost**. To start the server with a custom persistence directory and with authentication disabled:

changelog/unreleased/pull-197

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Enhancement: Prometheus metric for repository's last update
2+
3+
A new gauge metric `rest_server_repo_last_update_timestamp` was added to
4+
monitor each repository's last write access. This allows a basic
5+
monitoring for each repository's freshness.
6+
7+
This metric can be configured as an alerting rule. For example, to be
8+
notified if some repository is older than two days:
9+
> time() - rest_server_repo_last_update_timestamp >= 172800
10+
11+
In order to have this metric available at startup, a basic preloading for
12+
Prometheus metrics has been implemented. This operates by scanning the file
13+
system for restic repositories and using their last modified time.
14+
Subsequently, each write access updates the last update time.
15+
16+
If scanning each repository takes too long, it can be disabled through the
17+
`--prometheus-no-preload` flag.

cmd/rest-server/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func init() {
5454
flags.BoolVar(&server.PrivateRepos, "private-repos", server.PrivateRepos, "users can only access their private repo")
5555
flags.BoolVar(&server.Prometheus, "prometheus", server.Prometheus, "enable Prometheus metrics")
5656
flags.BoolVar(&server.PrometheusNoAuth, "prometheus-no-auth", server.PrometheusNoAuth, "disable auth for Prometheus /metrics endpoint")
57+
flags.BoolVar(&server.PrometheusNoPreload, "prometheus-no-preload", server.PrometheusNoPreload, "disable preloading Prometheus metrics during startup")
5758
}
5859

5960
var version = "0.11.0"
@@ -126,6 +127,12 @@ func runRoot(cmd *cobra.Command, args []string) error {
126127
log.Println("Private repositories disabled")
127128
}
128129

130+
if server.Prometheus && !server.PrometheusNoPreload {
131+
if err := server.PreloadMetrics(); err != nil {
132+
return fmt.Errorf("unable to preload metrics: %w", err)
133+
}
134+
}
135+
129136
enabledTLS, privateKey, publicKey, err := tlsSettings()
130137
if err != nil {
131138
return err

handlers.go

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package restserver
22

33
import (
44
"errors"
5+
"fmt"
6+
"io/fs"
57
"log"
68
"net/http"
9+
"os"
710
"path"
811
"path/filepath"
912
"strings"
@@ -14,23 +17,24 @@ import (
1417

1518
// Server encapsulates the rest-server's settings and repo management logic
1619
type Server struct {
17-
Path string
18-
HtpasswdPath string
19-
Listen string
20-
Log string
21-
CPUProfile string
22-
TLSKey string
23-
TLSCert string
24-
TLS bool
25-
NoAuth bool
26-
AppendOnly bool
27-
PrivateRepos bool
28-
Prometheus bool
29-
PrometheusNoAuth bool
30-
Debug bool
31-
MaxRepoSize int64
32-
PanicOnError bool
33-
NoVerifyUpload bool
20+
Path string
21+
HtpasswdPath string
22+
Listen string
23+
Log string
24+
CPUProfile string
25+
TLSKey string
26+
TLSCert string
27+
TLS bool
28+
NoAuth bool
29+
AppendOnly bool
30+
PrivateRepos bool
31+
Prometheus bool
32+
PrometheusNoAuth bool
33+
PrometheusNoPreload bool
34+
Debug bool
35+
MaxRepoSize int64
36+
PanicOnError bool
37+
NoVerifyUpload bool
3438

3539
htpasswdFile *HtpasswdFile
3640
quotaManager *quota.Manager
@@ -46,6 +50,98 @@ func httpDefaultError(w http.ResponseWriter, code int) {
4650
http.Error(w, http.StatusText(code), code)
4751
}
4852

53+
// PreloadMetrics for Prometheus for each available repository.
54+
func (s *Server) PreloadMetrics() error {
55+
// No need to preload metrics if those are disabled.
56+
if !s.Prometheus || s.PrometheusNoPreload {
57+
return nil
58+
}
59+
60+
if _, statErr := os.Lstat(s.Path); errors.Is(statErr, os.ErrNotExist) {
61+
log.Print("PreloadMetrics: skipping preloading as repo does not exists yet")
62+
return nil
63+
}
64+
65+
var repoPaths []string
66+
67+
walkFunc := func(path string, d fs.DirEntry, err error) error {
68+
if err != nil {
69+
return err
70+
}
71+
72+
if !d.IsDir() {
73+
return nil
74+
}
75+
76+
// Verify that we're in an allowed directory.
77+
for _, objectType := range repo.ObjectTypes {
78+
if d.Name() == objectType {
79+
return filepath.SkipDir
80+
}
81+
}
82+
83+
// Verify that we're also a valid repository.
84+
for _, objectType := range repo.ObjectTypes {
85+
stat, statErr := os.Lstat(filepath.Join(path, objectType))
86+
if errors.Is(statErr, os.ErrNotExist) || !stat.IsDir() {
87+
if s.Debug {
88+
log.Printf("PreloadMetrics: %s misses directory %s; skip", path, objectType)
89+
}
90+
return nil
91+
}
92+
}
93+
for _, fileType := range repo.FileTypes {
94+
stat, statErr := os.Lstat(filepath.Join(path, fileType))
95+
if errors.Is(statErr, os.ErrNotExist) || !stat.Mode().IsRegular() {
96+
if s.Debug {
97+
log.Printf("PreloadMetrics: %s misses file %s; skip", path, fileType)
98+
}
99+
return nil
100+
}
101+
}
102+
103+
if s.Debug {
104+
log.Printf("PreloadMetrics: found repository %s", path)
105+
}
106+
repoPaths = append(repoPaths, path)
107+
return nil
108+
}
109+
110+
if err := filepath.WalkDir(s.Path, walkFunc); err != nil {
111+
return err
112+
}
113+
114+
for _, repoPath := range repoPaths {
115+
// Remove leading path prefix.
116+
relPath := repoPath[len(s.Path):]
117+
if strings.HasPrefix(relPath, string(os.PathSeparator)) {
118+
relPath = relPath[1:]
119+
}
120+
folderPath := strings.Split(relPath, string(os.PathSeparator))
121+
122+
if !folderPathValid(folderPath) {
123+
return fmt.Errorf("invalid foder path %s for preloading",
124+
strings.Join(folderPath, string(os.PathSeparator)))
125+
}
126+
127+
opt := repo.Options{
128+
Debug: s.Debug,
129+
PanicOnError: s.PanicOnError,
130+
BlobMetricFunc: makeBlobMetricFunc("", folderPath),
131+
}
132+
133+
handler, err := repo.New(repoPath, opt)
134+
if err != nil {
135+
return err
136+
}
137+
138+
if err := handler.PreloadMetrics(); err != nil {
139+
return err
140+
}
141+
}
142+
return nil
143+
}
144+
49145
// ServeHTTP makes this server an http.Handler. It handlers the administrative
50146
// part of the request (figuring out the filesystem location, performing
51147
// authentication, etc) and then passes it on to repo.Handler for actual

metrics.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package restserver
22

33
import (
44
"strings"
5+
"time"
56

67
"github.com/prometheus/client_golang/prometheus"
78
"github.com/restic/rest-server/repo"
@@ -57,25 +58,39 @@ var metricBlobDeleteBytesTotal = prometheus.NewCounterVec(
5758
metricLabelList,
5859
)
5960

61+
var metricRepoLastUpdateTimestamp = prometheus.NewGaugeVec(
62+
prometheus.GaugeOpts{
63+
Name: "rest_server_repo_last_update_timestamp",
64+
Help: "Unix timestamp of repository's last write update",
65+
},
66+
[]string{"repo"},
67+
)
68+
6069
// makeBlobMetricFunc creates a metrics callback function that increments the
6170
// Prometheus metrics.
6271
func makeBlobMetricFunc(username string, folderPath []string) repo.BlobMetricFunc {
63-
var f repo.BlobMetricFunc = func(objectType string, operation repo.BlobOperation, nBytes uint64) {
72+
var f repo.BlobMetricFunc = func(objectType string, operation repo.BlobOperation, payload uint64) {
73+
repoPath := strings.Join(folderPath, "/")
6474
labels := prometheus.Labels{
6575
"user": username,
66-
"repo": strings.Join(folderPath, "/"),
76+
"repo": repoPath,
6777
"type": objectType,
6878
}
79+
6980
switch operation {
7081
case repo.BlobRead:
7182
metricBlobReadTotal.With(labels).Inc()
72-
metricBlobReadBytesTotal.With(labels).Add(float64(nBytes))
83+
metricBlobReadBytesTotal.With(labels).Add(float64(payload))
7384
case repo.BlobWrite:
7485
metricBlobWriteTotal.With(labels).Inc()
75-
metricBlobWriteBytesTotal.With(labels).Add(float64(nBytes))
86+
metricBlobWriteBytesTotal.With(labels).Add(float64(payload))
87+
metricRepoLastUpdateTimestamp.WithLabelValues(repoPath).Set(
88+
float64(time.Now().UnixMilli()) / 1000.0)
7689
case repo.BlobDelete:
7790
metricBlobDeleteTotal.With(labels).Inc()
78-
metricBlobDeleteBytesTotal.With(labels).Add(float64(nBytes))
91+
metricBlobDeleteBytesTotal.With(labels).Add(float64(payload))
92+
case repo.RepoPreloadLastUpdate:
93+
metricRepoLastUpdateTimestamp.WithLabelValues(repoPath).Set(float64(payload) / 1000.0)
7994
}
8095
}
8196
return f
@@ -89,4 +104,5 @@ func init() {
89104
prometheus.MustRegister(metricBlobReadBytesTotal)
90105
prometheus.MustRegister(metricBlobDeleteTotal)
91106
prometheus.MustRegister(metricBlobDeleteBytesTotal)
107+
prometheus.MustRegister(metricRepoLastUpdateTimestamp)
92108
}

repo/repo.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ const (
113113
BlobRead = 'R' // A blob has been read
114114
BlobWrite = 'W' // A blob has been written
115115
BlobDelete = 'D' // A blob has been deleted
116+
117+
RepoPreloadLastUpdate = 'U' // Set last update timestamp for preloading
116118
)
117119

118120
// BlobMetricFunc is the callback signature for blob metrics. Such a callback
@@ -123,6 +125,21 @@ const (
123125
// TODO: Perhaps add http.Request for the username so that this can be cached?
124126
type BlobMetricFunc func(objectType string, operation BlobOperation, nBytes uint64)
125127

128+
// PreloadMetrics for Prometheus.
129+
func (h *Handler) PreloadMetrics() error {
130+
if h.opt.Debug {
131+
log.Printf("%v.PreloadMetrics()", h)
132+
}
133+
134+
stat, err := os.Lstat(h.getSubPath("snapshots"))
135+
if err != nil {
136+
return err
137+
}
138+
h.sendMetric("", RepoPreloadLastUpdate, uint64(stat.ModTime().UnixMilli()))
139+
140+
return nil
141+
}
142+
126143
// ServeHTTP performs strict matching on the repo part of the URL path and
127144
// dispatches the request to the appropriate handler.
128145
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)