implement session stats, fixes last (admin|user) left at won't work for any provider that removed users after logout.

This commit is contained in:
Miroslav Šedivý 2024-09-07 18:55:27 +02:00
parent 356a566bc6
commit 4f401ac2b6
7 changed files with 124 additions and 21 deletions

View file

@ -47,6 +47,7 @@ func (api *ApiManagerCtx) Route(r types.Router) {
r.Post("/logout", api.Logout) r.Post("/logout", api.Logout)
r.Get("/whoami", api.Whoami) r.Get("/whoami", api.Whoami)
r.Post("/profile", api.UpdateProfile) r.Post("/profile", api.UpdateProfile)
r.Get("/stats", api.Stats)
sessionsHandler := sessions.New(api.sessions) sessionsHandler := sessions.New(api.sessions)
r.Route("/sessions", sessionsHandler.Route) r.Route("/sessions", sessionsHandler.Route)

View file

@ -103,3 +103,8 @@ func (api *ApiManagerCtx) UpdateProfile(w http.ResponseWriter, r *http.Request)
return utils.HttpSuccess(w, true) return utils.HttpSuccess(w, true)
} }
func (api *ApiManagerCtx) Stats(w http.ResponseWriter, r *http.Request) error {
stats := api.sessions.Stats()
return utils.HttpSuccess(w, stats)
}

View file

@ -7,10 +7,8 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"time"
"m1k1o/neko/internal/api" "m1k1o/neko/internal/api"
"m1k1o/neko/internal/api/room"
oldEvent "m1k1o/neko/internal/http/legacy/event" oldEvent "m1k1o/neko/internal/http/legacy/event"
oldMessage "m1k1o/neko/internal/http/legacy/message" oldMessage "m1k1o/neko/internal/http/legacy/message"
oldTypes "m1k1o/neko/internal/http/legacy/types" oldTypes "m1k1o/neko/internal/http/legacy/types"
@ -43,7 +41,6 @@ var (
type LegacyHandler struct { type LegacyHandler struct {
logger zerolog.Logger logger zerolog.Logger
serverAddr string serverAddr string
startedAt time.Time
} }
func New() *LegacyHandler { func New() *LegacyHandler {
@ -52,7 +49,6 @@ func New() *LegacyHandler {
return &LegacyHandler{ return &LegacyHandler{
logger: log.With().Str("module", "legacy").Logger(), logger: log.With().Str("module", "legacy").Logger(),
serverAddr: "127.0.0.1:8080", serverAddr: "127.0.0.1:8080",
startedAt: time.Now(),
} }
} }
@ -208,9 +204,9 @@ func (h *LegacyHandler) Route(r types.Router) {
return utils.HttpInternalServerError().WithInternalErr(err) return utils.HttpInternalServerError().WithInternalErr(err)
} }
// get current control status // get stats
control := room.ControlStatusPayload{} newStats := types.Stats{}
err = s.apiReq(http.MethodGet, "/api/room/control", nil, &control) err = s.apiReq(http.MethodGet, "/api/stats", nil, &newStats)
if err != nil { if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err) return utils.HttpInternalServerError().WithInternalErr(err)
} }
@ -236,18 +232,6 @@ func (h *LegacyHandler) Route(r types.Router) {
} }
// append members // append members
stats.Members = append(stats.Members, member) stats.Members = append(stats.Members, member)
} else if session.State.NotConnectedSince != nil {
//
// TODO: This wont work if the user is removed after the session is closed
//
// populate last admin left time
if session.Profile.IsAdmin && (stats.LastAdminLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastAdminLeftAt)) {
stats.LastAdminLeftAt = session.State.NotConnectedSince
}
// populate last user left time
if !session.Profile.IsAdmin && (stats.LastUserLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastUserLeftAt)) {
stats.LastUserLeftAt = session.State.NotConnectedSince
}
} }
} }
@ -256,10 +240,12 @@ func (h *LegacyHandler) Route(r types.Router) {
return err return err
} }
stats.Host = control.HostId stats.Host = newStats.HostId
// TODO: stats.Banned, not implemented yet // TODO: stats.Banned, not implemented yet
stats.Locked = locks stats.Locked = locks
stats.ServerStartedAt = h.startedAt stats.ServerStartedAt = newStats.ServerStartedAt
stats.LastAdminLeftAt = newStats.LastAdminLeftAt
stats.LastUserLeftAt = newStats.LastUserLeftAt
stats.ControlProtection = settings.ControlProtection stats.ControlProtection = settings.ControlProtection
stats.ImplicitControl = settings.ImplicitHosting stats.ImplicitControl = settings.ImplicitHosting

View file

@ -4,6 +4,7 @@ import (
"errors" "errors"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
"github.com/kataras/go-events" "github.com/kataras/go-events"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -31,6 +32,8 @@ func New(config *config.Session) *SessionManagerCtx {
sessions: make(map[string]*SessionCtx), sessions: make(map[string]*SessionCtx),
cursors: make(map[types.Session][]types.Cursor), cursors: make(map[types.Session][]types.Cursor),
emmiter: events.New(), emmiter: events.New(),
serverStartedAt: time.Now(),
} }
// create API session // create API session
@ -76,6 +79,12 @@ type SessionManagerCtx struct {
emmiter events.EventEmmiter emmiter events.EventEmmiter
apiSession *SessionCtx apiSession *SessionCtx
serverStartedAt time.Time
totalAdmins atomic.Int32
lastAdminLeftAt atomic.Value
totalUsers atomic.Int32
lastUserLeftAt atomic.Value
} }
func (manager *SessionManagerCtx) Create(id string, profile types.MemberProfile) (types.Session, string, error) { func (manager *SessionManagerCtx) Create(id string, profile types.MemberProfile) (types.Session, string, error) {
@ -468,3 +477,36 @@ func (manager *SessionManagerCtx) Settings() types.Settings {
func (manager *SessionManagerCtx) CookieEnabled() bool { func (manager *SessionManagerCtx) CookieEnabled() bool {
return manager.config.CookieEnabled return manager.config.CookieEnabled
} }
// ---
// stats
// ---
func (manager *SessionManagerCtx) Stats() types.Stats {
hostId := ""
host, hasHost := manager.GetHost()
if hasHost {
hostId = host.ID()
}
var lastUserLeftAt *time.Time
if t, ok := manager.lastUserLeftAt.Load().(*time.Time); ok {
lastUserLeftAt = t
}
var lastAdminLeftAt *time.Time
if t, ok := manager.lastAdminLeftAt.Load().(*time.Time); ok {
lastAdminLeftAt = t
}
return types.Stats{
HasHost: hasHost,
HostId: hostId,
ServerStartedAt: manager.serverStartedAt,
TotalUsers: int(manager.totalUsers.Load()),
LastUserLeftAt: lastUserLeftAt,
TotalAdmins: int(manager.totalAdmins.Load()),
LastAdminLeftAt: lastAdminLeftAt,
}
}

View file

@ -121,6 +121,14 @@ func (session *SessionCtx) ConnectWebSocketPeer(websocketPeer types.WebSocketPee
session.state.ConnectedSince = &now session.state.ConnectedSince = &now
session.state.NotConnectedSince = nil session.state.NotConnectedSince = nil
if session.profile.IsAdmin {
session.manager.totalAdmins.Add(1)
session.manager.lastAdminLeftAt.Store((*time.Time)(nil))
} else {
session.manager.totalUsers.Add(1)
session.manager.lastUserLeftAt.Store((*time.Time)(nil))
}
session.manager.emmiter.Emit("connected", session) session.manager.emmiter.Emit("connected", session)
// if there is a previous peer, destroy it // if there is a previous peer, destroy it
@ -180,6 +188,16 @@ func (session *SessionCtx) DisconnectWebSocketPeer(websocketPeer types.WebSocket
session.state.ConnectedSince = nil session.state.ConnectedSince = nil
session.state.NotConnectedSince = &now session.state.NotConnectedSince = &now
if session.profile.IsAdmin {
if session.manager.totalAdmins.Add(-1) == 0 {
session.manager.lastAdminLeftAt.Store(&now)
}
} else {
if session.manager.totalUsers.Add(-1) == 0 {
session.manager.lastUserLeftAt.Store(&now)
}
}
session.manager.emmiter.Emit("disconnected", session) session.manager.emmiter.Emit("disconnected", session)
session.websocketMu.Lock() session.websocketMu.Lock()

View file

@ -125,6 +125,21 @@ paths:
schema: schema:
$ref: '#/components/schemas/MemberProfile' $ref: '#/components/schemas/MemberProfile'
required: true required: true
/api/stats:
get:
summary: stats
operationId: stats
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Stats'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
# #
# sessions # sessions
@ -1145,6 +1160,30 @@ components:
is_watching: is_watching:
type: boolean type: boolean
Stats:
type: object
properties:
has_host:
type: boolean
host_id:
type: string
optional: true
server_started_at:
type: string
format: date-time
total_users:
type: integer
last_user_left_at:
type: string
format: date-time
optional: true
total_admins:
type: integer
last_admin_left_at:
type: string
format: date-time
optional: true
# #
# room # room
# #

View file

@ -52,6 +52,16 @@ type Settings struct {
Plugins PluginSettings `json:"plugins"` Plugins PluginSettings `json:"plugins"`
} }
type Stats struct {
HasHost bool `json:"has_host"`
HostId string `json:"host_id,omitempty"`
ServerStartedAt time.Time `json:"server_started_at"`
TotalUsers int `json:"total_users"`
LastUserLeftAt *time.Time `json:"last_user_left_at,omitempty"`
TotalAdmins int `json:"total_admins"`
LastAdminLeftAt *time.Time `json:"last_admin_left_at,omitempty"`
}
type Session interface { type Session interface {
ID() string ID() string
Profile() MemberProfile Profile() MemberProfile
@ -110,6 +120,8 @@ type SessionManager interface {
Settings() Settings Settings() Settings
CookieEnabled() bool CookieEnabled() bool
Stats() Stats
CookieSetToken(w http.ResponseWriter, token string) CookieSetToken(w http.ResponseWriter, token string)
CookieClearToken(w http.ResponseWriter, r *http.Request) CookieClearToken(w http.ResponseWriter, r *http.Request)
Authenticate(r *http.Request) (Session, error) Authenticate(r *http.Request) (Session, error)