mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Fix heartbeats insertion.
Add OS and editor fields. Introduce aggregations (not working yet).
This commit is contained in:
parent
3bc706ed4b
commit
d1be4ce2a3
6
main.go
6
main.go
@ -59,6 +59,7 @@ func main() {
|
|||||||
Addr: config.DbHost,
|
Addr: config.DbHost,
|
||||||
DBName: config.DbName,
|
DBName: config.DbName,
|
||||||
AllowNativePasswords: true,
|
AllowNativePasswords: true,
|
||||||
|
ParseTime: true,
|
||||||
}
|
}
|
||||||
db, _ := sql.Open("mysql", dbConfig.FormatDSN())
|
db, _ := sql.Open("mysql", dbConfig.FormatDSN())
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
@ -70,9 +71,11 @@ func main() {
|
|||||||
// Services
|
// Services
|
||||||
heartbeatSrvc := &services.HeartbeatService{db}
|
heartbeatSrvc := &services.HeartbeatService{db}
|
||||||
userSrvc := &services.UserService{db}
|
userSrvc := &services.UserService{db}
|
||||||
|
aggregationSrvc := &services.AggregationService{db, heartbeatSrvc}
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
heartbeatHandler := &routes.HeartbeatHandler{HeartbeatSrvc: heartbeatSrvc}
|
heartbeatHandler := &routes.HeartbeatHandler{HeartbeatSrvc: heartbeatSrvc}
|
||||||
|
aggregationHandler := &routes.AggregationHandler{AggregationSrvc: aggregationSrvc}
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
authenticate := &middlewares.AuthenticateMiddleware{UserSrvc: userSrvc}
|
authenticate := &middlewares.AuthenticateMiddleware{UserSrvc: userSrvc}
|
||||||
@ -85,6 +88,9 @@ func main() {
|
|||||||
heartbeats := apiRouter.Path("/heartbeat").Subrouter()
|
heartbeats := apiRouter.Path("/heartbeat").Subrouter()
|
||||||
heartbeats.Methods("POST").HandlerFunc(heartbeatHandler.Post)
|
heartbeats.Methods("POST").HandlerFunc(heartbeatHandler.Post)
|
||||||
|
|
||||||
|
aggreagations := apiRouter.Path("/aggregation").Subrouter()
|
||||||
|
aggreagations.Methods("GET").HandlerFunc(aggregationHandler.Get)
|
||||||
|
|
||||||
// Sub-Routes Setup
|
// Sub-Routes Setup
|
||||||
router.PathPrefix("/api").Handler(negroni.Classic().With(
|
router.PathPrefix("/api").Handler(negroni.Classic().With(
|
||||||
negroni.HandlerFunc(authenticate.Handle),
|
negroni.HandlerFunc(authenticate.Handle),
|
||||||
|
24
models/aggregation.go
Normal file
24
models/aggregation.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type AggregationType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AggregationProject AggregationType = "project"
|
||||||
|
AggregationLanguage AggregationType = "language"
|
||||||
|
AggregationEditor AggregationType = "editor"
|
||||||
|
AggregationOS AggregationType = "os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Aggregation struct {
|
||||||
|
From time.Time
|
||||||
|
To time.Time
|
||||||
|
Type AggregationType
|
||||||
|
Items []AggregationItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type AggregationItem struct {
|
||||||
|
Key string
|
||||||
|
Total time.Duration
|
||||||
|
}
|
@ -9,15 +9,17 @@ import (
|
|||||||
type HeartbeatReqTime time.Time
|
type HeartbeatReqTime time.Time
|
||||||
|
|
||||||
type Heartbeat struct {
|
type Heartbeat struct {
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
Entity string `json:"entity"`
|
Entity string `json:"entity"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project"`
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
IsWrite bool `json:"is_write"`
|
IsWrite bool `json:"is_write"`
|
||||||
Time HeartbeatReqTime `json:"time"`
|
Editor string `json:"editor"`
|
||||||
|
OperatingSystem string `json:"operating_system"`
|
||||||
|
Time HeartbeatReqTime `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *HeartbeatReqTime) UnmarshalJSON(b []byte) error {
|
func (j *HeartbeatReqTime) UnmarshalJSON(b []byte) error {
|
||||||
|
39
routes/aggregation.go
Normal file
39
routes/aggregation.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/n1try/wakapi/models"
|
||||||
|
"github.com/n1try/wakapi/services"
|
||||||
|
"github.com/n1try/wakapi/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AggregationHandler struct {
|
||||||
|
AggregationSrvc *services.AggregationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AggregationHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
w.WriteHeader(415)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
|
params := r.URL.Query()
|
||||||
|
from, err := utils.ParseDate(params.Get("from"))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
w.Write([]byte("Missing 'from' parameter"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
to, err := utils.ParseDate(params.Get("to"))
|
||||||
|
if err != nil {
|
||||||
|
to = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
h.AggregationSrvc.Aggregate(from, to, user)
|
||||||
|
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/n1try/wakapi/services"
|
"github.com/n1try/wakapi/services"
|
||||||
|
"github.com/n1try/wakapi/utils"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
"github.com/n1try/wakapi/models"
|
"github.com/n1try/wakapi/models"
|
||||||
@ -20,14 +21,21 @@ func (h *HeartbeatHandler) Post(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(415)
|
w.WriteHeader(415)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent"))
|
||||||
|
|
||||||
dec := json.NewDecoder(r.Body)
|
dec := json.NewDecoder(r.Body)
|
||||||
var heartbeats []models.Heartbeat
|
var heartbeats []*models.Heartbeat
|
||||||
err := dec.Decode(&heartbeats)
|
err := dec.Decode(&heartbeats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(400)
|
w.WriteHeader(400)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
for _, h := range heartbeats {
|
||||||
|
h.OperatingSystem = opSys
|
||||||
|
h.Editor = editor
|
||||||
|
}
|
||||||
|
|
||||||
user := r.Context().Value(models.UserKey).(*models.User)
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
err = h.HeartbeatSrvc.InsertBatch(heartbeats, user)
|
err = h.HeartbeatSrvc.InsertBatch(heartbeats, user)
|
||||||
@ -37,5 +45,5 @@ func (h *HeartbeatHandler) Post(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(201)
|
w.WriteHeader(200)
|
||||||
}
|
}
|
||||||
|
29
services/aggregation.go
Normal file
29
services/aggregation.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/n1try/wakapi/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AggregationService struct {
|
||||||
|
Db *sql.DB
|
||||||
|
HeartbeatService *HeartbeatService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AggregationService) Aggregate(from time.Time, to time.Time, user *models.User) {
|
||||||
|
heartbeats, err := srv.HeartbeatService.GetAllFrom(from, user)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, h := range heartbeats {
|
||||||
|
fmt.Printf("%+v\n", h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AggregationService) aggregateBy(*[]models.Heartbeat, models.AggregationType) *models.Aggregation {
|
||||||
|
return &models.Aggregation{}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/n1try/wakapi/models"
|
"github.com/n1try/wakapi/models"
|
||||||
)
|
)
|
||||||
@ -14,14 +15,14 @@ type HeartbeatService struct {
|
|||||||
Db *sql.DB
|
Db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *HeartbeatService) InsertBatch(heartbeats []models.Heartbeat, user *models.User) error {
|
func (srv *HeartbeatService) InsertBatch(heartbeats []*models.Heartbeat, user *models.User) error {
|
||||||
qTpl := "INSERT INTO %+s (user, time, entity, type, category, is_write, project, branch, language) VALUES %+s;"
|
qTpl := "INSERT INTO %+s (user, time, entity, type, category, is_write, project, branch, language, operating_system, editor) VALUES %+s;"
|
||||||
qFill := ""
|
qFill := ""
|
||||||
vals := []interface{}{}
|
vals := []interface{}{}
|
||||||
|
|
||||||
for _, h := range heartbeats {
|
for _, h := range heartbeats {
|
||||||
qFill = "(?, ?, ?, ?, ?, ?, ?, ?, ?),"
|
qFill = qFill + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),"
|
||||||
vals = append(vals, user.UserId, h.Time.String(), h.Entity, h.Type, h.Category, h.IsWrite, h.Project, h.Branch, h.Language)
|
vals = append(vals, user.UserId, h.Time.String(), h.Entity, h.Type, h.Category, h.IsWrite, h.Project, h.Branch, h.Language, h.OperatingSystem, h.Editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := fmt.Sprintf(qTpl, TableHeartbeat, qFill[0:len(qFill)-1])
|
q := fmt.Sprintf(qTpl, TableHeartbeat, qFill[0:len(qFill)-1])
|
||||||
@ -36,3 +37,42 @@ func (srv *HeartbeatService) InsertBatch(heartbeats []models.Heartbeat, user *mo
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *HeartbeatService) GetAllFrom(date time.Time, user *models.User) ([]models.Heartbeat, error) {
|
||||||
|
q := fmt.Sprintf("SELECT user, time, language, project, operating_system, editor FROM %+s WHERE time >= ? AND user = ?", TableHeartbeat)
|
||||||
|
rows, err := srv.Db.Query(q, date.String(), user.UserId)
|
||||||
|
defer rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
return make([]models.Heartbeat, 0), err
|
||||||
|
}
|
||||||
|
|
||||||
|
var heartbeats []models.Heartbeat
|
||||||
|
for rows.Next() {
|
||||||
|
var h models.Heartbeat
|
||||||
|
var language sql.NullString
|
||||||
|
var project sql.NullString
|
||||||
|
var operatingSystem sql.NullString
|
||||||
|
var editor sql.NullString
|
||||||
|
|
||||||
|
err := rows.Scan(&h.User, &h.Time, &language, &project, &operatingSystem, &editor)
|
||||||
|
|
||||||
|
if language.Valid {
|
||||||
|
h.Language = language.String
|
||||||
|
}
|
||||||
|
if project.Valid {
|
||||||
|
h.Project = project.String
|
||||||
|
}
|
||||||
|
if operatingSystem.Valid {
|
||||||
|
h.OperatingSystem = operatingSystem.String
|
||||||
|
}
|
||||||
|
if editor.Valid {
|
||||||
|
h.Editor = editor.String
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return make([]models.Heartbeat, 0), err
|
||||||
|
}
|
||||||
|
heartbeats = append(heartbeats, h)
|
||||||
|
}
|
||||||
|
return heartbeats, nil
|
||||||
|
}
|
||||||
|
24
utils/common.go
Normal file
24
utils/common.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseDate(date string) (time.Time, error) {
|
||||||
|
return time.Parse("2006-01-02 15:04:05", date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatDate(date time.Time) string {
|
||||||
|
return date.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseUserAgent(ua string) (string, string, error) {
|
||||||
|
re := regexp.MustCompile(`^wakatime\/[\d+.]+\s\((\w+).*\)\s.+\s(\w+)\/.+$`)
|
||||||
|
groups := re.FindAllStringSubmatch(ua, -1)
|
||||||
|
if len(groups) == 0 || len(groups[0]) != 3 {
|
||||||
|
return "", "", errors.New("Failed to parse user agent string")
|
||||||
|
}
|
||||||
|
return groups[0][1], groups[0][2], nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user