Initial commit
This commit is contained in:
commit
5e162b4be5
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
main
|
24
go.mod
Normal file
24
go.mod
Normal file
@ -0,0 +1,24 @@
|
||||
module polynom.me/polynom.me/page-metrics
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require (
|
||||
github.com/go-co-op/gocron/v2 v2.2.4
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
52
go.sum
Normal file
52
go.sum
Normal file
@ -0,0 +1,52 @@
|
||||
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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-co-op/gocron/v2 v2.2.4 h1:fL6a8/U+BJQ9UbaeqKxua8wY02w4ftKZsxPzLSNOCKk=
|
||||
github.com/go-co-op/gocron/v2 v2.2.4/go.mod h1:igssOwzZkfcnu3m2kwnCf/mYj4SmhP9ecSgmYjCOHkk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
208
main.go
Normal file
208
main.go
Normal file
@ -0,0 +1,208 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type DomainState struct {
|
||||
// Hits per page (file)
|
||||
Pages map[string]int
|
||||
PagesMetrics map[string]prometheus.Gauge
|
||||
|
||||
// Referrals
|
||||
Referrals map[string]int
|
||||
ReferralsMetrics map[string]prometheus.Gauge
|
||||
}
|
||||
|
||||
type State struct {
|
||||
// Maps domain names to the sliding window
|
||||
Domains map[string]DomainState
|
||||
|
||||
// Lock guarding the access to Domains
|
||||
Lock sync.Mutex
|
||||
}
|
||||
|
||||
func increment(key string, dict *map[string]int) {
|
||||
v, found := (*dict)[key]
|
||||
if !found {
|
||||
(*dict)[key] = 1
|
||||
} else {
|
||||
(*dict)[key] = v + 1
|
||||
}
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
Domain string `json:"domain"`
|
||||
Referer string `json:"referer"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func ingestHandler(registry *prometheus.Registry, state *State) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
log.Debugf("Unexpected %s request", req.Method)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to read body: %v", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var track Track
|
||||
err = json.Unmarshal(body, &track)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to unmarshal request body: %v", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Acquire lock
|
||||
state.Lock.Lock()
|
||||
defer state.Lock.Unlock()
|
||||
|
||||
// Update everything
|
||||
log.Debugf("%v", state.Domains)
|
||||
log.Debugf("Got Domain: %s", track.Domain)
|
||||
domainState, found := state.Domains[track.Domain]
|
||||
if !found {
|
||||
log.Debugf("Domain %s not found. Creating state", track.Domain)
|
||||
domainState = DomainState{
|
||||
Pages: make(map[string]int),
|
||||
PagesMetrics: make(map[string]prometheus.Gauge),
|
||||
Referrals: make(map[string]int),
|
||||
ReferralsMetrics: make(map[string]prometheus.Gauge),
|
||||
}
|
||||
state.Domains[track.Domain] = domainState
|
||||
}
|
||||
|
||||
// Record the referrer
|
||||
referrer := track.Referer
|
||||
if referrer != "" {
|
||||
v, found := domainState.Referrals[referrer]
|
||||
if found {
|
||||
value := v + 1
|
||||
domainState.Referrals[referrer] = value
|
||||
|
||||
metric, _ := domainState.ReferralsMetrics[referrer]
|
||||
metric.Set(float64(value))
|
||||
} else {
|
||||
metric := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "referals",
|
||||
ConstLabels: prometheus.Labels{"domain": track.Domain, "referer": referrer},
|
||||
},
|
||||
)
|
||||
metric.Set(1)
|
||||
domainState.Referrals[referrer] = 1
|
||||
domainState.ReferralsMetrics[referrer] = metric
|
||||
log.Debugf("Registering gauge \"referals\" with referer=%s", referrer)
|
||||
registry.MustRegister(metric)
|
||||
}
|
||||
}
|
||||
|
||||
// Record the file
|
||||
path := track.Path
|
||||
v, found := domainState.Pages[path]
|
||||
log.Debugf("v=%d, found=%v", v, found)
|
||||
if found {
|
||||
value := v + 1
|
||||
domainState.Pages[path] = value
|
||||
|
||||
metric, _ := domainState.PagesMetrics[path]
|
||||
metric.Set(float64(value))
|
||||
} else {
|
||||
metric := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pages",
|
||||
ConstLabels: prometheus.Labels{"domain": track.Domain, "path": path},
|
||||
},
|
||||
)
|
||||
metric.Set(1)
|
||||
domainState.Pages[path] = 1
|
||||
domainState.PagesMetrics[path] = metric
|
||||
log.Debugf("Registering gauge \"path\" with path=%s", path)
|
||||
registry.MustRegister(metric)
|
||||
}
|
||||
state.Domains[track.Domain] = domainState
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
// Setup metrics
|
||||
registry := prometheus.NewRegistry()
|
||||
state := State{
|
||||
Domains: make(map[string]DomainState),
|
||||
}
|
||||
|
||||
// Setup the scheduler
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create scheduler: %v", err)
|
||||
return
|
||||
}
|
||||
_, err = scheduler.NewJob(
|
||||
gocron.DailyJob(1, gocron.NewAtTimes(gocron.NewAtTime(0, 0, 0))),
|
||||
gocron.NewTask(
|
||||
func() {
|
||||
// Acquire the lock
|
||||
log.Info("Starting reset procedure...")
|
||||
state.Lock.Lock()
|
||||
defer state.Lock.Unlock()
|
||||
|
||||
for domainName, domain := range state.Domains {
|
||||
log.Infof("Resetting counters for %s", domainName)
|
||||
|
||||
// Reset pages
|
||||
for page := range domain.Pages {
|
||||
domain.Pages[page] = 0
|
||||
domain.PagesMetrics[page].Set(0)
|
||||
}
|
||||
|
||||
// Reset referals
|
||||
for referer := range domain.Referrals {
|
||||
domain.Referrals[referer] = 0
|
||||
domain.ReferralsMetrics[referer].Set(0)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create task: %v", err)
|
||||
return
|
||||
}
|
||||
scheduler.Start()
|
||||
|
||||
// Set up the HTTP server
|
||||
http.Handle(
|
||||
"/metrics", promhttp.HandlerFor(
|
||||
registry,
|
||||
promhttp.HandlerOpts{
|
||||
EnableOpenMetrics: true,
|
||||
},
|
||||
),
|
||||
)
|
||||
http.Handle(
|
||||
"/track", ingestHandler(registry, &state),
|
||||
)
|
||||
addr := "127.0.0.1:9999"
|
||||
log.Infof("Starting server at %s", addr)
|
||||
http.ListenAndServe(addr, nil)
|
||||
}
|
Loading…
Reference in New Issue
Block a user