From 0ce110fa526fea689c2586f2cb3b1f173efa3137 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 3 Dec 2023 15:12:43 +0100 Subject: [PATCH] feat(metrics): add active link share logins --- pkg/metrics/active_users.go | 108 ++++++++++++++++++++++++------------ pkg/metrics/metrics.go | 10 +--- pkg/models/error.go | 12 ++-- pkg/routes/metrics.go | 4 ++ pkg/user/error.go | 4 +- 5 files changed, 87 insertions(+), 51 deletions(-) diff --git a/pkg/metrics/active_users.go b/pkg/metrics/active_users.go index d2eba779b93..394de8a83d6 100644 --- a/pkg/metrics/active_users.go +++ b/pkg/metrics/active_users.go @@ -27,83 +27,119 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" ) -// SecondsUntilInactive defines the seconds until a user is considered inactive -const SecondsUntilInactive = 30 +const secondsUntilInactive = 30 +const activeUsersKey = `active_users` +const activeLinkSharesKey = `active_link_shares` -// ActiveUsersKey is the key used to store active users in redis -const ActiveUsersKey = `activeusers` - -// ActiveUser defines an active user -type ActiveUser struct { - UserID int64 +// ActiveAuthenticable defines an active user or link share +type ActiveAuthenticable struct { + ID int64 LastSeen time.Time } -type activeUsersMap map[int64]*ActiveUser +type activeUsersMap map[int64]*ActiveAuthenticable -// ActiveUsers is the type used to save active users type ActiveUsers struct { users activeUsersMap mutex *sync.Mutex } -// activeUsers holds a map with all active users var activeUsers *ActiveUsers +type activeLinkSharesMap map[int64]*ActiveAuthenticable + +type ActiveLinkShares struct { + shares activeLinkSharesMap + mutex *sync.Mutex +} + +var activeLinkShares *ActiveLinkShares + func init() { activeUsers = &ActiveUsers{ - users: make(map[int64]*ActiveUser), + users: make(map[int64]*ActiveAuthenticable), mutex: &sync.Mutex{}, } + activeLinkShares = &ActiveLinkShares{ + shares: make(map[int64]*ActiveAuthenticable), + mutex: &sync.Mutex{}, + } } func setupActiveUsersMetric() { err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{ Name: "vikunja_active_users", - Help: "The number of users active within the last 30 seconds on this node", + Help: "The number of shares active within the last 30 seconds", }, func() float64 { - allActiveUsers, err := getActiveUsers() + allActiveUsers := activeUsersMap{} + _, err := keyvalue.GetWithValue(activeUsersKey, &allActiveUsers) if err != nil { log.Error(err.Error()) + return 0 } if allActiveUsers == nil { return 0 } - activeUsersCount := 0 + count := 0 for _, u := range allActiveUsers { - if time.Since(u.LastSeen) < SecondsUntilInactive*time.Second { - activeUsersCount++ + if time.Since(u.LastSeen) < secondsUntilInactive*time.Second { + count++ } } - return float64(activeUsersCount) + return float64(count) })) if err != nil { - log.Criticalf("Could not register metrics for currently active users: %s", err) + log.Criticalf("Could not register metrics for currently active shares: %s", err) } } -// SetUserActive sets a user as active and pushes it to redis +func setupActiveLinkSharesMetric() { + err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "vikunja_active_link_shares", + Help: "The number of link shares active within the last 30 seconds. Similar to vikunja_active_users.", + }, func() float64 { + allActiveLinkShares := activeLinkSharesMap{} + _, err := keyvalue.GetWithValue(activeLinkSharesKey, &allActiveLinkShares) + if err != nil { + log.Error(err.Error()) + return 0 + } + if allActiveLinkShares == nil { + return 0 + } + count := 0 + for _, u := range allActiveLinkShares { + if time.Since(u.LastSeen) < secondsUntilInactive*time.Second { + count++ + } + } + return float64(count) + })) + if err != nil { + log.Criticalf("Could not register metrics for currently active link shares: %s", err) + } +} + +// SetUserActive sets a user as active and pushes it to keyvalue func SetUserActive(a web.Auth) (err error) { activeUsers.mutex.Lock() - activeUsers.users[a.GetID()] = &ActiveUser{ - UserID: a.GetID(), + defer activeUsers.mutex.Unlock() + activeUsers.users[a.GetID()] = &ActiveAuthenticable{ + ID: a.GetID(), LastSeen: time.Now(), } - activeUsers.mutex.Unlock() - return PushActiveUsers() + + return keyvalue.Put(activeUsersKey, activeUsers.users) } -// getActiveUsers returns the active users from redis -func getActiveUsers() (users activeUsersMap, err error) { - users = activeUsersMap{} - _, err = keyvalue.GetWithValue(ActiveUsersKey, &users) - return -} +// SetLinkShareActive sets a user as active and pushes it to keyvalue +func SetLinkShareActive(a web.Auth) (err error) { + activeLinkShares.mutex.Lock() + defer activeLinkShares.mutex.Unlock() + activeLinkShares.shares[a.GetID()] = &ActiveAuthenticable{ + ID: a.GetID(), + LastSeen: time.Now(), + } -// PushActiveUsers pushed the content of the activeUsers map to redis -func PushActiveUsers() (err error) { - activeUsers.mutex.Lock() - defer activeUsers.mutex.Unlock() - - return keyvalue.Put(ActiveUsersKey, activeUsers.users) + return keyvalue.Put(activeLinkSharesKey, activeLinkShares.shares) } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index af54056a099..fe02d28f708 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -31,7 +31,7 @@ const ( // ProjectCountKey is the name of the key in which we save the project count ProjectCountKey = `projectcount` - // UserCountKey is the name of the key we use to store total users in redis + // UserCountKey is the name of the key we use to store total shares in redis UserCountKey = `usercount` // TaskCountKey is the name of the key we use to store the amount of total tasks in redis @@ -55,11 +55,6 @@ func GetRegistry() *prometheus.Registry { // InitMetrics Initializes the metrics func InitMetrics() { - // init active users, sometimes we'll have garbage from previous runs in redis instead - if err := PushActiveUsers(); err != nil { - log.Fatalf("Could not set initial count for active users, error was %s", err) - } - GetRegistry() // Register total project count metric @@ -77,7 +72,7 @@ func InitMetrics() { // Register total user count metric err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{ Name: "vikunja_user_count", - Help: "The total number of users on this instance", + Help: "The total number of shares on this instance", }, func() float64 { count, _ := GetCount(UserCountKey) return float64(count) @@ -111,6 +106,7 @@ func InitMetrics() { } setupActiveUsersMetric() + setupActiveLinkSharesMetric() } // GetCount returns the current count from keyvalue diff --git a/pkg/models/error.go b/pkg/models/error.go index 77cde695513..195bf402dea 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -161,7 +161,7 @@ func IsErrNeedToHaveProjectReadAccess(err error) bool { } func (err ErrNeedToHaveProjectReadAccess) Error() string { - return fmt.Sprintf("User needs to have read access to that project [ProjectID: %d, UserID: %d]", err.ProjectID, err.UserID) + return fmt.Sprintf("User needs to have read access to that project [ProjectID: %d, ID: %d]", err.ProjectID, err.UserID) } // ErrCodeNeedToHaveProjectReadAccess holds the unique world-error code of this error @@ -518,7 +518,7 @@ func IsErrNoRightToSeeTask(err error) bool { } func (err ErrNoRightToSeeTask) Error() string { - return fmt.Sprintf("User does not have the right to see the task [TaskID: %v, UserID: %v]", err.TaskID, err.UserID) + return fmt.Sprintf("User does not have the right to see the task [TaskID: %v, ID: %v]", err.TaskID, err.UserID) } // ErrCodeNoRightToSeeTask holds the unique world-error code of this error @@ -961,7 +961,7 @@ func IsErrUserAlreadyAssigned(err error) bool { } func (err ErrUserAlreadyAssigned) Error() string { - return fmt.Sprintf("User is already assigned to task [TaskID: %d, UserID: %d]", err.TaskID, err.UserID) + return fmt.Sprintf("User is already assigned to task [TaskID: %d, ID: %d]", err.TaskID, err.UserID) } // ErrCodeUserAlreadyAssigned holds the unique world-error code of this error @@ -1190,7 +1190,7 @@ func IsErrUserDoesNotHaveAccessToProject(err error) bool { } func (err ErrUserDoesNotHaveAccessToProject) Error() string { - return fmt.Sprintf("User does not have access to the project [ProjectID: %d, UserID: %d]", err.ProjectID, err.UserID) + return fmt.Sprintf("User does not have access to the project [ProjectID: %d, ID: %d]", err.ProjectID, err.UserID) } // ErrCodeUserDoesNotHaveAccessToProject holds the unique world-error code of this error @@ -1273,7 +1273,7 @@ func IsErrUserHasNoAccessToLabel(err error) bool { } func (err ErrUserHasNoAccessToLabel) Error() string { - return fmt.Sprintf("The user does not have access to this label [LabelID: %v, UserID: %v]", err.LabelID, err.UserID) + return fmt.Sprintf("The user does not have access to this label [LabelID: %v, ID: %v]", err.LabelID, err.UserID) } // ErrCodeUserHasNoAccessToLabel holds the unique world-error code of this error @@ -1568,7 +1568,7 @@ func IsErrSubscriptionAlreadyExists(err error) bool { } func (err *ErrSubscriptionAlreadyExists) Error() string { - return fmt.Sprintf("Subscription for this (entity_id, entity_type, user_id) already exists [EntityType: %d, EntityID: %d, UserID: %d]", err.EntityType, err.EntityID, err.UserID) + return fmt.Sprintf("Subscription for this (entity_id, entity_type, user_id) already exists [EntityType: %d, EntityID: %d, ID: %d]", err.EntityType, err.EntityID, err.UserID) } // ErrCodeSubscriptionAlreadyExists holds the unique world-error code of this error diff --git a/pkg/routes/metrics.go b/pkg/routes/metrics.go index a754192650e..9f60a6e1fa2 100644 --- a/pkg/routes/metrics.go +++ b/pkg/routes/metrics.go @@ -110,5 +110,9 @@ func updateActiveUsersFromContext(c echo.Context) (err error) { return } + if _, is := auth.(*models.LinkSharing); is { + return metrics.SetLinkShareActive(auth) + } + return metrics.SetUserActive(auth) } diff --git a/pkg/user/error.go b/pkg/user/error.go index ce1f8d0dbce..cb73f06d4bf 100644 --- a/pkg/user/error.go +++ b/pkg/user/error.go @@ -146,7 +146,7 @@ type ErrNoPasswordResetToken struct { } func (err ErrNoPasswordResetToken) Error() string { - return fmt.Sprintf("No token to reset a password [UserID: %d]", err.UserID) + return fmt.Sprintf("No token to reset a password [ID: %d]", err.UserID) } // ErrCodeNoPasswordResetToken holds the unique world-error code of this error @@ -237,7 +237,7 @@ type ErrEmailNotConfirmed struct { } func (err ErrEmailNotConfirmed) Error() string { - return fmt.Sprintf("Email is not confirmed [UserID: %d]", err.UserID) + return fmt.Sprintf("Email is not confirmed [ID: %d]", err.UserID) } // ErrCodeEmailNotConfirmed holds the unique world-error code of this error