Added persistent stats file

This commit is contained in:
Seednode 2023-02-08 07:50:40 -06:00
parent 9087cf6b4d
commit 537633a030
4 changed files with 157 additions and 30 deletions

View File

@ -318,7 +318,6 @@ func splitPath(path string, Regexes *Regexes) (*Path, error) {
p.base = split[0][1] p.base = split[0][1]
p.number, err = strconv.Atoi(split[0][2]) p.number, err = strconv.Atoi(split[0][2])
if err != nil { if err != nil {
return &Path{}, err return &Path{}, err
} }

View File

@ -12,14 +12,15 @@ import (
) )
var ( var (
cache bool cache bool
cacheFile string cacheFile string
filtering bool filtering bool
port uint16 port uint16
recursive bool recursive bool
sorting bool sorting bool
statistics bool statistics bool
verbose bool statisticsFile string
verbose bool
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "roulette <path> [path]...", Use: "roulette <path> [path]...",
@ -52,6 +53,7 @@ func init() {
rootCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "recurse into subdirectories") rootCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "recurse into subdirectories")
rootCmd.Flags().BoolVarP(&sorting, "sort", "s", false, "enable sorting") rootCmd.Flags().BoolVarP(&sorting, "sort", "s", false, "enable sorting")
rootCmd.Flags().BoolVar(&statistics, "stats", false, "expose stats endpoint") rootCmd.Flags().BoolVar(&statistics, "stats", false, "expose stats endpoint")
rootCmd.Flags().StringVar(&statisticsFile, "stats-file", "", "path to optional persistent stats file")
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "log accessed files to stdout") rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "log accessed files to stdout")
rootCmd.Flags().SetInterspersed(true) rootCmd.Flags().SetInterspersed(true)
} }

View File

@ -10,7 +10,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var Version = "0.36.2" var Version = "0.37"
func init() { func init() {
rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(versionCmd)

View File

@ -10,10 +10,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
@ -21,6 +23,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
@ -122,16 +125,18 @@ func (i *Index) Export(path string) error {
enc := gob.NewEncoder(z) enc := gob.NewEncoder(z)
i.mutex.RLock()
enc.Encode(&i.list) enc.Encode(&i.list)
i.mutex.RUnlock()
return nil return nil
} }
func (i *Index) Import(path string) error { func (i *Index) Import(path string) error {
file, err := os.OpenFile(path, os.O_RDONLY, 0600) file, err := os.OpenFile(path, os.O_RDONLY, 0600)
if os.IsNotExist(err) { if err != nil {
return ErrIndexNotExist
} else if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
@ -144,7 +149,12 @@ func (i *Index) Import(path string) error {
dec := gob.NewDecoder(z) dec := gob.NewDecoder(z)
i.mutex.Lock()
err = dec.Decode(&i.list) err = dec.Decode(&i.list)
i.mutex.Unlock()
if err != nil { if err != nil {
return err return err
} }
@ -160,6 +170,13 @@ type ServeStats struct {
times map[string][]string times map[string][]string
} }
type exportedServeStats struct {
List []string
Count map[string]uint64
Size map[string]string
Times map[string][]string
}
func (s *ServeStats) incrementCounter(image string, timestamp time.Time, filesize string) { func (s *ServeStats) incrementCounter(image string, timestamp time.Time, filesize string) {
s.mutex.Lock() s.mutex.Lock()
@ -179,27 +196,66 @@ func (s *ServeStats) incrementCounter(image string, timestamp time.Time, filesiz
s.mutex.Unlock() s.mutex.Unlock()
} }
func (s *ServeStats) ListImages() ([]byte, error) { func (s *ServeStats) toExported() *exportedServeStats {
stats := &exportedServeStats{
List: []string{},
Count: make(map[string]uint64),
Size: make(map[string]string),
Times: make(map[string][]string),
}
s.mutex.RLock() s.mutex.RLock()
sortedList := &ServeStats{ stats.List = append(stats.List, s.list...)
mutex: sync.RWMutex{},
list: s.list, for k, v := range s.count {
count: s.count, stats.Count[k] = v
size: s.size, }
times: s.times,
for k, v := range s.size {
stats.Size[k] = v
}
for k, v := range s.times {
stats.Times[k] = v
} }
s.mutex.RUnlock() s.mutex.RUnlock()
sort.SliceStable(sortedList.list, func(p, q int) bool { return stats
return sortedList.list[p] < sortedList.list[q] }
func (s *ServeStats) toImported(stats *exportedServeStats) {
s.mutex.Lock()
s.list = append(s.list, stats.List...)
for k, v := range stats.Count {
s.count[k] = v
}
for k, v := range stats.Size {
s.size[k] = v
}
for k, v := range stats.Times {
s.times[k] = v
}
s.mutex.Unlock()
}
func (s *ServeStats) ListImages() ([]byte, error) {
stats := s.toExported()
sort.SliceStable(stats.List, func(p, q int) bool {
return stats.List[p] < stats.List[q]
}) })
a := []timesServed{} a := []timesServed{}
for _, image := range sortedList.list { for _, image := range stats.List {
a = append(a, timesServed{image, sortedList.count[image], sortedList.size[image], sortedList.times[image]}) a = append(a, timesServed{image, stats.Count[image], stats.Size[image], stats.Times[image]})
} }
r, err := json.MarshalIndent(a, "", " ") r, err := json.MarshalIndent(a, "", " ")
@ -210,6 +266,63 @@ func (s *ServeStats) ListImages() ([]byte, error) {
return r, nil return r, nil
} }
func (s *ServeStats) Export(path string) error {
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer file.Close()
z, err := zstd.NewWriter(file)
if err != nil {
return err
}
defer z.Close()
enc := gob.NewEncoder(z)
stats := s.toExported()
err = enc.Encode(&stats)
if err != nil {
log.Fatal(err)
}
return nil
}
func (s *ServeStats) Import(path string) error {
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
if err != nil {
return err
}
defer file.Close()
z, err := zstd.NewReader(file)
if err != nil {
return err
}
defer z.Close()
dec := gob.NewDecoder(z)
stats := &exportedServeStats{
List: []string{},
Count: make(map[string]uint64),
Size: make(map[string]string),
Times: make(map[string][]string),
}
err = dec.Decode(stats)
if err != nil {
return err
}
s.toImported(stats)
return nil
}
type timesServed struct { type timesServed struct {
File string File string
Served uint64 Served uint64
@ -522,6 +635,10 @@ func serveStatsHandler(args []string, stats *ServeStats) http.HandlerFunc {
time.Since(startTime).Round(time.Microsecond), time.Since(startTime).Round(time.Microsecond),
) )
} }
if statisticsFile != "" {
stats.Export(statisticsFile)
}
} }
} }
@ -655,13 +772,9 @@ func ServePage(args []string) error {
skipIndex := false skipIndex := false
if cacheFile != "" { if cacheFile != "" {
err = index.Import(cacheFile) err := index.Import(cacheFile)
switch err { if err == nil {
case ErrIndexNotExist:
case nil:
skipIndex = true skipIndex = true
default:
return err
} }
} }
@ -680,6 +793,19 @@ func ServePage(args []string) error {
times: make(map[string][]string), times: make(map[string][]string),
} }
if statistics && statisticsFile != "" {
stats.Import(statisticsFile)
gracefulShutdown := make(chan os.Signal, 1)
signal.Notify(gracefulShutdown, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-gracefulShutdown
stats.Export(statisticsFile)
os.Exit(0)
}()
}
http.Handle("/", serveHtmlHandler(paths, Regexes, index)) http.Handle("/", serveHtmlHandler(paths, Regexes, index))
http.Handle(Prefix+"/", http.StripPrefix(Prefix, serveStaticFileHandler(paths, stats))) http.Handle(Prefix+"/", http.StripPrefix(Prefix, serveStaticFileHandler(paths, stats)))
http.HandleFunc("/favicon.ico", doNothing) http.HandleFunc("/favicon.ico", doNothing)