Add prometheus endpoint for getting metrics (#33)
All checks were successful
the build was successful
All checks were successful
the build was successful
This commit is contained in:
parent
ee398b5272
commit
e047673c6b
@ -9,6 +9,9 @@ service:
|
||||
frontendurl: ""
|
||||
# The number of items which gets returned per page
|
||||
pagecount: 50
|
||||
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
|
||||
# You'll need to use redis for this in order to enable common metrics over multiple nodes
|
||||
enablemetrics: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql and sqlite.
|
||||
@ -25,18 +28,26 @@ database:
|
||||
Path: "./vikunja.db"
|
||||
# Whether to show mysql queries or not. Useful for debugging.
|
||||
showqueries: "false"
|
||||
# Sets the max open connections to the database. Only used when using mysql.
|
||||
openconnections: 100
|
||||
|
||||
redis:
|
||||
# Whether to enable redis or not
|
||||
enabled: false
|
||||
# The host of the redis server including its port.
|
||||
redishost: 'localhost:6379'
|
||||
# The password used to authenicate against the redis server
|
||||
redispassword: ''
|
||||
# 0 means default database
|
||||
db: 0
|
||||
|
||||
cache:
|
||||
# If cache is enabled or not
|
||||
enabled: false
|
||||
# Cache type. Possible values are memory or redis
|
||||
# Cache type. Possible values are memory or redis, you'll need to enable redis below when using redis
|
||||
type: memory
|
||||
# When using memory this defines the maximum size an element can take
|
||||
maxelementsize: 1000
|
||||
# When using redis, this is the host of the redis server including its port.
|
||||
redishost: 'localhost:6379'
|
||||
# When using redis, this is the password used to authenicate against the redis server
|
||||
redispassword: ''
|
||||
|
||||
mailer:
|
||||
# SMTP Host
|
||||
|
@ -35,6 +35,9 @@ service:
|
||||
rootpath: <the path of the executable>
|
||||
# The number of items which gets returned per page
|
||||
pagecount: 50
|
||||
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
|
||||
# You'll need to use redis for this in order to enable common metrics over multiple nodes
|
||||
enablemetrics: false
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql and sqlite.
|
||||
@ -54,18 +57,23 @@ database:
|
||||
# Sets the max open connections to the database. Only used when using mysql.
|
||||
openconnections: 100
|
||||
|
||||
|
||||
cache:
|
||||
# If cache is enabled or not
|
||||
enabled: false
|
||||
# Cache type. Possible values are memory or redis
|
||||
# Cache type. Possible values are memory or redis, you'll need to enable redis below when using redis
|
||||
type: memory
|
||||
# When using memory this defines the maximum size an element can take
|
||||
maxelementsize: 1000
|
||||
# When using redis, this is the host of the redis server including its port.
|
||||
|
||||
redis:
|
||||
# Whether to enable redis or not
|
||||
enabled: false
|
||||
# The host of the redis server including its port.
|
||||
redishost: 'localhost:6379'
|
||||
# When using redis, this is the password used to authenicate against the redis server
|
||||
# The password used to authenicate against the redis server
|
||||
redispassword: ''
|
||||
# 0 means default database
|
||||
db: 0
|
||||
|
||||
mailer:
|
||||
# SMTP Host
|
||||
|
2
go.mod
2
go.mod
@ -30,6 +30,7 @@ require (
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-openapi/spec v0.17.2 // indirect
|
||||
github.com/go-openapi/swag v0.17.2 // indirect
|
||||
github.com/go-redis/redis v6.14.2+incompatible
|
||||
github.com/go-sql-driver/mysql v0.0.0-20171007150158-ee359f95877b
|
||||
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 // indirect
|
||||
github.com/go-xorm/core v0.5.8
|
||||
@ -49,6 +50,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pkg/errors v0.8.0 // indirect
|
||||
github.com/prometheus/client_golang v0.9.2
|
||||
github.com/spf13/viper v1.2.0
|
||||
github.com/stretchr/testify v1.2.2
|
||||
github.com/swaggo/echo-swagger v0.0.0-20180315045949-97f46bb9e5a5
|
||||
|
19
go.sum
19
go.sum
@ -14,6 +14,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5Vpd
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -39,6 +41,8 @@ github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsd
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.17.2 h1:K/ycE/XTUDFltNHSO32cGRUhrVGJD64o8WgAIZNyc3k=
|
||||
github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-redis/redis v6.14.2+incompatible h1:UE9pLhzmWf+xHNmZsoccjXosPicuiNaInPgym8nzfg0=
|
||||
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-sql-driver/mysql v0.0.0-20171007150158-ee359f95877b h1:/CMGgAYard7jx9+bI7tUIqafFDR7Pv2BRu2Tb5dDaqM=
|
||||
github.com/go-sql-driver/mysql v0.0.0-20171007150158-ee359f95877b/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 h1:jUX9yw6+iKrs/WuysV2M6ap/ObK/07SE/a7I2uxitwM=
|
||||
@ -51,6 +55,8 @@ github.com/go-xorm/xorm v0.0.0-20170930012613-29d4a0330a00 h1:jlA1XEj8QHl6my6FUk
|
||||
github.com/go-xorm/xorm v0.0.0-20170930012613-29d4a0330a00/go.mod h1:i7qRPD38xj/v75UV+a9pEzr5tfRaH2ndJfwt/fGbQhs=
|
||||
github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2 h1:57QbyUkFcFjipHJQstYR5owRxsQzgD8/OAO/hr4yl/E=
|
||||
github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2/go.mod h1:xxK9FGkFXrau9/vGdDYSOyQfSgKXBV7iHXpQfNuv6B0=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY=
|
||||
@ -84,6 +90,8 @@ github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5 h1:+IPgoz43mdEYG5lrq
|
||||
github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8=
|
||||
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
|
||||
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
@ -96,6 +104,14 @@ github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
|
||||
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
|
||||
@ -133,6 +149,9 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
|
||||
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
15
main.go
15
main.go
@ -18,12 +18,11 @@ package main
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/docs"
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
_ "code.vikunja.io/api/pkg/config" // To trigger its init() which initializes the config
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/routes"
|
||||
|
||||
"context"
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
@ -39,18 +38,10 @@ func main() {
|
||||
// Init logging
|
||||
log.InitLogger()
|
||||
|
||||
// Init Config
|
||||
err := config.InitConfig()
|
||||
if err != nil {
|
||||
log.Log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Set Engine
|
||||
err = models.SetEngine()
|
||||
err := models.SetEngine()
|
||||
if err != nil {
|
||||
log.Log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
log.Log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// Start the mail daemon
|
||||
|
@ -17,6 +17,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
@ -26,13 +27,13 @@ import (
|
||||
)
|
||||
|
||||
// InitConfig initializes the config, sets defaults etc.
|
||||
func InitConfig() (err error) {
|
||||
func init() {
|
||||
|
||||
// Set defaults
|
||||
// Service config
|
||||
random, err := random(32)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// Service
|
||||
@ -46,6 +47,7 @@ func InitConfig() (err error) {
|
||||
exPath := filepath.Dir(ex)
|
||||
viper.SetDefault("service.rootpath", exPath)
|
||||
viper.SetDefault("service.pagecount", 50)
|
||||
viper.SetDefault("service.enablemetrics", false)
|
||||
// Database
|
||||
viper.SetDefault("database.type", "sqlite")
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
@ -59,8 +61,6 @@ func InitConfig() (err error) {
|
||||
viper.SetDefault("cache.enabled", false)
|
||||
viper.SetDefault("cache.type", "memory")
|
||||
viper.SetDefault("cache.maxelementsize", 1000)
|
||||
viper.SetDefault("cache.redishost", "localhost:6379")
|
||||
viper.SetDefault("cache.redispassword", "")
|
||||
// Mailer
|
||||
viper.SetDefault("mailer.host", "")
|
||||
viper.SetDefault("mailer.port", "587")
|
||||
@ -70,6 +70,11 @@ func InitConfig() (err error) {
|
||||
viper.SetDefault("mailer.fromemail", "mail@vikunja")
|
||||
viper.SetDefault("mailer.queuelength", 100)
|
||||
viper.SetDefault("mailer.queuetimeout", 30)
|
||||
// Redis
|
||||
viper.SetDefault("redis.enabled", false)
|
||||
viper.SetDefault("redis.host", "localhost:6379")
|
||||
viper.SetDefault("redis.password", "")
|
||||
viper.SetDefault("redis.db", 0)
|
||||
|
||||
// Init checking for environment variables
|
||||
viper.SetEnvPrefix("vikunja")
|
||||
@ -81,11 +86,9 @@ func InitConfig() (err error) {
|
||||
viper.SetConfigName("config")
|
||||
err = viper.ReadInConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Using defaults.")
|
||||
log.Log.Info(err)
|
||||
log.Log.Info("Using defaults.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func random(length int) (string, error) {
|
||||
|
92
pkg/metrics/active_users.go
Normal file
92
pkg/metrics/active_users.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"encoding/gob"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SecondsUntilInactive defines the seconds until a user is considered inactive
|
||||
const SecondsUntilInactive = 60
|
||||
|
||||
// ActiveUsersKey is the key used to store active users in redis
|
||||
const ActiveUsersKey = `activeusers`
|
||||
|
||||
// ActiveUser defines an active user
|
||||
type ActiveUser struct {
|
||||
UserID int64
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
func init() {
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_active_users",
|
||||
Help: "The currently active users on this node",
|
||||
}, func() float64 {
|
||||
|
||||
allActiveUsers, err := GetActiveUsers()
|
||||
if err != nil {
|
||||
log.Log.Error(err.Error())
|
||||
}
|
||||
activeUsersCount := 0
|
||||
for _, u := range allActiveUsers {
|
||||
if time.Since(u.LastSeen) < SecondsUntilInactive*time.Second {
|
||||
activeUsersCount++
|
||||
}
|
||||
}
|
||||
return float64(activeUsersCount)
|
||||
})
|
||||
}
|
||||
|
||||
// GetActiveUsers returns the active users from redis
|
||||
func GetActiveUsers() (users []*ActiveUser, err error) {
|
||||
|
||||
activeUsersR, err := r.Get(ActiveUsersKey).Bytes()
|
||||
if err != nil {
|
||||
if err.Error() == "redis: nil" {
|
||||
return users, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
_, err = b.Write(activeUsersR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := gob.NewDecoder(&b)
|
||||
if err := d.Decode(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetActiveUsers sets the active users from redis
|
||||
func SetActiveUsers(users []*ActiveUser) (err error) {
|
||||
var b bytes.Buffer
|
||||
e := gob.NewEncoder(&b)
|
||||
if err := e.Encode(users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Set(ActiveUsersKey, b.Bytes(), 0).Err()
|
||||
}
|
123
pkg/metrics/metrics.go
Normal file
123
pkg/metrics/metrics.go
Normal file
@ -0,0 +1,123 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/red"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var r = red.GetRedis()
|
||||
|
||||
const (
|
||||
// ListCountKey is the name of the key in which we save the list count
|
||||
ListCountKey = `listcount`
|
||||
|
||||
// UserCountKey is the name of the key we use to store total users in redis
|
||||
UserCountKey = `usercount`
|
||||
|
||||
// NamespaceCountKey is the name of the key we use to store the amount of total namespaces in redis
|
||||
NamespaceCountKey = `namespacecount`
|
||||
|
||||
// TaskCountKey is the name of the key we use to store the amount of total tasks in redis
|
||||
TaskCountKey = `taskcount`
|
||||
|
||||
// TeamCountKey is the name of the key we use to store the amount of total teams in redis
|
||||
TeamCountKey = `teamcount`
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register total list count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_list_count",
|
||||
Help: "The number of lists on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(ListCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
|
||||
// Register total user count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_user_count",
|
||||
Help: "The total number of users on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(UserCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
|
||||
// Register total Namespaces count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_namespcae_count",
|
||||
Help: "The total number of namespaces on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(NamespaceCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
|
||||
// Register total Tasks count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_task_count",
|
||||
Help: "The total number of tasks on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(TaskCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
|
||||
// Register total user count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_team_count",
|
||||
Help: "The total number of teams on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(TeamCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
}
|
||||
|
||||
// GetCount returns the current count from redis
|
||||
func GetCount(key string) (count int64, err error) {
|
||||
count, err = r.Get(key).Int64()
|
||||
if err != nil && err.Error() != "redis: nil" {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetCount sets the list count to a given value
|
||||
func SetCount(count int64, key string) error {
|
||||
return r.Set(key, count, 0).Err()
|
||||
}
|
||||
|
||||
// UpdateCount updates a count with a given amount
|
||||
func UpdateCount(update int64, key string) {
|
||||
if !viper.GetBool("service.enablemetrics") {
|
||||
return
|
||||
}
|
||||
oldtotal, err := GetCount(key)
|
||||
if err != nil {
|
||||
log.Log.Error(err.Error())
|
||||
}
|
||||
|
||||
err = SetCount(oldtotal+update, key)
|
||||
if err != nil {
|
||||
log.Log.Error(err.Error())
|
||||
}
|
||||
}
|
@ -16,7 +16,10 @@
|
||||
|
||||
package models
|
||||
|
||||
import "code.vikunja.io/web"
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
// CreateOrUpdateList updates a list or creates it if it doesn't exist
|
||||
func CreateOrUpdateList(list *List) (err error) {
|
||||
@ -36,6 +39,7 @@ func CreateOrUpdateList(list *List) (err error) {
|
||||
|
||||
if list.ID == 0 {
|
||||
_, err = x.Insert(list)
|
||||
metrics.UpdateCount(1, metrics.ListCountKey)
|
||||
} else {
|
||||
_, err = x.ID(list.ID).Update(list)
|
||||
}
|
||||
|
@ -16,7 +16,10 @@
|
||||
|
||||
package models
|
||||
|
||||
import _ "code.vikunja.io/web" // For swaggerdocs generation
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
_ "code.vikunja.io/web" // For swaggerdocs generation
|
||||
)
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
// @Summary Deletes a list
|
||||
@ -41,6 +44,7 @@ func (l *List) Delete() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
metrics.UpdateCount(-1, metrics.ListCountKey)
|
||||
|
||||
// Delete all todotasks on that list
|
||||
_, err = x.Where("list_id = ?", l.ID).Delete(&ListTask{})
|
||||
|
@ -17,6 +17,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
@ -61,8 +62,12 @@ func (i *ListTask) Create(a web.Auth) (err error) {
|
||||
|
||||
i.CreatedByID = u.ID
|
||||
i.CreatedBy = u
|
||||
_, err = x.Insert(i)
|
||||
return err
|
||||
if _, err = x.Insert(i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.UpdateCount(1, metrics.TaskCountKey)
|
||||
return
|
||||
}
|
||||
|
||||
// Update updates a list task
|
||||
|
@ -16,7 +16,10 @@
|
||||
|
||||
package models
|
||||
|
||||
import _ "code.vikunja.io/web" // For swaggerdocs generation
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
_ "code.vikunja.io/web" // For swaggerdocs generation
|
||||
)
|
||||
|
||||
// Delete implements the delete method for listTask
|
||||
// @Summary Delete a task
|
||||
@ -38,6 +41,10 @@ func (i *ListTask) Delete() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = x.ID(i.ID).Delete(ListTask{})
|
||||
if _, err = x.ID(i.ID).Delete(ListTask{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.UpdateCount(-1, metrics.TaskCountKey)
|
||||
return
|
||||
}
|
||||
|
@ -17,14 +17,13 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql" // Because.
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
xrc "github.com/go-xorm/xorm-redis-cache"
|
||||
_ "github.com/mattn/go-sqlite3" // Because.
|
||||
|
||||
"encoding/gob"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@ -86,7 +85,7 @@ func SetEngine() (err error) {
|
||||
x.SetDefaultCacher(cacher)
|
||||
break
|
||||
case "redis":
|
||||
cacher := xrc.NewRedisCacher(viper.GetString("cache.redishost"), viper.GetString("cache.redispassword"), xrc.DEFAULT_EXPIRATION, x.Logger())
|
||||
cacher := xrc.NewRedisCacher(viper.GetString("redis.host"), viper.GetString("redis.password"), xrc.DEFAULT_EXPIRATION, x.Logger())
|
||||
x.SetDefaultCacher(cacher)
|
||||
gob.Register(tables)
|
||||
break
|
||||
@ -118,3 +117,8 @@ func getLimitFromPageIndex(page int) (limit, start int) {
|
||||
start = limit * (page - 1)
|
||||
return
|
||||
}
|
||||
|
||||
// GetTotalCount returns the total amount of something
|
||||
func GetTotalCount(counting interface{}) (count int64, err error) {
|
||||
return x.Count(counting)
|
||||
}
|
||||
|
@ -16,7 +16,10 @@
|
||||
|
||||
package models
|
||||
|
||||
import "code.vikunja.io/web"
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
// Create implements the creation method via the interface
|
||||
// @Summary Creates a new namespace
|
||||
@ -51,6 +54,10 @@ func (n *Namespace) Create(a web.Auth) (err error) {
|
||||
n.OwnerID = n.Owner.ID
|
||||
|
||||
// Insert
|
||||
_, err = x.Insert(n)
|
||||
if _, err = x.Insert(n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.UpdateCount(1, metrics.NamespaceCountKey)
|
||||
return
|
||||
}
|
||||
|
@ -16,7 +16,10 @@
|
||||
|
||||
package models
|
||||
|
||||
import _ "code.vikunja.io/web" // For swaggerdocs generation
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
_ "code.vikunja.io/web" // For swaggerdocs generation
|
||||
)
|
||||
|
||||
// Delete deletes a namespace
|
||||
// @Summary Deletes a namespace
|
||||
@ -66,5 +69,7 @@ func (n *Namespace) Delete() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
metrics.UpdateCount(-1, metrics.NamespaceCountKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -16,7 +16,10 @@
|
||||
|
||||
package models
|
||||
|
||||
import "code.vikunja.io/web"
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
// Create is the handler to create a team
|
||||
// @Summary Creates a new team
|
||||
@ -51,6 +54,10 @@ func (t *Team) Create(a web.Auth) (err error) {
|
||||
|
||||
// Insert the current user as member and admin
|
||||
tm := TeamMember{TeamID: t.ID, UserID: doer.ID, Admin: true}
|
||||
err = tm.Create(doer)
|
||||
if err = tm.Create(doer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.UpdateCount(1, metrics.TeamCountKey)
|
||||
return
|
||||
}
|
||||
|
@ -16,7 +16,10 @@
|
||||
|
||||
package models
|
||||
|
||||
import _ "code.vikunja.io/web" // For swaggerdocs generation
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
_ "code.vikunja.io/web" // For swaggerdocs generation
|
||||
)
|
||||
|
||||
// Delete deletes a team
|
||||
// @Summary Deletes a team
|
||||
@ -57,5 +60,10 @@ func (t *Team) Delete() (err error) {
|
||||
|
||||
// Delete team <-> lists relations
|
||||
_, err = x.Where("team_id = ?", t.ID).Delete(&TeamList{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
metrics.UpdateCount(-1, metrics.TeamCountKey)
|
||||
return
|
||||
}
|
||||
|
@ -17,6 +17,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
_ "code.vikunja.io/api/pkg/config" // To trigger its init() which initializes the config
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"fmt"
|
||||
"github.com/go-xorm/core"
|
||||
@ -36,8 +38,7 @@ func MainTest(m *testing.M, pathToRoot string) {
|
||||
var err error
|
||||
fixturesDir := filepath.Join(pathToRoot, "models", "fixtures")
|
||||
if err = createTestEngine(fixturesDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating test engine: %v\n", err)
|
||||
os.Exit(1)
|
||||
log.Log.Fatalf("Error creating test engine: %v\n", err)
|
||||
}
|
||||
|
||||
IsTesting = true
|
||||
@ -46,7 +47,9 @@ func MainTest(m *testing.M, pathToRoot string) {
|
||||
mail.StartMailDaemon()
|
||||
|
||||
// Create test database
|
||||
PrepareTestDatabase()
|
||||
if err = PrepareTestDatabase(); err != nil {
|
||||
log.Log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
@ -18,12 +18,14 @@ package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/web"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserLogin Object to recive user credentials in JSON format
|
||||
@ -159,3 +161,30 @@ func GetCurrentUser(c echo.Context) (user *User, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateActiveUsersFromContext updates the currently active users in redis
|
||||
func UpdateActiveUsersFromContext(c echo.Context) (err error) {
|
||||
user, err := GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allActiveUsers, err := metrics.GetActiveUsers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var uupdated bool
|
||||
for in, u := range allActiveUsers {
|
||||
if u.UserID == user.ID {
|
||||
allActiveUsers[in].LastSeen = time.Now()
|
||||
uupdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if !uupdated {
|
||||
allActiveUsers = append(allActiveUsers, &metrics.ActiveUser{UserID: user.ID, LastSeen: time.Now()})
|
||||
}
|
||||
|
||||
return metrics.SetActiveUsers(allActiveUsers)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@ -78,6 +79,9 @@ func CreateUser(user User) (newUser User, err error) {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Update the metrics
|
||||
metrics.UpdateCount(1, metrics.ActiveUsersKey)
|
||||
|
||||
// Get the full new User
|
||||
newUserOut, err := GetUser(newUser)
|
||||
if err != nil {
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
package models
|
||||
|
||||
import "code.vikunja.io/api/pkg/metrics"
|
||||
|
||||
// DeleteUserByID deletes a user by its ID
|
||||
func DeleteUserByID(id int64, doer *User) error {
|
||||
// Check if the id is 0
|
||||
@ -30,5 +32,8 @@ func DeleteUserByID(id int64, doer *User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the metrics
|
||||
metrics.UpdateCount(-1, metrics.ActiveUsersKey)
|
||||
|
||||
return err
|
||||
}
|
||||
|
52
pkg/red/redis.go
Normal file
52
pkg/red/redis.go
Normal file
@ -0,0 +1,52 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package red
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var r *redis.Client
|
||||
|
||||
// SetRedis initializes a redis connection
|
||||
func init() {
|
||||
if !viper.GetBool("redis.enabled") {
|
||||
return
|
||||
}
|
||||
|
||||
if viper.GetString("redis.host") == "" {
|
||||
log.Log.Fatal("No redis host provided.")
|
||||
}
|
||||
|
||||
r = redis.NewClient(&redis.Options{
|
||||
Addr: viper.GetString("redis.host"),
|
||||
Password: viper.GetString("redis.password"),
|
||||
DB: viper.GetInt("redis.db"),
|
||||
})
|
||||
|
||||
err := r.Ping().Err()
|
||||
if err != nil {
|
||||
log.Log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// GetRedis returns a pointer to a redis client
|
||||
func GetRedis() *redis.Client {
|
||||
return r
|
||||
}
|
@ -45,6 +45,7 @@ package routes
|
||||
import (
|
||||
_ "code.vikunja.io/api/docs" // To generate swagger docs
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"code.vikunja.io/web"
|
||||
@ -52,6 +53,7 @@ import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/swaggo/echo-swagger"
|
||||
)
|
||||
@ -112,6 +114,54 @@ func RegisterRoutes(e *echo.Echo) {
|
||||
// Swagger UI
|
||||
a.GET("/swagger/*", echoSwagger.WrapHandler)
|
||||
|
||||
// Prometheus endpoint
|
||||
if viper.GetBool("service.enablemetrics") {
|
||||
|
||||
if !viper.GetBool("redis.enabled") {
|
||||
log.Log.Fatal("You have to enable redis in order to use metrics")
|
||||
}
|
||||
|
||||
type countable struct {
|
||||
Rediskey string
|
||||
Type interface{}
|
||||
}
|
||||
|
||||
for _, c := range []countable{
|
||||
{
|
||||
metrics.ListCountKey,
|
||||
models.List{},
|
||||
},
|
||||
{
|
||||
metrics.UserCountKey,
|
||||
models.User{},
|
||||
},
|
||||
{
|
||||
metrics.NamespaceCountKey,
|
||||
models.Namespace{},
|
||||
},
|
||||
{
|
||||
metrics.TaskCountKey,
|
||||
models.ListTask{},
|
||||
},
|
||||
{
|
||||
metrics.TeamCountKey,
|
||||
models.Team{},
|
||||
},
|
||||
} {
|
||||
// Set initial totals
|
||||
total, err := models.GetTotalCount(c.Type)
|
||||
if err != nil {
|
||||
log.Log.Fatalf("Could not set initial count for %v, error was %s", c.Type, err)
|
||||
}
|
||||
if err := metrics.SetCount(total, c.Rediskey); err != nil {
|
||||
log.Log.Fatalf("Could not set initial count for %v, error was %s", c.Type, err)
|
||||
}
|
||||
}
|
||||
|
||||
a.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
|
||||
}
|
||||
|
||||
// User stuff
|
||||
a.POST("/login", apiv1.Login)
|
||||
a.POST("/register", apiv1.RegisterUser)
|
||||
a.POST("/user/password/token", apiv1.UserRequestResetPasswordToken)
|
||||
@ -138,6 +188,21 @@ func RegisterRoutes(e *echo.Echo) {
|
||||
}
|
||||
})
|
||||
|
||||
// Middleware to collect metrics
|
||||
if viper.GetBool("service.enablemetrics") {
|
||||
a.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
||||
// Update currently active users
|
||||
if err := models.UpdateActiveUsersFromContext(c); err != nil {
|
||||
log.Log.Error(err)
|
||||
return next(c)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
a.POST("/tokenTest", apiv1.CheckToken)
|
||||
|
||||
// User stuff
|
||||
|
20
vendor/github.com/beorn7/perks/LICENSE
generated
vendored
Normal file
20
vendor/github.com/beorn7/perks/LICENSE
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright (C) 2013 Blake Mizerany
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
2388
vendor/github.com/beorn7/perks/quantile/exampledata.txt
generated
vendored
Normal file
2388
vendor/github.com/beorn7/perks/quantile/exampledata.txt
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
316
vendor/github.com/beorn7/perks/quantile/stream.go
generated
vendored
Normal file
316
vendor/github.com/beorn7/perks/quantile/stream.go
generated
vendored
Normal file
@ -0,0 +1,316 @@
|
||||
// Package quantile computes approximate quantiles over an unbounded data
|
||||
// stream within low memory and CPU bounds.
|
||||
//
|
||||
// A small amount of accuracy is traded to achieve the above properties.
|
||||
//
|
||||
// Multiple streams can be merged before calling Query to generate a single set
|
||||
// of results. This is meaningful when the streams represent the same type of
|
||||
// data. See Merge and Samples.
|
||||
//
|
||||
// For more detailed information about the algorithm used, see:
|
||||
//
|
||||
// Effective Computation of Biased Quantiles over Data Streams
|
||||
//
|
||||
// http://www.cs.rutgers.edu/~muthu/bquant.pdf
|
||||
package quantile
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Sample holds an observed value and meta information for compression. JSON
|
||||
// tags have been added for convenience.
|
||||
type Sample struct {
|
||||
Value float64 `json:",string"`
|
||||
Width float64 `json:",string"`
|
||||
Delta float64 `json:",string"`
|
||||
}
|
||||
|
||||
// Samples represents a slice of samples. It implements sort.Interface.
|
||||
type Samples []Sample
|
||||
|
||||
func (a Samples) Len() int { return len(a) }
|
||||
func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value }
|
||||
func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
type invariant func(s *stream, r float64) float64
|
||||
|
||||
// NewLowBiased returns an initialized Stream for low-biased quantiles
|
||||
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
|
||||
// error guarantees can still be given even for the lower ranks of the data
|
||||
// distribution.
|
||||
//
|
||||
// The provided epsilon is a relative error, i.e. the true quantile of a value
|
||||
// returned by a query is guaranteed to be within (1±Epsilon)*Quantile.
|
||||
//
|
||||
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
|
||||
// properties.
|
||||
func NewLowBiased(epsilon float64) *Stream {
|
||||
ƒ := func(s *stream, r float64) float64 {
|
||||
return 2 * epsilon * r
|
||||
}
|
||||
return newStream(ƒ)
|
||||
}
|
||||
|
||||
// NewHighBiased returns an initialized Stream for high-biased quantiles
|
||||
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
|
||||
// error guarantees can still be given even for the higher ranks of the data
|
||||
// distribution.
|
||||
//
|
||||
// The provided epsilon is a relative error, i.e. the true quantile of a value
|
||||
// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile).
|
||||
//
|
||||
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
|
||||
// properties.
|
||||
func NewHighBiased(epsilon float64) *Stream {
|
||||
ƒ := func(s *stream, r float64) float64 {
|
||||
return 2 * epsilon * (s.n - r)
|
||||
}
|
||||
return newStream(ƒ)
|
||||
}
|
||||
|
||||
// NewTargeted returns an initialized Stream concerned with a particular set of
|
||||
// quantile values that are supplied a priori. Knowing these a priori reduces
|
||||
// space and computation time. The targets map maps the desired quantiles to
|
||||
// their absolute errors, i.e. the true quantile of a value returned by a query
|
||||
// is guaranteed to be within (Quantile±Epsilon).
|
||||
//
|
||||
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties.
|
||||
func NewTargeted(targetMap map[float64]float64) *Stream {
|
||||
// Convert map to slice to avoid slow iterations on a map.
|
||||
// ƒ is called on the hot path, so converting the map to a slice
|
||||
// beforehand results in significant CPU savings.
|
||||
targets := targetMapToSlice(targetMap)
|
||||
|
||||
ƒ := func(s *stream, r float64) float64 {
|
||||
var m = math.MaxFloat64
|
||||
var f float64
|
||||
for _, t := range targets {
|
||||
if t.quantile*s.n <= r {
|
||||
f = (2 * t.epsilon * r) / t.quantile
|
||||
} else {
|
||||
f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile)
|
||||
}
|
||||
if f < m {
|
||||
m = f
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
return newStream(ƒ)
|
||||
}
|
||||
|
||||
type target struct {
|
||||
quantile float64
|
||||
epsilon float64
|
||||
}
|
||||
|
||||
func targetMapToSlice(targetMap map[float64]float64) []target {
|
||||
targets := make([]target, 0, len(targetMap))
|
||||
|
||||
for quantile, epsilon := range targetMap {
|
||||
t := target{
|
||||
quantile: quantile,
|
||||
epsilon: epsilon,
|
||||
}
|
||||
targets = append(targets, t)
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
// Stream computes quantiles for a stream of float64s. It is not thread-safe by
|
||||
// design. Take care when using across multiple goroutines.
|
||||
type Stream struct {
|
||||
*stream
|
||||
b Samples
|
||||
sorted bool
|
||||
}
|
||||
|
||||
func newStream(ƒ invariant) *Stream {
|
||||
x := &stream{ƒ: ƒ}
|
||||
return &Stream{x, make(Samples, 0, 500), true}
|
||||
}
|
||||
|
||||
// Insert inserts v into the stream.
|
||||
func (s *Stream) Insert(v float64) {
|
||||
s.insert(Sample{Value: v, Width: 1})
|
||||
}
|
||||
|
||||
func (s *Stream) insert(sample Sample) {
|
||||
s.b = append(s.b, sample)
|
||||
s.sorted = false
|
||||
if len(s.b) == cap(s.b) {
|
||||
s.flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Query returns the computed qth percentiles value. If s was created with
|
||||
// NewTargeted, and q is not in the set of quantiles provided a priori, Query
|
||||
// will return an unspecified result.
|
||||
func (s *Stream) Query(q float64) float64 {
|
||||
if !s.flushed() {
|
||||
// Fast path when there hasn't been enough data for a flush;
|
||||
// this also yields better accuracy for small sets of data.
|
||||
l := len(s.b)
|
||||
if l == 0 {
|
||||
return 0
|
||||
}
|
||||
i := int(math.Ceil(float64(l) * q))
|
||||
if i > 0 {
|
||||
i -= 1
|
||||
}
|
||||
s.maybeSort()
|
||||
return s.b[i].Value
|
||||
}
|
||||
s.flush()
|
||||
return s.stream.query(q)
|
||||
}
|
||||
|
||||
// Merge merges samples into the underlying streams samples. This is handy when
|
||||
// merging multiple streams from separate threads, database shards, etc.
|
||||
//
|
||||
// ATTENTION: This method is broken and does not yield correct results. The
|
||||
// underlying algorithm is not capable of merging streams correctly.
|
||||
func (s *Stream) Merge(samples Samples) {
|
||||
sort.Sort(samples)
|
||||
s.stream.merge(samples)
|
||||
}
|
||||
|
||||
// Reset reinitializes and clears the list reusing the samples buffer memory.
|
||||
func (s *Stream) Reset() {
|
||||
s.stream.reset()
|
||||
s.b = s.b[:0]
|
||||
}
|
||||
|
||||
// Samples returns stream samples held by s.
|
||||
func (s *Stream) Samples() Samples {
|
||||
if !s.flushed() {
|
||||
return s.b
|
||||
}
|
||||
s.flush()
|
||||
return s.stream.samples()
|
||||
}
|
||||
|
||||
// Count returns the total number of samples observed in the stream
|
||||
// since initialization.
|
||||
func (s *Stream) Count() int {
|
||||
return len(s.b) + s.stream.count()
|
||||
}
|
||||
|
||||
func (s *Stream) flush() {
|
||||
s.maybeSort()
|
||||
s.stream.merge(s.b)
|
||||
s.b = s.b[:0]
|
||||
}
|
||||
|
||||
func (s *Stream) maybeSort() {
|
||||
if !s.sorted {
|
||||
s.sorted = true
|
||||
sort.Sort(s.b)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) flushed() bool {
|
||||
return len(s.stream.l) > 0
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
n float64
|
||||
l []Sample
|
||||
ƒ invariant
|
||||
}
|
||||
|
||||
func (s *stream) reset() {
|
||||
s.l = s.l[:0]
|
||||
s.n = 0
|
||||
}
|
||||
|
||||
func (s *stream) insert(v float64) {
|
||||
s.merge(Samples{{v, 1, 0}})
|
||||
}
|
||||
|
||||
func (s *stream) merge(samples Samples) {
|
||||
// TODO(beorn7): This tries to merge not only individual samples, but
|
||||
// whole summaries. The paper doesn't mention merging summaries at
|
||||
// all. Unittests show that the merging is inaccurate. Find out how to
|
||||
// do merges properly.
|
||||
var r float64
|
||||
i := 0
|
||||
for _, sample := range samples {
|
||||
for ; i < len(s.l); i++ {
|
||||
c := s.l[i]
|
||||
if c.Value > sample.Value {
|
||||
// Insert at position i.
|
||||
s.l = append(s.l, Sample{})
|
||||
copy(s.l[i+1:], s.l[i:])
|
||||
s.l[i] = Sample{
|
||||
sample.Value,
|
||||
sample.Width,
|
||||
math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1),
|
||||
// TODO(beorn7): How to calculate delta correctly?
|
||||
}
|
||||
i++
|
||||
goto inserted
|
||||
}
|
||||
r += c.Width
|
||||
}
|
||||
s.l = append(s.l, Sample{sample.Value, sample.Width, 0})
|
||||
i++
|
||||
inserted:
|
||||
s.n += sample.Width
|
||||
r += sample.Width
|
||||
}
|
||||
s.compress()
|
||||
}
|
||||
|
||||
func (s *stream) count() int {
|
||||
return int(s.n)
|
||||
}
|
||||
|
||||
func (s *stream) query(q float64) float64 {
|
||||
t := math.Ceil(q * s.n)
|
||||
t += math.Ceil(s.ƒ(s, t) / 2)
|
||||
p := s.l[0]
|
||||
var r float64
|
||||
for _, c := range s.l[1:] {
|
||||
r += p.Width
|
||||
if r+c.Width+c.Delta > t {
|
||||
return p.Value
|
||||
}
|
||||
p = c
|
||||
}
|
||||
return p.Value
|
||||
}
|
||||
|
||||
func (s *stream) compress() {
|
||||
if len(s.l) < 2 {
|
||||
return
|
||||
}
|
||||
x := s.l[len(s.l)-1]
|
||||
xi := len(s.l) - 1
|
||||
r := s.n - 1 - x.Width
|
||||
|
||||
for i := len(s.l) - 2; i >= 0; i-- {
|
||||
c := s.l[i]
|
||||
if c.Width+x.Width+x.Delta <= s.ƒ(s, r) {
|
||||
x.Width += c.Width
|
||||
s.l[xi] = x
|
||||
// Remove element at i.
|
||||
copy(s.l[i:], s.l[i+1:])
|
||||
s.l = s.l[:len(s.l)-1]
|
||||
xi -= 1
|
||||
} else {
|
||||
x = c
|
||||
xi = i
|
||||
}
|
||||
r -= c.Width
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stream) samples() Samples {
|
||||
samples := make(Samples, len(s.l))
|
||||
copy(samples, s.l)
|
||||
return samples
|
||||
}
|
2
vendor/github.com/go-redis/redis/.gitignore
generated
vendored
Normal file
2
vendor/github.com/go-redis/redis/.gitignore
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.rdb
|
||||
testdata/*/
|
21
vendor/github.com/go-redis/redis/.travis.yml
generated
vendored
Normal file
21
vendor/github.com/go-redis/redis/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
sudo: false
|
||||
language: go
|
||||
|
||||
services:
|
||||
- redis-server
|
||||
|
||||
go:
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||