diff --git a/Makefile b/Makefile index fb55df4..11198a4 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,15 @@ BUILDDATE!=date '+%Y-%m-%e' LDFLAGS=-ldflags="-X '${VPKG}.Version=${VER}' -X '${VPKG}.Built=${BUILDDATE}'" all: checkcalls - go build -o gordio ${LDFLAGS} ./cmd/gordio/ - go build -o calls ${LDFLAGS} ./cmd/calls/ + go build -o gordio ${GOFLAGS} ${LDFLAGS} ./cmd/gordio/ + go build -o calls ${GOFLAGS} ${LDFLAGS} ./cmd/calls/ + +buildpprof: + go build -o gordio-pprof ${GOFLAGS} ${LDFLAGS} -tags pprof ./cmd/gordio clean: rm -rf client/calls/ && mkdir client/calls && touch client/calls/.gitkeep - rm -f gordio calls + rm -f gordio calls gordio-pprof checkcalls: @test -e client/calls/index.html || make getcalls diff --git a/go.mod b/go.mod index 4080887..345f628 100644 --- a/go.mod +++ b/go.mod @@ -1,38 +1,39 @@ module dynatron.me/x/stillbox -go 1.22.5 +go 1.23.2 require ( dynatron.me/x/go-minimp3 v0.0.0-20240805171536-7ea857e216d6 + github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/go-audio/wav v1.1.0 - github.com/go-chi/chi v1.5.5 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.9.0 github.com/go-chi/jwtauth/v5 v5.3.1 github.com/go-chi/render v1.0.3 github.com/golang-migrate/migrate/v4 v4.17.1 - github.com/google/uuid v1.4.0 + github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/hajimehoshi/oto v1.0.1 github.com/jackc/pgx/v5 v5.6.0 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 - golang.org/x/crypto v0.21.0 - golang.org/x/sync v0.5.0 - golang.org/x/term v0.18.0 - google.golang.org/protobuf v1.33.0 + golang.org/x/crypto v0.28.0 + golang.org/x/sync v0.8.0 + golang.org/x/term v0.25.0 + google.golang.org/protobuf v1.35.1 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/ajg/form v1.5.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/go-audio/audio v1.0.0 // indirect github.com/go-audio/riff v1.0.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -48,14 +49,16 @@ require ( github.com/lestrrat-go/jwx/v2 v2.0.20 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.9.0 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/exp/shiny v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/image v0.14.0 // indirect golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index aa4890b..f712c84 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,10 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -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/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= +github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= +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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -32,8 +34,6 @@ github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= -github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= -github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= @@ -51,10 +51,10 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4= @@ -97,8 +97,10 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -111,12 +113,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= @@ -128,14 +132,14 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/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= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI= github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp/shiny v0.0.0-20240719175910-8a7402abbf56 h1:8jM66xzUJjNInw31Y8bic4AYSLVChztDRT93+kmofUY= golang.org/x/exp/shiny v0.0.0-20240719175910-8a7402abbf56/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= @@ -145,30 +149,28 @@ golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= -golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/jsontime/jsontime.go b/internal/jsontime/jsontime.go new file mode 100644 index 0000000..359ce41 --- /dev/null +++ b/internal/jsontime/jsontime.go @@ -0,0 +1,107 @@ +package jsontime + +import ( + "encoding/json" + "strings" + "time" + + "github.com/araddon/dateparse" + "gopkg.in/yaml.v3" +) + +type Time time.Time + +func (t *Time) UnmarshalYAML(n *yaml.Node) error { + var s string + + err := n.Decode(&s) + if err != nil { + return err + } + + tm, err := dateparse.ParseAny(s) + if err != nil { + return err + } + *t = Time(tm) + return nil +} + +func (t *Time) UnmarshalJSON(b []byte) error { + s := strings.Trim(string(b), `"`) + tm, err := dateparse.ParseAny(s) + if err != nil { + return err + } + *t = Time(tm) + return nil +} + +func (t Time) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Time(t)) +} + +func (t Time) String() string { + return time.Time(t).String() +} + +func (t Time) Time() time.Time { + return time.Time(t) +} + +type Duration time.Duration + +func (d *Duration) UnmarshalYAML(n *yaml.Node) error { + var s string + + err := n.Decode(&s) + if err != nil { + return err + } + + dur, err := time.ParseDuration(s) + if err != nil { + return err + } + + *d = Duration(dur) + return nil +} + +func (d *Duration) UnmarshalJSON(b []byte) error { + s := strings.Trim(string(b), `"`) + dur, err := time.ParseDuration(s) + if err != nil { + return err + } + + *d = Duration(dur) + return nil +} + +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(d)) +} + +func (d Duration) Duration() time.Duration { + return time.Duration(d) +} + +func (d Duration) String() string { + return time.Duration(d).String() +} + +func ParseDuration(s string) (Duration, error) { + d, err := time.ParseDuration(s) + return Duration(d), err +} + +func ParseAny(s string, opt ...dateparse.ParserOption) (Time, error) { + t, err := dateparse.ParseAny(s, opt...) + return Time(t), err +} + +func ParseInLocal(s string, opt ...dateparse.ParserOption) (Time, error) { + t, err := dateparse.ParseIn(s, time.Now().Location(), opt...) + return Time(t), err +} diff --git a/internal/timeseries/level.go b/internal/timeseries/level.go index 8dd41b0..5ac5f48 100644 --- a/internal/timeseries/level.go +++ b/internal/timeseries/level.go @@ -1,8 +1,9 @@ package timeseries import ( - "log" "time" + + "github.com/rs/zerolog/log" ) type level struct { @@ -53,7 +54,7 @@ func (l *level) increaseAtTime(amount int, time time.Time) { if difference < 0 { // this cannot be negative because we advance before // can at least be 0 - log.Println("level.increaseTime was called with a time in the future") + log.Error().Time("time", time).Msg("level.increaseTime was called with a time in the future") } // l.length-1 because the newest element is always l.length-1 away from oldest steps := (l.length - 1) - int(difference/l.granularity) diff --git a/internal/trending/slidingwindow/slidingwindow.go b/internal/trending/slidingwindow/slidingwindow.go index 9b825e7..7b12ede 100644 --- a/internal/trending/slidingwindow/slidingwindow.go +++ b/internal/trending/slidingwindow/slidingwindow.go @@ -9,13 +9,6 @@ type Clock interface { Now() time.Time } -// defaultClock is used in case no clock is provided to the constructor. -type defaultClock struct{} - -func (c *defaultClock) Now() time.Time { - return time.Now() -} - type slidingWindow struct { buffer []float64 length int @@ -68,7 +61,7 @@ func NewSlidingWindow(os ...option) *slidingWindow { o(&opts) } if opts.clock == nil { - opts.clock = &defaultClock{} + panic("clock not set") } if opts.step.Nanoseconds() == 0 { opts.step = defaultStep diff --git a/pkg/gordio/alerting/alerting.go b/pkg/gordio/alerting/alerting.go index 12c9bfa..ad1db21 100644 --- a/pkg/gordio/alerting/alerting.go +++ b/pkg/gordio/alerting/alerting.go @@ -39,10 +39,9 @@ type alerter struct { scorer trending.Scorer[cl.Talkgroup] scores trending.Scores[cl.Talkgroup] lastScore time.Time + sim *Simulation } -type noopAlerter struct{} - type offsetClock time.Duration func (c *offsetClock) Now() time.Time { @@ -53,18 +52,21 @@ func (c *offsetClock) Duration() time.Duration { return time.Duration(*c) } +// OffsetClock returns a clock whose Now() method returns the specified offset from the current time. func OffsetClock(d time.Duration) offsetClock { return offsetClock(d) } type AlertOption func(*alerter) +// WithClock makes the alerter use a simulated clock. func WithClock(clock timeseries.Clock) AlertOption { return func(as *alerter) { as.clock = clock } } +// New creates a new Alerter using the provided configuration. func New(cfg config.Alerting, opts ...AlertOption) Alerter { if !cfg.Enable { return &noopAlerter{} @@ -82,8 +84,8 @@ func New(cfg config.Alerting, opts ...AlertOption) Alerter { as.scorer = trending.NewScorer[cl.Talkgroup]( trending.WithTimeSeries(as.newTimeSeries), trending.WithStorageDuration[cl.Talkgroup](time.Hour*24*time.Duration(cfg.LookbackDays)), - trending.WithRecentDuration[cl.Talkgroup](cfg.Recent), - trending.WithHalfLife[cl.Talkgroup](cfg.HalfLife), + trending.WithRecentDuration[cl.Talkgroup](time.Duration(cfg.Recent)), + trending.WithHalfLife[cl.Talkgroup](time.Duration(cfg.HalfLife)), trending.WithScoreThreshold[cl.Talkgroup](ScoreThreshold), trending.WithCountThreshold[cl.Talkgroup](CountThreshold), trending.WithClock[cl.Talkgroup](as.clock), @@ -92,6 +94,7 @@ func New(cfg config.Alerting, opts ...AlertOption) Alerter { return as } +// Go is the alerting loop. It does not start a goroutine. func (as *alerter) Go(ctx context.Context) { as.startBackfill(ctx) @@ -126,12 +129,12 @@ func (as *alerter) startBackfill(ctx context.Context) { now := time.Now() since := now.Add(-24 * time.Hour * time.Duration(as.cfg.LookbackDays)) log.Debug().Time("since", since).Msg("starting stats backfill") - count, err := as.backfill(ctx, since) + count, err := as.backfill(ctx, since, now) if err != nil { log.Error().Err(err).Msg("backfill failed") return } - log.Debug().Int("count", count).Str("in", time.Now().Sub(now).String()).Int("len", as.scorer.Score().Len()).Msg("backfill finished") + log.Debug().Int("callsCount", count).Str("in", time.Now().Sub(now).String()).Int("tgCount", as.scorer.Score().Len()).Msg("backfill finished") } func (as *alerter) score(ctx context.Context, now time.Time) { @@ -143,11 +146,11 @@ func (as *alerter) score(ctx context.Context, now time.Time) { sort.Sort(as.scores) } -func (as *alerter) backfill(ctx context.Context, since time.Time) (count int, err error) { +func (as *alerter) backfill(ctx context.Context, since time.Time, until time.Time) (count int, err error) { db := database.FromCtx(ctx) - const backfillStatsQuery = `SELECT system, talkgroup, call_date FROM calls WHERE call_date > $1 AND call_date < $2` + const backfillStatsQuery = `SELECT system, talkgroup, call_date FROM calls WHERE call_date > $1 AND call_date < $2 ORDER BY call_date ASC` - rows, err := db.Query(ctx, backfillStatsQuery, since, timeseries.DefaultClock.Now()) + rows, err := db.Query(ctx, backfillStatsQuery, since, until) if err != nil { return count, err } @@ -163,6 +166,9 @@ func (as *alerter) backfill(ctx context.Context, since time.Time) (count int, er return count, err } as.scorer.AddEvent(tg, callDate) + if as.sim != nil { // step the simulator if it is active + as.sim.stepClock(callDate) + } count++ } @@ -187,6 +193,9 @@ func (as *alerter) Call(ctx context.Context, call *cl.Call) error { func (*alerter) Enabled() bool { return true } +// noopAlerter is used when alerting is disabled. +type noopAlerter struct{} + func (*noopAlerter) SinkType() string { return "noopAlerter" } func (*noopAlerter) Call(_ context.Context, _ *cl.Call) error { return nil } func (*noopAlerter) Go(_ context.Context) {} diff --git a/pkg/gordio/alerting/simulate.go b/pkg/gordio/alerting/simulate.go new file mode 100644 index 0000000..f8bf15f --- /dev/null +++ b/pkg/gordio/alerting/simulate.go @@ -0,0 +1,172 @@ +package alerting + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + "strconv" + "time" + + "dynatron.me/x/stillbox/internal/jsontime" + "dynatron.me/x/stillbox/internal/trending" + cl "dynatron.me/x/stillbox/pkg/calls" + "dynatron.me/x/stillbox/pkg/gordio/config" + + "github.com/rs/zerolog/log" +) + +// A Simulation simulates what happens to the alerter during a specified time +// period using past data from the database. +type Simulation struct { + // normal Alerting config + config.Alerting + + // ScoreStart is the time when scoring begins + ScoreStart jsontime.Time `json:"scoreStart"` + // ScoreEnd is the time when the score simulator ends. Left blank, it defaults to time.Now() + ScoreEnd jsontime.Time `json:"scoreEnd"` + + // SimInterval is the interval at which the scorer will be called + SimInterval jsontime.Duration `json:"simInterval"` + + clock offsetClock `json:"-"` + *alerter `json:"-"` +} + +func (s *Simulation) verify() error { + switch { + case !s.ScoreEnd.Time().IsZero() && s.ScoreStart.Time().After(s.ScoreEnd.Time()): + return errors.New("end is before start") + case s.LookbackDays > 14: + return errors.New("lookback days >14") + } + return nil +} + +// stepClock is called by backfill during simulation operations. +func (s *Simulation) stepClock(t time.Time) { + now := s.clock.Now() + step := t.Sub(s.lastScore) + if step > time.Duration(s.SimInterval) { + s.clock += offsetClock(s.SimInterval) + s.scores = s.scorer.Score() + s.lastScore = now + } + +} + +// Simulate begins the simulation using the DB handle from ctx. It returns final scores. +func (s *Simulation) Simulate(ctx context.Context) trending.Scores[cl.Talkgroup] { + now := time.Now() + s.Enable = true + s.alerter = New(s.Alerting, WithClock(&s.clock)).(*alerter) + if time.Time(s.ScoreEnd).IsZero() { + s.ScoreEnd = jsontime.Time(now) + } + log.Debug().Time("scoreStart", s.ScoreStart.Time()). + Time("scoreEnd", s.ScoreEnd.Time()). + Str("interval", s.SimInterval.String()). + Uint("lookbackDays", s.LookbackDays). + Msg("simulation start") + + scoreEnd := time.Time(s.ScoreEnd) + + // compute lookback start Time + sinceLookback := time.Time(scoreEnd).Add(-24 * time.Hour * time.Duration(s.LookbackDays)) + + // backfill from lookback start until score start + s.backfill(ctx, sinceLookback, time.Time(s.ScoreStart)) + + // initial score + s.scores = s.scorer.Score() + s.lastScore = time.Time(s.ScoreStart) + + ssT := time.Time(s.ScoreStart) + nowDiff := now.Sub(time.Time(ssT)) + + // and set the clock offset to it + s.clock -= offsetClock(nowDiff) + + // turn on sim mode + s.alerter.sim = s + + // compute time since score start until now + // backfill from scorestart until now. sim is enabled, so scoring will be done by stepClock() + s.backfill(ctx, time.Time(s.ScoreStart), scoreEnd) + + s.lastScore = scoreEnd + sort.Sort(s.scores) + + return s.scores +} + +// simulateHandler is the POST endpoint handler. +func (as *alerter) simulateHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := new(Simulation) + switch r.Header.Get("Content-Type") { + case "application/json": + err := json.NewDecoder(r.Body).Decode(s) + if err != nil { + err = fmt.Errorf("simulate decode: %w", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + default: + err := r.ParseForm() + if err != nil { + err = fmt.Errorf("simulate form parse: %w", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + lbd, err := strconv.Atoi(r.Form["lookbackDays"][0]) + if err != nil { + err = fmt.Errorf("lookbackDays parse: %w", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + s.LookbackDays = uint(lbd) + s.HalfLife, err = jsontime.ParseDuration(r.Form["halfLife"][0]) + if err != nil { + err = fmt.Errorf("halfLife parse: %w", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + s.Recent, err = jsontime.ParseDuration(r.Form["recent"][0]) + if err != nil { + err = fmt.Errorf("recent parse: %w", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + s.SimInterval, err = jsontime.ParseDuration(r.Form["simInterval"][0]) + if err != nil { + err = fmt.Errorf("simInterval parse: %w", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + s.ScoreStart, err = jsontime.ParseInLocal(r.Form["scoreStart"][0]) + if err != nil { + err = fmt.Errorf("scoreStart parse: %w", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + s.ScoreEnd, err = jsontime.ParseInLocal(r.Form["scoreEnd"][0]) + if err != nil { + s.ScoreEnd = jsontime.Time{} + } + + } + + err := s.verify() + if err != nil { + err = fmt.Errorf("simulation profile verify: %w", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + s.Simulate(ctx) + s.tgStatsHandler(w, r) +} diff --git a/pkg/gordio/alerting/stats.go b/pkg/gordio/alerting/stats.go index 164b565..2c69dec 100644 --- a/pkg/gordio/alerting/stats.go +++ b/pkg/gordio/alerting/stats.go @@ -2,14 +2,18 @@ package alerting import ( _ "embed" + "errors" "fmt" "html/template" "net/http" + "strconv" "time" "dynatron.me/x/stillbox/pkg/calls" + "dynatron.me/x/stillbox/pkg/gordio/config" "dynatron.me/x/stillbox/pkg/gordio/database" + "dynatron.me/x/stillbox/internal/jsontime" "dynatron.me/x/stillbox/internal/trending" "github.com/go-chi/chi/v5" @@ -25,20 +29,48 @@ type stats interface { var ( funcMap = template.FuncMap{ - "f": func(v float64) string { + "f": func(v float64, places ...int) string { + if len(places) > 0 { + return fmt.Sprintf("%."+strconv.Itoa(places[0])+"f", v) + } return fmt.Sprintf("%.4f", v) }, + "dict": func(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, errors.New("invalid dict call") + } + dict := make(map[string]interface{}, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, errors.New("dict keys must be strings") + } + dict[key] = values[i+1] + } + return dict, nil + }, + "formTime": func(t jsontime.Time) string { + return time.Time(t).Format("2006-01-02T15:04") + }, + "ago": func(s string) (string, error) { + d, err := time.ParseDuration(s) + if err != nil { + return "", err + } + return time.Now().Add(-d).Format("2006-01-02T15:04"), nil + }, } statTmpl = template.Must(template.New("stats").Funcs(funcMap).Parse(statsTemplateFile)) ) func (as *alerter) PrivateRoutes(r chi.Router) { - r.Get("/tgstats", as.tgStats) + r.Get("/tgstats", as.tgStatsHandler) + r.Post("/tgstats", as.simulateHandler) } func (as *noopAlerter) PrivateRoutes(r chi.Router) {} -func (as *alerter) tgStats(w http.ResponseWriter, r *http.Request) { +func (as *alerter) tgStatsHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() db := database.FromCtx(ctx) @@ -60,13 +92,17 @@ func (as *alerter) tgStats(w http.ResponseWriter, r *http.Request) { } renderData := struct { - TGs map[calls.Talkgroup]database.GetTalkgroupsByPackedIDsRow - Scores trending.Scores[calls.Talkgroup] - LastScore time.Time + TGs map[calls.Talkgroup]database.GetTalkgroupsByPackedIDsRow + Scores trending.Scores[calls.Talkgroup] + LastScore time.Time + Simulation *Simulation + Config config.Alerting }{ - TGs: tgMap, - Scores: as.scores, - LastScore: as.lastScore, + TGs: tgMap, + Scores: as.scores, + LastScore: as.lastScore, + Config: as.cfg, + Simulation: as.sim, } w.WriteHeader(http.StatusOK) diff --git a/pkg/gordio/alerting/stats.html b/pkg/gordio/alerting/stats.html index 61e4e56..24e1af6 100644 --- a/pkg/gordio/alerting/stats.html +++ b/pkg/gordio/alerting/stats.html @@ -12,7 +12,7 @@ background: #105469; font-family: sans-serif; } - table { + table, #simform { background: #012B39; border-radius: 0.25em; border-collapse: collapse; @@ -29,7 +29,7 @@ padding: 0.5em 1em; text-align: left; } - td { + td, #simform { color: #fff; font-weight: 400; padding: 0.65em 1em; @@ -46,7 +46,30 @@
+Simulating from {{ formTime .Simulation.ScoreStart }} until {{ formTime .Simulation.ScoreEnd }} | +||||||||||
System | TG | @@ -64,9 +87,9 @@|||||||||
---|---|---|---|---|---|---|---|---|---|---|
{{ $tg.Name_2}} | {{ $tg.Name}} | -{{ .ID.Talkgroup }} | -{{ .Count }} | -{{ f .RecentCount }} | +{{ .ID.Talkgroup }} | +{{ f .Count 0 }} | +{{ f .RecentCount 0 }} | {{ f .Score }} | {{ f .Probability }} | {{ f .Expectation }} | diff --git a/pkg/gordio/config/config.go b/pkg/gordio/config/config.go index 31f9899..3ba54d3 100644 --- a/pkg/gordio/config/config.go +++ b/pkg/gordio/config/config.go @@ -5,6 +5,8 @@ import ( "sync" "time" + "dynatron.me/x/stillbox/internal/jsontime" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "gopkg.in/yaml.v3" @@ -51,10 +53,10 @@ type RateLimit struct { } type Alerting struct { - Enable bool `yaml:"enable"` - LookbackDays uint `yaml:"lookbackDays"` - HalfLife time.Duration `yaml:"halfLife"` - Recent time.Duration `yaml:"recent"` + Enable bool `yaml:"enable"` + LookbackDays uint `yaml:"lookbackDays"` + HalfLife jsontime.Duration `yaml:"halfLife"` + Recent jsontime.Duration `yaml:"recent"` } func (rl *RateLimit) Verify() bool { diff --git a/pkg/gordio/server/noprofile.go b/pkg/gordio/server/noprofile.go new file mode 100644 index 0000000..ceff05c --- /dev/null +++ b/pkg/gordio/server/noprofile.go @@ -0,0 +1,6 @@ +//go:build !pprof +// +build !pprof + +package server + +func (s *Server) installPprof() {} diff --git a/pkg/gordio/server/profile.go b/pkg/gordio/server/profile.go new file mode 100644 index 0000000..eb5787e --- /dev/null +++ b/pkg/gordio/server/profile.go @@ -0,0 +1,17 @@ +//go:build pprof +// +build pprof + +package server + +import ( + "net/http/pprof" +) + +func (s *Server) installPprof() { + r := s.r + r.HandleFunc("/debug/pprof/", pprof.Index) + r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + r.HandleFunc("/debug/pprof/profile", pprof.Profile) + r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + r.HandleFunc("/debug/pprof/trace", pprof.Trace) +} diff --git a/pkg/gordio/server/routes.go b/pkg/gordio/server/routes.go index 601c2db..19c03d9 100644 --- a/pkg/gordio/server/routes.go +++ b/pkg/gordio/server/routes.go @@ -28,6 +28,8 @@ func (s *Server) setupRoutes() { r := s.r r.Use(middleware.WithValue(database.DBCTXKeyValue, s.db)) + s.installPprof() + r.Group(func(r chi.Router) { // authenticated routes r.Use(s.auth.VerifyMiddleware(), s.auth.AuthMiddleware())