Stats API
This commit is contained in:
parent
e9d1da92a0
commit
d3702b375b
5 changed files with 120 additions and 2 deletions
|
@ -47,6 +47,9 @@ var Policy = &restrict.PolicyDefinition{
|
|||
&restrict.Permission{Preset: PresetUpdateOwn},
|
||||
&restrict.Permission{Preset: PresetDeleteOwn},
|
||||
},
|
||||
entities.ResourceCallStats: {
|
||||
&restrict.Permission{Action: entities.ActionRead},
|
||||
},
|
||||
},
|
||||
},
|
||||
entities.RoleSubmitter: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/url"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/shares"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
@ -165,6 +166,7 @@ var statusMapping = map[error]errResponder{
|
|||
shares.ErrNoShare: notFoundErrText,
|
||||
ErrBadShare: notFoundErrText,
|
||||
shares.ErrBadType: badRequestErrText,
|
||||
calls.ErrInvalidInterval: badRequestErrText,
|
||||
}
|
||||
|
||||
func autoError(err error) render.Renderer {
|
||||
|
|
|
@ -10,8 +10,10 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/internal/forms"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/calls/callstore"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/stats"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
@ -34,6 +36,7 @@ func (ca *callsAPI) Subrouter() http.Handler {
|
|||
r.Get(`/{call:[a-f0-9-]+}`, ca.getAudioRoute)
|
||||
r.Get(`/{call:[a-f0-9-]+}/{download:download}`, ca.getAudioRoute)
|
||||
r.Post(`/`, ca.listCalls)
|
||||
r.Get(`/stats/{interval}`, ca.getCallStats)
|
||||
|
||||
return r
|
||||
}
|
||||
|
@ -55,6 +58,29 @@ func (ca *callsAPI) getAudioRoute(w http.ResponseWriter, r *http.Request) {
|
|||
ca.getAudio(p, w, r)
|
||||
}
|
||||
|
||||
func (ca *callsAPI) getCallStats(w http.ResponseWriter, r *http.Request) {
|
||||
p := struct {
|
||||
Interval calls.StatsInterval `param:"interval"`
|
||||
}{}
|
||||
|
||||
err := decodeParams(&p, r)
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
sts := stats.FromCtx(ctx)
|
||||
|
||||
st, err := sts.GetCallStats(ctx, p.Interval)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, st)
|
||||
}
|
||||
|
||||
func (ca *callsAPI) getAudio(p getAudioParams, w http.ResponseWriter, r *http.Request) {
|
||||
if p.CallID == nil {
|
||||
wErr(w, r, badRequest(ErrNoCall))
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"dynatron.me/x/stillbox/pkg/shares"
|
||||
"dynatron.me/x/stillbox/pkg/sinks"
|
||||
"dynatron.me/x/stillbox/pkg/sources"
|
||||
"dynatron.me/x/stillbox/pkg/stats"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
|
||||
|
@ -55,6 +56,7 @@ type Server struct {
|
|||
incidents incstore.Store
|
||||
share shares.Service
|
||||
rbac rbac.RBAC
|
||||
stats stats.Stats
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||
|
@ -80,13 +82,16 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
|||
}
|
||||
|
||||
tgCache := tgstore.NewCache(db)
|
||||
api := rest.New(cfg.BaseURL.URL())
|
||||
|
||||
rbacSvc, err := rbac.New(policy.Policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
callStore := callstore.NewStore(db)
|
||||
statsSvc := stats.NewStats(callStore, stats.DefaultExpiration)
|
||||
api := rest.New(cfg.BaseURL.URL())
|
||||
|
||||
srv := &Server{
|
||||
auth: authenticator,
|
||||
conf: cfg,
|
||||
|
@ -101,9 +106,10 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
|||
rest: api,
|
||||
share: shares.NewService(),
|
||||
users: ust,
|
||||
calls: callstore.NewStore(db),
|
||||
calls: callStore,
|
||||
incidents: incstore.NewStore(),
|
||||
rbac: rbacSvc,
|
||||
stats: statsSvc,
|
||||
}
|
||||
|
||||
if cfg.DB.Partition.Enabled {
|
||||
|
@ -169,6 +175,7 @@ func (s *Server) fillCtx(ctx context.Context) context.Context {
|
|||
ctx = incstore.CtxWithStore(ctx, s.incidents)
|
||||
ctx = shares.CtxWithStore(ctx, s.share)
|
||||
ctx = rbac.CtxWithRBAC(ctx, s.rbac)
|
||||
ctx = stats.CtxWithStats(ctx, s.stats)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
|
80
pkg/stats/stats.go
Normal file
80
pkg/stats/stats.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package stats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/cache"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/calls/callstore"
|
||||
"dynatron.me/x/stillbox/pkg/services"
|
||||
)
|
||||
|
||||
const DefaultExpiration = 5 * time.Minute
|
||||
|
||||
type Stats interface {
|
||||
GetCallStats(ctx context.Context, interval calls.StatsInterval) (*calls.Stats, error)
|
||||
}
|
||||
|
||||
type statsKeyType string
|
||||
|
||||
const StatsKey statsKeyType = "stats"
|
||||
|
||||
func CtxWithStats(ctx context.Context, s Stats) context.Context {
|
||||
return services.WithValue(ctx, StatsKey, s)
|
||||
}
|
||||
|
||||
func FromCtx(ctx context.Context) Stats {
|
||||
s, ok := services.Value(ctx, StatsKey).(Stats)
|
||||
if !ok {
|
||||
panic("no stats in context")
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
type stats struct {
|
||||
cs callstore.Store
|
||||
sc cache.Cache[calls.StatsInterval, *calls.Stats]
|
||||
}
|
||||
|
||||
func NewStats(cst callstore.Store, cacheExp time.Duration) *stats {
|
||||
s := &stats{
|
||||
cs: cst,
|
||||
sc: cache.New[calls.StatsInterval, *calls.Stats](cache.WithExpiration(cacheExp)),
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *stats) GetCallStats(ctx context.Context, interval calls.StatsInterval) (*calls.Stats, error) {
|
||||
st, has := s.sc.Get(interval)
|
||||
if has {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
var start time.Time
|
||||
now := time.Now()
|
||||
switch interval {
|
||||
case calls.IntervalHour:
|
||||
start = now.Add(-24 * time.Hour) // one day
|
||||
case calls.IntervalDay:
|
||||
start = now.Add(-7 * 24 * time.Hour) // one week
|
||||
case calls.IntervalWeek:
|
||||
start = now.Add(-30 * 24 * time.Hour) // one month
|
||||
case calls.IntervalMonth:
|
||||
start = now.Add(-365 * 24 * time.Hour) // one year
|
||||
default:
|
||||
return nil, calls.ErrInvalidInterval
|
||||
}
|
||||
|
||||
st, err := s.cs.CallStats(ctx, interval, jsontypes.Time(start), jsontypes.Time(now))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.sc.Set(interval, st)
|
||||
|
||||
return st, nil
|
||||
}
|
Loading…
Add table
Reference in a new issue