Add talkgroup activity alerting #17
14 changed files with 458 additions and 82 deletions
9
Makefile
9
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
|
||||
|
|
25
go.mod
25
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
|
||||
)
|
||||
|
|
64
go.sum
64
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=
|
||||
|
|
107
internal/jsontime/jsontime.go
Normal file
107
internal/jsontime/jsontime.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {}
|
||||
|
|
172
pkg/gordio/alerting/simulate.go
Normal file
172
pkg/gordio/alerting/simulate.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="simform">
|
||||
<form action="/tgstats" method="POST">
|
||||
{{ define "simform" }}
|
||||
<label for="lookbackDays">Lookback Days</label> <input id="lookbackDays" name="lookbackDays" type="number" min="1" max="14" value="{{ .Lookback }}" />
|
||||
<label for="halfLife">Half life</label> <input id="halfLife" name="halfLife" type="text" value="{{ .HalfLife }}" />
|
||||
<label for="recent">Recent</label> <input id="recent" name="recent" type="text" value="{{ .Recent }}" />
|
||||
<label for="simInterval">Sim Interval</label> <input id="simInterval" name="simInterval" type="text" value="{{ .SimInterval }}" />
|
||||
<label for="scoreStart">Score Start</label> <input id="scoreStart" name="scoreStart" type="datetime-local" value="{{ .ScoreStart }}" />
|
||||
<label for="scoreEnd">Score End</label> <input id="scoreEnd" name="scoreEnd" type="datetime-local" value="{{ .ScoreEnd }}" />
|
||||
<input type="submit" value="Simulate" />
|
||||
{{end}}
|
||||
{{ if .Simulation }}
|
||||
{{ template "simform" dict "Lookback" .Simulation.LookbackDays "HalfLife" .Simulation.HalfLife "Recent" .Simulation.Recent "ScoreStart" (formTime .Simulation.ScoreStart) "ScoreEnd" (formTime .Simulation.ScoreEnd) "SimInterval" .Simulation.SimInterval }}
|
||||
{{ else }}
|
||||
{{ template "simform" dict "Lookback" .Config.LookbackDays "HalfLife" .Config.HalfLife "Recent" .Config.Recent "ScoreStart" (ago "72h") "ScoreEnd" "" "SimInterval" "5m" }}
|
||||
{{ end }}
|
||||
</form>
|
||||
</div>
|
||||
<table>
|
||||
{{ if .Simulation }}
|
||||
<tr>
|
||||
<td colspan="10">Simulating from {{ formTime .Simulation.ScoreStart }} until {{ formTime .Simulation.ScoreEnd }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<th>System</th>
|
||||
<th>TG</th>
|
||||
|
@ -64,9 +87,9 @@
|
|||
<tr>
|
||||
<td>{{ $tg.Name_2}}</td>
|
||||
<td>{{ $tg.Name}}</td>
|
||||
<td>{{ .ID.Talkgroup }}</td>
|
||||
<td>{{ .Count }}</td>
|
||||
<td>{{ f .RecentCount }}</td>
|
||||
<td>{{ .ID.Talkgroup }}</td>
|
||||
<td>{{ f .Count 0 }}</td>
|
||||
<td>{{ f .RecentCount 0 }}</td>
|
||||
<td>{{ f .Score }}</td>
|
||||
<td>{{ f .Probability }}</td>
|
||||
<td>{{ f .Expectation }}</td>
|
||||
|
|
|
@ -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 {
|
||||
|
|
6
pkg/gordio/server/noprofile.go
Normal file
6
pkg/gordio/server/noprofile.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
//go:build !pprof
|
||||
// +build !pprof
|
||||
|
||||
package server
|
||||
|
||||
func (s *Server) installPprof() {}
|
17
pkg/gordio/server/profile.go
Normal file
17
pkg/gordio/server/profile.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue