mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bbc85de34b | ||
![]() |
ec70d024fa | ||
![]() |
eae45baf38 | ||
![]() |
4cea50b5c8 | ||
![]() |
e4814431e0 | ||
![]() |
91b4cb2c13 | ||
![]() |
a3acdc7041 |
File diff suppressed because it is too large
Load Diff
17
main.go
17
main.go
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lpar/gzipped/v2"
|
"github.com/lpar/gzipped/v2"
|
||||||
"github.com/muety/wakapi/models"
|
|
||||||
"github.com/muety/wakapi/routes/relay"
|
"github.com/muety/wakapi/routes/relay"
|
||||||
|
|
||||||
"github.com/emvi/logbuch"
|
"github.com/emvi/logbuch"
|
||||||
@@ -58,6 +57,7 @@ var (
|
|||||||
summaryRepository repositories.ISummaryRepository
|
summaryRepository repositories.ISummaryRepository
|
||||||
keyValueRepository repositories.IKeyValueRepository
|
keyValueRepository repositories.IKeyValueRepository
|
||||||
diagnosticsRepository repositories.IDiagnosticsRepository
|
diagnosticsRepository repositories.IDiagnosticsRepository
|
||||||
|
metricsRepository *repositories.MetricsRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -149,6 +149,7 @@ func main() {
|
|||||||
summaryRepository = repositories.NewSummaryRepository(db)
|
summaryRepository = repositories.NewSummaryRepository(db)
|
||||||
keyValueRepository = repositories.NewKeyValueRepository(db)
|
keyValueRepository = repositories.NewKeyValueRepository(db)
|
||||||
diagnosticsRepository = repositories.NewDiagnosticsRepository(db)
|
diagnosticsRepository = repositories.NewDiagnosticsRepository(db)
|
||||||
|
metricsRepository = repositories.NewMetricsRepository(db)
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
mailService = mail.NewMailService()
|
mailService = mail.NewMailService()
|
||||||
@@ -178,7 +179,7 @@ func main() {
|
|||||||
healthApiHandler := api.NewHealthApiHandler(db)
|
healthApiHandler := api.NewHealthApiHandler(db)
|
||||||
heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService)
|
heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService)
|
||||||
summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService)
|
summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService)
|
||||||
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService)
|
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService, metricsRepository)
|
||||||
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
|
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
|
||||||
avatarHandler := api.NewAvatarHandler()
|
avatarHandler := api.NewAvatarHandler()
|
||||||
|
|
||||||
@@ -267,18 +268,6 @@ func main() {
|
|||||||
middlewares.NewFileTypeFilterMiddleware([]string{".go"})(staticFileServer),
|
middlewares.NewFileTypeFilterMiddleware([]string{".go"})(staticFileServer),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Miscellaneous
|
|
||||||
// Pre-warm projects cache
|
|
||||||
if !config.IsDev() {
|
|
||||||
allUsers, err := userService.GetAll()
|
|
||||||
if err == nil {
|
|
||||||
logbuch.Info("pre-warming user project cache")
|
|
||||||
for _, u := range allUsers {
|
|
||||||
go heartbeatService.GetEntitySetByUser(models.SummaryProject, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen HTTP
|
// Listen HTTP
|
||||||
listen(router)
|
listen(router)
|
||||||
}
|
}
|
||||||
|
41
migrations/20220318_mysql_timestamp_precision.go
Normal file
41
migrations/20220318_mysql_timestamp_precision.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const name = "20220318-mysql_timestamp_precision"
|
||||||
|
f := migrationFunc{
|
||||||
|
name: name,
|
||||||
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
|
if hasRun(name, db) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Db.IsMySQL() {
|
||||||
|
logbuch.Info("altering heartbeats table, this may take a while (up to hours)")
|
||||||
|
|
||||||
|
db.Exec("SET foreign_key_checks=0;")
|
||||||
|
db.Exec("SET unique_checks=0;")
|
||||||
|
if err := db.Exec("ALTER TABLE heartbeats MODIFY COLUMN `time` TIMESTAMP(3) NOT NULL").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Exec("ALTER TABLE heartbeats MODIFY COLUMN `created_at` TIMESTAMP(3) NOT NULL").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
db.Exec("SET foreign_key_checks=1;")
|
||||||
|
db.Exec("SET unique_checks=1;")
|
||||||
|
|
||||||
|
logbuch.Info("migrated timestamp columns to millisecond precision")
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasRun(name, db)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPostMigration(f)
|
||||||
|
}
|
39
migrations/202203191_drop_diagnostics_user.go
Normal file
39
migrations/202203191_drop_diagnostics_user.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const name = "202203191-drop_diagnostics_user"
|
||||||
|
f := migrationFunc{
|
||||||
|
name: name,
|
||||||
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
|
if hasRun(name, db) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator := db.Migrator()
|
||||||
|
|
||||||
|
if migrator.HasColumn(&models.Diagnostics{}, "user_id") {
|
||||||
|
logbuch.Info("running migration '%s'", name)
|
||||||
|
|
||||||
|
if err := migrator.DropConstraint(&models.Diagnostics{}, "fk_diagnostics_user"); err != nil {
|
||||||
|
logbuch.Warn("failed to drop 'fk_diagnostics_user' constraint (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migrator.DropColumn(&models.Diagnostics{}, "user_id"); err != nil {
|
||||||
|
logbuch.Warn("failed to drop user_id column of diagnostics (%v)", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasRun(name, db)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPostMigration(f)
|
||||||
|
}
|
35
migrations/20220319_add_user_project_idx.go
Normal file
35
migrations/20220319_add_user_project_idx.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const name = "20220319-add_user_project_idx"
|
||||||
|
f := migrationFunc{
|
||||||
|
name: name,
|
||||||
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
|
if hasRun(name, db) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idxName := "idx_user_project"
|
||||||
|
|
||||||
|
if !db.Migrator().HasIndex(&models.Heartbeat{}, idxName) {
|
||||||
|
logbuch.Info("running migration '%s'", name)
|
||||||
|
if err := db.Exec(fmt.Sprintf("create index %s on heartbeats (user_id, project)", idxName)).Error; err != nil {
|
||||||
|
logbuch.Warn("failed to create %s", idxName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasRun(name, db)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPostMigration(f)
|
||||||
|
}
|
@@ -20,8 +20,8 @@ func (m *HeartbeatServiceMock) InsertBatch(heartbeats []*models.Heartbeat) error
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *HeartbeatServiceMock) Count() (int64, error) {
|
func (m *HeartbeatServiceMock) Count(a bool) (int64, error) {
|
||||||
args := m.Called()
|
args := m.Called(a)
|
||||||
return int64(args.Int(0)), args.Error(1)
|
return int64(args.Int(0)), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,8 +2,6 @@ package models
|
|||||||
|
|
||||||
type Diagnostics struct {
|
type Diagnostics struct {
|
||||||
ID uint `gorm:"primary_key"`
|
ID uint `gorm:"primary_key"`
|
||||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
||||||
UserID string `json:"-" gorm:"not null; index:idx_diagnostics_user"`
|
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
Architecture string `json:"architecture"`
|
Architecture string `json:"architecture"`
|
||||||
Plugin string `json:"plugin"`
|
Plugin string `json:"plugin"`
|
||||||
|
@@ -11,11 +11,11 @@ import (
|
|||||||
type Heartbeat struct {
|
type Heartbeat struct {
|
||||||
ID uint64 `gorm:"primary_key" hash:"ignore"`
|
ID uint64 `gorm:"primary_key" hash:"ignore"`
|
||||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
|
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
|
||||||
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
|
UserID string `json:"-" gorm:"not null; index:idx_time_user,idx_user_project"` // idx_user_project is for quickly fetching a user's project list (settings page)
|
||||||
Entity string `json:"entity" gorm:"not null"`
|
Entity string `json:"entity" gorm:"not null"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Project string `json:"project" gorm:"index:idx_project"`
|
Project string `json:"project" gorm:"index:idx_project,idx_user_project"`
|
||||||
Branch string `json:"branch" gorm:"index:idx_branch"`
|
Branch string `json:"branch" gorm:"index:idx_branch"`
|
||||||
Language string `json:"language" gorm:"index:idx_language"`
|
Language string `json:"language" gorm:"index:idx_language"`
|
||||||
IsWrite bool `json:"is_write"`
|
IsWrite bool `json:"is_write"`
|
||||||
@@ -23,11 +23,11 @@ type Heartbeat struct {
|
|||||||
OperatingSystem string `json:"operating_system" gorm:"index:idx_operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
OperatingSystem string `json:"operating_system" gorm:"index:idx_operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
||||||
Machine string `json:"machine" gorm:"index:idx_machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
Machine string `json:"machine" gorm:"index:idx_machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
||||||
UserAgent string `json:"user_agent" hash:"ignore" gorm:"type:varchar(255)"`
|
UserAgent string `json:"user_agent" hash:"ignore" gorm:"type:varchar(255)"`
|
||||||
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
Time CustomTime `json:"time" gorm:"type:timestamp(3); index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
||||||
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
||||||
Origin string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
Origin string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
||||||
OriginId string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
OriginId string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
||||||
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
|
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp(3)" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Heartbeat) Valid() bool {
|
func (h *Heartbeat) Valid() bool {
|
||||||
@@ -36,7 +36,7 @@ func (h *Heartbeat) Valid() bool {
|
|||||||
|
|
||||||
func (h *Heartbeat) Timely(maxAge time.Duration) bool {
|
func (h *Heartbeat) Timely(maxAge time.Duration) bool {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return now.Sub(h.Time.T()) <= maxAge && h.Time.T().Before(now)
|
return now.Sub(h.Time.T()) <= maxAge && h.Time.T().Sub(now) < 1*time.Hour
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Heartbeat) Augment(languageMappings map[string]string) {
|
func (h *Heartbeat) Augment(languageMappings map[string]string) {
|
||||||
|
@@ -4,7 +4,7 @@ import "fmt"
|
|||||||
|
|
||||||
type CounterMetric struct {
|
type CounterMetric struct {
|
||||||
Name string
|
Name string
|
||||||
Value int
|
Value int64
|
||||||
Desc string
|
Desc string
|
||||||
Labels Labels
|
Labels Labels
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
@@ -9,10 +10,11 @@ import (
|
|||||||
|
|
||||||
type HeartbeatRepository struct {
|
type HeartbeatRepository struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
config *conf.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHeartbeatRepository(db *gorm.DB) *HeartbeatRepository {
|
func NewHeartbeatRepository(db *gorm.DB) *HeartbeatRepository {
|
||||||
return &HeartbeatRepository{db: db}
|
return &HeartbeatRepository{config: conf.Get(), db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use with caution!!
|
// Use with caution!!
|
||||||
@@ -116,12 +118,19 @@ func (r *HeartbeatRepository) GetLastByUsers() ([]*models.TimeByUser, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HeartbeatRepository) Count() (int64, error) {
|
func (r *HeartbeatRepository) Count(approximate bool) (count int64, err error) {
|
||||||
var count int64
|
if r.config.Db.IsMySQL() && approximate {
|
||||||
if err := r.db.
|
err = r.db.Table("information_schema.tables").
|
||||||
|
Select("table_rows").
|
||||||
|
Where("table_schema = ?", r.config.Db.Name).
|
||||||
|
Where("table_name = 'heartbeats'").
|
||||||
|
Scan(&count).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
err = r.db.
|
||||||
Model(&models.Heartbeat{}).
|
Model(&models.Heartbeat{}).
|
||||||
Count(&count).Error; err != nil {
|
Count(&count).Error
|
||||||
return 0, err
|
|
||||||
}
|
}
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
@@ -145,6 +154,10 @@ func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.Coun
|
|||||||
userIds[i] = u.ID
|
userIds[i] = u.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(userIds) == 0 {
|
||||||
|
return counts, nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.db.
|
if err := r.db.
|
||||||
Model(&models.Heartbeat{}).
|
Model(&models.Heartbeat{}).
|
||||||
Select("user_id as user, count(id) as count").
|
Select("user_id as user, count(id) as count").
|
||||||
@@ -153,6 +166,7 @@ func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.Coun
|
|||||||
Find(&counts).Error; err != nil {
|
Find(&counts).Error; err != nil {
|
||||||
return counts, err
|
return counts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
43
repositories/metrics.go
Normal file
43
repositories/metrics.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MetricsRepository struct {
|
||||||
|
config *config.Config
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeTplMysql = `
|
||||||
|
SELECT SUM(data_length + index_length)
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = ?
|
||||||
|
GROUP BY table_schema`
|
||||||
|
|
||||||
|
const sizeTplPostgres = `SELECT pg_database_size('%s');`
|
||||||
|
|
||||||
|
const sizeTplSqlite = `
|
||||||
|
SELECT page_count * page_size as size
|
||||||
|
FROM pragma_page_count(), pragma_page_size();`
|
||||||
|
|
||||||
|
func NewMetricsRepository(db *gorm.DB) *MetricsRepository {
|
||||||
|
return &MetricsRepository{config: config.Get(), db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *MetricsRepository) GetDatabaseSize() (size int64, err error) {
|
||||||
|
cfg := srv.config.Db
|
||||||
|
|
||||||
|
query := srv.db.Raw("SELECT 0")
|
||||||
|
if cfg.IsMySQL() {
|
||||||
|
query = srv.db.Raw(sizeTplMysql, cfg.Name)
|
||||||
|
} else if cfg.IsPostgres() {
|
||||||
|
query = srv.db.Raw(sizeTplPostgres, cfg.Name)
|
||||||
|
} else if cfg.IsSQLite() {
|
||||||
|
query = srv.db.Raw(sizeTplSqlite)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.Scan(&size).Error
|
||||||
|
return size, err
|
||||||
|
}
|
@@ -25,7 +25,7 @@ type IHeartbeatRepository interface {
|
|||||||
GetLastByUsers() ([]*models.TimeByUser, error)
|
GetLastByUsers() ([]*models.TimeByUser, error)
|
||||||
GetLatestByUser(*models.User) (*models.Heartbeat, error)
|
GetLatestByUser(*models.User) (*models.Heartbeat, error)
|
||||||
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
||||||
Count() (int64, error)
|
Count(bool) (int64, error)
|
||||||
CountByUser(*models.User) (int64, error)
|
CountByUser(*models.User) (int64, error)
|
||||||
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
||||||
GetEntitySetByUser(uint8, *models.User) ([]string, error)
|
GetEntitySetByUser(uint8, *models.User) ([]string, error)
|
||||||
|
@@ -98,10 +98,9 @@ func (r *UserRepository) GetByLoggedInAfter(t time.Time) ([]*models.User, error)
|
|||||||
// Returns a list of user ids, whose last heartbeat is not older than t
|
// Returns a list of user ids, whose last heartbeat is not older than t
|
||||||
// NOTE: Only ID field will be populated
|
// NOTE: Only ID field will be populated
|
||||||
func (r *UserRepository) GetByLastActiveAfter(t time.Time) ([]*models.User, error) {
|
func (r *UserRepository) GetByLastActiveAfter(t time.Time) ([]*models.User, error) {
|
||||||
subQuery1 := r.db.Model(&models.User{}).
|
subQuery1 := r.db.Model(&models.Heartbeat{}).
|
||||||
Select("users.id as user, max(time) as time").
|
Select("user_id as user, max(time) as time").
|
||||||
Joins("left join heartbeats on users.id = heartbeats.user_id").
|
Group("user_id")
|
||||||
Group("user")
|
|
||||||
|
|
||||||
var userIds []string
|
var userIds []string
|
||||||
if err := r.db.
|
if err := r.db.
|
||||||
|
@@ -46,20 +46,12 @@ func (h *DiagnosticsApiHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
func (h *DiagnosticsApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
func (h *DiagnosticsApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||||
var diagnostics models.Diagnostics
|
var diagnostics models.Diagnostics
|
||||||
|
|
||||||
user := middlewares.GetPrincipal(r)
|
|
||||||
if user == nil {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
w.Write([]byte(conf.ErrUnauthorized))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&diagnostics); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&diagnostics); err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte(conf.ErrBadRequest))
|
w.Write([]byte(conf.ErrBadRequest))
|
||||||
conf.Log().Request(r).Error("failed to parse diagnostics for user %s - %v", err)
|
conf.Log().Request(r).Error("failed to parse diagnostics for user %s - %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
diagnostics.UserID = user.ID
|
|
||||||
|
|
||||||
if _, err := h.diagnosticsSrvc.Create(&diagnostics); err != nil {
|
if _, err := h.diagnosticsSrvc.Create(&diagnostics); err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||||
mm "github.com/muety/wakapi/models/metrics"
|
mm "github.com/muety/wakapi/models/metrics"
|
||||||
|
"github.com/muety/wakapi/repositories"
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -39,6 +40,7 @@ const (
|
|||||||
DescMemAllocTotal = "Total number of bytes allocated for heap"
|
DescMemAllocTotal = "Total number of bytes allocated for heap"
|
||||||
DescMemSysTotal = "Total number of bytes obtained from the OS"
|
DescMemSysTotal = "Total number of bytes obtained from the OS"
|
||||||
DescGoroutines = "Total number of running goroutines"
|
DescGoroutines = "Total number of running goroutines"
|
||||||
|
DescDatabaseSize = "Total database size in bytes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetricsHandler struct {
|
type MetricsHandler struct {
|
||||||
@@ -47,14 +49,16 @@ type MetricsHandler struct {
|
|||||||
summarySrvc services.ISummaryService
|
summarySrvc services.ISummaryService
|
||||||
heartbeatSrvc services.IHeartbeatService
|
heartbeatSrvc services.IHeartbeatService
|
||||||
keyValueSrvc services.IKeyValueService
|
keyValueSrvc services.IKeyValueService
|
||||||
|
metricsRepo *repositories.MetricsRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMetricsHandler(userService services.IUserService, summaryService services.ISummaryService, heartbeatService services.IHeartbeatService, keyValueService services.IKeyValueService) *MetricsHandler {
|
func NewMetricsHandler(userService services.IUserService, summaryService services.ISummaryService, heartbeatService services.IHeartbeatService, keyValueService services.IKeyValueService, metricsRepo *repositories.MetricsRepository) *MetricsHandler {
|
||||||
return &MetricsHandler{
|
return &MetricsHandler{
|
||||||
userSrvc: userService,
|
userSrvc: userService,
|
||||||
summarySrvc: summaryService,
|
summarySrvc: summaryService,
|
||||||
heartbeatSrvc: heartbeatService,
|
heartbeatSrvc: heartbeatService,
|
||||||
keyValueSrvc: keyValueService,
|
keyValueSrvc: keyValueService,
|
||||||
|
metricsRepo: metricsRepo,
|
||||||
config: conf.Get(),
|
config: conf.Get(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,21 +145,21 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_cumulative_seconds_total",
|
Name: MetricsPrefix + "_cumulative_seconds_total",
|
||||||
Desc: DescAllTime,
|
Desc: DescAllTime,
|
||||||
Value: int(v1.NewAllTimeFrom(summaryAllTime).Data.TotalSeconds),
|
Value: int64(v1.NewAllTimeFrom(summaryAllTime).Data.TotalSeconds),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_seconds_total",
|
Name: MetricsPrefix + "_seconds_total",
|
||||||
Desc: DescTotal,
|
Desc: DescTotal,
|
||||||
Value: int(summaryToday.TotalTime().Seconds()),
|
Value: int64(summaryToday.TotalTime().Seconds()),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_heartbeats_total",
|
Name: MetricsPrefix + "_heartbeats_total",
|
||||||
Desc: DescHeartbeats,
|
Desc: DescHeartbeats,
|
||||||
Value: int(heartbeatCount),
|
Value: int64(heartbeatCount),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -163,7 +167,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_project_seconds_total",
|
Name: MetricsPrefix + "_project_seconds_total",
|
||||||
Desc: DescProjects,
|
Desc: DescProjects,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryProject, p.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryProject, p.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: p.Key}},
|
Labels: []mm.Label{{Key: "name", Value: p.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -172,7 +176,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_language_seconds_total",
|
Name: MetricsPrefix + "_language_seconds_total",
|
||||||
Desc: DescLanguages,
|
Desc: DescLanguages,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryLanguage, l.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryLanguage, l.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: l.Key}},
|
Labels: []mm.Label{{Key: "name", Value: l.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -181,7 +185,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_editor_seconds_total",
|
Name: MetricsPrefix + "_editor_seconds_total",
|
||||||
Desc: DescEditors,
|
Desc: DescEditors,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryEditor, e.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryEditor, e.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: e.Key}},
|
Labels: []mm.Label{{Key: "name", Value: e.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -190,7 +194,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_operating_system_seconds_total",
|
Name: MetricsPrefix + "_operating_system_seconds_total",
|
||||||
Desc: DescOperatingSystems,
|
Desc: DescOperatingSystems,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryOS, o.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryOS, o.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: o.Key}},
|
Labels: []mm.Label{{Key: "name", Value: o.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -199,7 +203,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_machine_seconds_total",
|
Name: MetricsPrefix + "_machine_seconds_total",
|
||||||
Desc: DescMachines,
|
Desc: DescMachines,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryMachine, m.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryMachine, m.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -208,7 +212,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_label_seconds_total",
|
Name: MetricsPrefix + "_label_seconds_total",
|
||||||
Desc: DescLabels,
|
Desc: DescLabels,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryLabel, m.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryLabel, m.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -220,21 +224,34 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_goroutines_total",
|
Name: MetricsPrefix + "_goroutines_total",
|
||||||
Desc: DescGoroutines,
|
Desc: DescGoroutines,
|
||||||
Value: runtime.NumGoroutine(),
|
Value: int64(runtime.NumGoroutine()),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_mem_alloc_total",
|
Name: MetricsPrefix + "_mem_alloc_total",
|
||||||
Desc: DescMemAllocTotal,
|
Desc: DescMemAllocTotal,
|
||||||
Value: int(memStats.Alloc),
|
Value: int64(memStats.Alloc),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_mem_sys_total",
|
Name: MetricsPrefix + "_mem_sys_total",
|
||||||
Desc: DescMemSysTotal,
|
Desc: DescMemSysTotal,
|
||||||
Value: int(memStats.Sys),
|
Value: int64(memStats.Sys),
|
||||||
|
Labels: []mm.Label{},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Database metrics
|
||||||
|
dbSize, err := h.metricsRepo.GetDatabaseSize()
|
||||||
|
if err != nil {
|
||||||
|
logbuch.Warn("failed to get database size (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
|
Name: MetricsPrefix + "_db_total_bytes",
|
||||||
|
Desc: DescDatabaseSize,
|
||||||
|
Value: dbSize,
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -256,7 +273,7 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
totalUsers, _ := h.userSrvc.Count()
|
totalUsers, _ := h.userSrvc.Count()
|
||||||
totalHeartbeats, _ := h.heartbeatSrvc.Count()
|
totalHeartbeats, _ := h.heartbeatSrvc.Count(true)
|
||||||
|
|
||||||
activeUsers, err := h.userSrvc.GetActive(false)
|
activeUsers, err := h.userSrvc.GetActive(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -267,28 +284,28 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_seconds_total",
|
Name: MetricsPrefix + "_admin_seconds_total",
|
||||||
Desc: DescAdminTotalTime,
|
Desc: DescAdminTotalTime,
|
||||||
Value: totalSeconds,
|
Value: int64(totalSeconds),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_heartbeats_total",
|
Name: MetricsPrefix + "_admin_heartbeats_total",
|
||||||
Desc: DescAdminTotalHeartbeats,
|
Desc: DescAdminTotalHeartbeats,
|
||||||
Value: int(totalHeartbeats),
|
Value: totalHeartbeats,
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_users_total",
|
Name: MetricsPrefix + "_admin_users_total",
|
||||||
Desc: DescAdminTotalUsers,
|
Desc: DescAdminTotalUsers,
|
||||||
Value: int(totalUsers),
|
Value: totalUsers,
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_users_active_total",
|
Name: MetricsPrefix + "_admin_users_active_total",
|
||||||
Desc: DescAdminActiveUsers,
|
Desc: DescAdminActiveUsers,
|
||||||
Value: len(activeUsers),
|
Value: int64(len(activeUsers)),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -304,7 +321,7 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_user_heartbeats_total",
|
Name: MetricsPrefix + "_admin_user_heartbeats_total",
|
||||||
Desc: DescAdminUserHeartbeats,
|
Desc: DescAdminUserHeartbeats,
|
||||||
Value: int(uc.Count),
|
Value: uc.Count,
|
||||||
Labels: []mm.Label{{Key: "user", Value: uc.User}},
|
Labels: []mm.Label{{Key: "user", Value: uc.User}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
9
scripts/aggregate_durations.sql
Normal file
9
scripts/aggregate_durations.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
SELECT project, language, editor, operating_system, machine, branch, SUM(GREATEST(1, diff)) as 'sum'
|
||||||
|
FROM (
|
||||||
|
SELECT project, language, editor, operating_system, machine, branch, TIME_TO_SEC(LEAST(TIMEDIFF(time, LAG(time) over w), '00:02:00')) as 'diff'
|
||||||
|
FROM heartbeats
|
||||||
|
WHERE user_id = 'n1try'
|
||||||
|
WINDOW w AS (ORDER BY time)
|
||||||
|
) s2
|
||||||
|
WHERE diff IS NOT NULL
|
||||||
|
GROUP BY project, language, editor, operating_system, machine, branch;
|
12
scripts/clean_duplicates.sql
Normal file
12
scripts/clean_duplicates.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
DELETE t1
|
||||||
|
FROM heartbeats t1
|
||||||
|
INNER JOIN heartbeats t2
|
||||||
|
WHERE t1.id < t2.id
|
||||||
|
AND t1.time = t2.time
|
||||||
|
AND t1.entity = t2.entity
|
||||||
|
AND t1.is_write = t2.is_write
|
||||||
|
AND t1.branch = t2.branch
|
||||||
|
AND t1.editor = t2.editor
|
||||||
|
AND t1.machine = t2.machine
|
||||||
|
AND t1.operating_system = t2.operating_system
|
||||||
|
AND t1.user_id = t2.user_id;
|
10
scripts/count_duplicates_by_user.sql
Normal file
10
scripts/count_duplicates_by_user.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
SELECT s2.user_id, sum(c) as count, total, (sum(c) / total) as ratio
|
||||||
|
FROM (
|
||||||
|
SELECT time, user_id, entity, is_write, branch, editor, machine, operating_system, COUNT(time) as c
|
||||||
|
FROM heartbeats
|
||||||
|
GROUP BY time, user_id, entity, is_write, branch, editor, machine, operating_system
|
||||||
|
HAVING COUNT(time) > 1
|
||||||
|
) s2
|
||||||
|
LEFT JOIN (SELECT user_id, count(id) AS total FROM heartbeats GROUP BY user_id) s3 ON s2.user_id = s3.user_id
|
||||||
|
GROUP BY user_id
|
||||||
|
ORDER BY count DESC;
|
@@ -36,6 +36,8 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Aggregation
|
// Aggregation
|
||||||
|
// the below logic is approximately equivalent to the SQL query at scripts/aggregate_durations.sql,
|
||||||
|
// but unfortunately we cannot use it, as it features mysql-specific functions (lag(), timediff(), ...)
|
||||||
var count int
|
var count int
|
||||||
var latest *models.Duration
|
var latest *models.Duration
|
||||||
|
|
||||||
@@ -80,12 +82,20 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *
|
|||||||
|
|
||||||
for _, list := range mapping {
|
for _, list := range mapping {
|
||||||
for _, d := range list {
|
for _, d := range list {
|
||||||
|
// will only happen if two heartbeats with different hashes (e.g. different project) have the same timestamp
|
||||||
|
// that, in turn, will most likely only happen for mysql, where `time` column's precision was set to second for a while
|
||||||
|
// assume that two non-identical heartbeats with identical time are sub-second apart from each other, so round up to expectancy value
|
||||||
|
// also see https://github.com/muety/wakapi/issues/340
|
||||||
if d.Duration == 0 {
|
if d.Duration == 0 {
|
||||||
d.Duration = HeartbeatDiffThreshold
|
d.Duration = 500 * time.Millisecond
|
||||||
}
|
}
|
||||||
durations = append(durations, d)
|
durations = append(durations, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(heartbeats) == 1 && len(durations) == 1 {
|
||||||
|
durations[0].Duration = HeartbeatDiffThreshold
|
||||||
|
}
|
||||||
|
|
||||||
return durations.Sorted(), nil
|
return durations.Sorted(), nil
|
||||||
}
|
}
|
||||||
|
@@ -65,6 +65,17 @@ func (suite *DurationServiceTestSuite) SetupSuite() {
|
|||||||
Machine: TestMachine1,
|
Machine: TestMachine1,
|
||||||
Time: models.CustomTime(suite.TestStartTime.Add(30 * time.Second)), // 0:30
|
Time: models.CustomTime(suite.TestStartTime.Add(30 * time.Second)), // 0:30
|
||||||
},
|
},
|
||||||
|
// duplicate of previous one
|
||||||
|
{
|
||||||
|
ID: rand.Uint64(),
|
||||||
|
UserID: TestUserId,
|
||||||
|
Project: TestProject1,
|
||||||
|
Language: TestLanguageGo,
|
||||||
|
Editor: TestEditorGoland,
|
||||||
|
OperatingSystem: TestOsLinux,
|
||||||
|
Machine: TestMachine1,
|
||||||
|
Time: models.CustomTime(suite.TestStartTime.Add(30 * time.Second)), // 0:30
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ID: rand.Uint64(),
|
ID: rand.Uint64(),
|
||||||
UserID: TestUserId,
|
UserID: TestUserId,
|
||||||
@@ -160,7 +171,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
|
|||||||
assert.Equal(suite.T(), TestEditorGoland, durations[0].Editor)
|
assert.Equal(suite.T(), TestEditorGoland, durations[0].Editor)
|
||||||
assert.Equal(suite.T(), TestEditorGoland, durations[1].Editor)
|
assert.Equal(suite.T(), TestEditorGoland, durations[1].Editor)
|
||||||
assert.Equal(suite.T(), TestEditorVscode, durations[2].Editor)
|
assert.Equal(suite.T(), TestEditorVscode, durations[2].Editor)
|
||||||
assert.Equal(suite.T(), 2, durations[0].NumHeartbeats)
|
assert.Equal(suite.T(), 3, durations[0].NumHeartbeats)
|
||||||
assert.Equal(suite.T(), 1, durations[1].NumHeartbeats)
|
assert.Equal(suite.T(), 1, durations[1].NumHeartbeats)
|
||||||
assert.Equal(suite.T(), 3, durations[2].NumHeartbeats)
|
assert.Equal(suite.T(), 3, durations[2].NumHeartbeats)
|
||||||
}
|
}
|
||||||
|
@@ -73,12 +73,12 @@ func (srv *HeartbeatService) InsertBatch(heartbeats []*models.Heartbeat) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *HeartbeatService) Count() (int64, error) {
|
func (srv *HeartbeatService) Count(approximate bool) (int64, error) {
|
||||||
result, ok := srv.cache.Get(srv.countTotalCacheKey())
|
result, ok := srv.cache.Get(srv.countTotalCacheKey())
|
||||||
if ok {
|
if ok {
|
||||||
return result.(int64), nil
|
return result.(int64), nil
|
||||||
}
|
}
|
||||||
count, err := srv.repository.Count()
|
count, err := srv.repository.Count(approximate)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
srv.cache.Set(srv.countTotalCacheKey(), count, srv.countCacheTtl())
|
srv.cache.Set(srv.countTotalCacheKey(), count, srv.countCacheTtl())
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@ type IAliasService interface {
|
|||||||
type IHeartbeatService interface {
|
type IHeartbeatService interface {
|
||||||
Insert(*models.Heartbeat) error
|
Insert(*models.Heartbeat) error
|
||||||
InsertBatch([]*models.Heartbeat) error
|
InsertBatch([]*models.Heartbeat) error
|
||||||
Count() (int64, error)
|
Count(bool) (int64, error)
|
||||||
CountByUser(*models.User) (int64, error)
|
CountByUser(*models.User) (int64, error)
|
||||||
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
||||||
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
||||||
|
@@ -1 +1 @@
|
|||||||
2.3.0
|
2.3.1
|
Reference in New Issue
Block a user