-
Notifications
You must be signed in to change notification settings - Fork 149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prometheus metric for repository's last update #197
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
Enhancement: 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. | ||
|
||
This metric can be configured as an alerting rule. For example, to be | ||
notified if some repository is older than two days: | ||
> time() - rest_server_repo_last_update_timestamp >= 172800 | ||
|
||
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,11 @@ package restserver | |
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io/fs" | ||
"log" | ||
"net/http" | ||
"os" | ||
"path" | ||
"path/filepath" | ||
"strings" | ||
|
@@ -14,23 +17,24 @@ import ( | |
|
||
// Server encapsulates the rest-server's settings and repo management logic | ||
type Server struct { | ||
Path string | ||
HtpasswdPath string | ||
Listen string | ||
Log string | ||
CPUProfile string | ||
TLSKey string | ||
TLSCert string | ||
TLS bool | ||
NoAuth bool | ||
AppendOnly bool | ||
PrivateRepos bool | ||
Prometheus bool | ||
PrometheusNoAuth bool | ||
Debug bool | ||
MaxRepoSize int64 | ||
PanicOnError bool | ||
NoVerifyUpload bool | ||
Path string | ||
HtpasswdPath string | ||
Listen string | ||
Log string | ||
CPUProfile string | ||
TLSKey string | ||
TLSCert string | ||
TLS bool | ||
NoAuth bool | ||
AppendOnly bool | ||
PrivateRepos bool | ||
Prometheus bool | ||
PrometheusNoAuth bool | ||
PrometheusNoPreload bool | ||
Debug bool | ||
MaxRepoSize int64 | ||
PanicOnError bool | ||
NoVerifyUpload bool | ||
|
||
htpasswdFile *HtpasswdFile | ||
quotaManager *quota.Manager | ||
|
@@ -46,6 +50,98 @@ func httpDefaultError(w http.ResponseWriter, code int) { | |
http.Error(w, http.StatusText(code), code) | ||
} | ||
|
||
// PreloadMetrics for Prometheus for each available repository. | ||
func (s *Server) PreloadMetrics() error { | ||
// No need to preload metrics if those are disabled. | ||
if !s.Prometheus || s.PrometheusNoPreload { | ||
return nil | ||
} | ||
|
||
if _, statErr := os.Lstat(s.Path); errors.Is(statErr, os.ErrNotExist) { | ||
log.Print("PreloadMetrics: skipping preloading as repo does not exists yet") | ||
return nil | ||
} | ||
|
||
var repoPaths []string | ||
|
||
walkFunc := func(path string, d fs.DirEntry, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if !d.IsDir() { | ||
return nil | ||
} | ||
|
||
// Verify that we're in an allowed directory. | ||
for _, objectType := range repo.ObjectTypes { | ||
if d.Name() == objectType { | ||
return filepath.SkipDir | ||
} | ||
} | ||
|
||
// Verify that we're also a valid repository. | ||
for _, objectType := range repo.ObjectTypes { | ||
stat, statErr := os.Lstat(filepath.Join(path, objectType)) | ||
if errors.Is(statErr, os.ErrNotExist) || !stat.IsDir() { | ||
if s.Debug { | ||
log.Printf("PreloadMetrics: %s misses directory %s; skip", path, objectType) | ||
} | ||
return nil | ||
} | ||
} | ||
for _, fileType := range repo.FileTypes { | ||
stat, statErr := os.Lstat(filepath.Join(path, fileType)) | ||
if errors.Is(statErr, os.ErrNotExist) || !stat.Mode().IsRegular() { | ||
if s.Debug { | ||
log.Printf("PreloadMetrics: %s misses file %s; skip", path, fileType) | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
if s.Debug { | ||
log.Printf("PreloadMetrics: found repository %s", path) | ||
} | ||
repoPaths = append(repoPaths, path) | ||
return nil | ||
} | ||
|
||
if err := filepath.WalkDir(s.Path, walkFunc); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that WalkDir will not follow symlinks, while symlinks to repos are accepted by rest-server. An implementation that does follow symlinks risks a loop. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are totally right about this. I wasn't aware that following symlinks was a requirement of the rest-server, as it isn't part of the README. I can exchange |
||
return err | ||
} | ||
|
||
for _, repoPath := range repoPaths { | ||
// Remove leading path prefix. | ||
relPath := repoPath[len(s.Path):] | ||
if strings.HasPrefix(relPath, string(os.PathSeparator)) { | ||
relPath = relPath[1:] | ||
} | ||
folderPath := strings.Split(relPath, string(os.PathSeparator)) | ||
|
||
if !folderPathValid(folderPath) { | ||
return fmt.Errorf("invalid foder path %s for preloading", | ||
strings.Join(folderPath, string(os.PathSeparator))) | ||
} | ||
|
||
opt := repo.Options{ | ||
Debug: s.Debug, | ||
PanicOnError: s.PanicOnError, | ||
BlobMetricFunc: makeBlobMetricFunc("", folderPath), | ||
} | ||
|
||
handler, err := repo.New(repoPath, opt) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := handler.PreloadMetrics(); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// ServeHTTP makes this server an http.Handler. It handlers the administrative | ||
// part of the request (figuring out the filesystem location, performing | ||
// authentication, etc) and then passes it on to repo.Handler for actual | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should really use existing validation functions like
folderPathValid
andsplitURLPath
(note themaxDepth
param) to ensure that the list of repositories returned really matches the repositories that are accessible.Recursing to arbitrary depth could be very slow when data is stored on remote spinning disks and the directories for some reason contain other large directory trees that are not restic repositories.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
folderPathValid
function will be used below in line 122, as both the repository's absolute path as well as the relative folder path are needed. I don't see any advantage in using thesplitURLPath
function directly, especially as bothrepo.ObjectTypes
andrepo.FileTypes
are being checked in thewalkFunc
to determine if some directory path should be followed further.As any occurrence of an
repo.ObjectTypes
like directory aborts this path by returningfilepath.SkipDir
, I don't expect too much directory walking. However, there might be special cases where this might perform poorly. I'm open to suggestions how to address this.