Merge pull request #135 from lanrenwo/home_charts

实现后台首页图表(用户在线数、网络吞吐量、CPU、内存)
This commit is contained in:
bjdgyc
2022-09-15 08:46:14 +08:00
committed by GitHub
11 changed files with 680 additions and 49 deletions

View File

@@ -0,0 +1,33 @@
package admin
import (
"errors"
"net/http"
"github.com/bjdgyc/anylink/dbdata"
)
func StatsInfoList(w http.ResponseWriter, r *http.Request) {
var ok bool
_ = r.ParseForm()
action := r.FormValue("action")
scope := r.FormValue("scope")
ok = dbdata.StatsInfoIns.ValidAction(action)
if !ok {
RespError(w, RespParamErr, errors.New("不存在的图表类别"))
return
}
ok = dbdata.StatsInfoIns.ValidScope(scope)
if !ok {
RespError(w, RespParamErr, errors.New("不存在的日期范围"))
return
}
datas, err := dbdata.StatsInfoIns.GetData(action, scope)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
data := make(map[string]interface{})
data["datas"] = datas
RespSucess(w, data)
}

View File

@@ -8,6 +8,7 @@ import (
"net/http/pprof"
"github.com/bjdgyc/anylink/base"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
@@ -18,6 +19,7 @@ func StartAdmin() {
r := mux.NewRouter()
r.Use(authMiddleware)
r.Use(handlers.CompressHandler)
// 监控检测
r.HandleFunc("/status.html", func(w http.ResponseWriter, r *http.Request) {
@@ -64,6 +66,8 @@ func StartAdmin() {
r.HandleFunc("/group/set", GroupSet)
r.HandleFunc("/group/del", GroupDel)
r.HandleFunc("/statsinfo/list", StatsInfoList)
// pprof
if base.Cfg.Pprof {
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline).Name("debug")

View File

@@ -25,7 +25,7 @@ func initDb() {
}
// 初始化数据库
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{})
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{})
if err != nil {
base.Fatal(err)
}

View File

@@ -70,6 +70,15 @@ func Find(data interface{}, limit, page int) error {
return xdb.Limit(limit, start).Find(data)
}
func FindWhere(data interface{}, limit int, page int, where string, args ...interface{}) error {
if limit == 0 {
return xdb.Where(where, args...).Find(data)
}
start := (page - 1) * limit
return xdb.Where(where, args...).Limit(limit, start).Find(data)
}
func CountPrefix(fieldName string, prefix string, data interface{}) int {
n, _ := xdb.Where(fieldName+" like ?", prefix+"%").Count(data)
return int(n)

231
server/dbdata/statsinfo.go Normal file
View File

@@ -0,0 +1,231 @@
package dbdata
import (
"container/list"
"errors"
"fmt"
"strconv"
"time"
"github.com/bjdgyc/anylink/base"
)
const (
LayoutTimeFormat = "2006-01-02 15:04:05"
LayoutTimeFormatMin = "2006-01-02 15:04"
RealTimeMaxSize = 120 // 实时数据最大保存条数
)
type StatsInfo struct {
RealtimeData map[string]*list.List
Actions []string
Scopes []string
}
type ScopeDetail struct {
sTime time.Time
eTime time.Time
minutes int
fsTime string
feTime string
}
var StatsInfoIns *StatsInfo
func init() {
StatsInfoIns = &StatsInfo{
Actions: []string{"online", "network", "cpu", "mem"},
Scopes: []string{"rt", "1h", "24h", "3d", "7d", "30d"},
RealtimeData: make(map[string]*list.List),
}
for _, v := range StatsInfoIns.Actions {
StatsInfoIns.RealtimeData[v] = list.New()
}
}
// 校验统计类型值
func (s *StatsInfo) ValidAction(action string) bool {
for _, item := range s.Actions {
if item == action {
return true
}
}
return false
}
// 校验日期范围值
func (s *StatsInfo) ValidScope(scope string) bool {
for _, item := range s.Scopes {
if item == scope {
return true
}
}
return false
}
// 设置实时统计数据
func (s *StatsInfo) SetRealTime(action string, val interface{}) {
if s.RealtimeData[action].Len() >= RealTimeMaxSize {
ele := s.RealtimeData[action].Front()
s.RealtimeData[action].Remove(ele)
}
s.RealtimeData[action].PushBack(val)
}
// 获取实时统计数据
func (s *StatsInfo) GetRealTime(action string) (res []interface{}) {
for e := s.RealtimeData[action].Front(); e != nil; e = e.Next() {
res = append(res, e.Value)
}
return
}
// 保存数据至数据库
func (s *StatsInfo) SaveStatsInfo(so *StatsOnline, sn *StatsNetwork, sc *StatsCpu, sm *StatsMem) {
if so.Num != 0 {
_ = Add(so)
}
if sn.Up != 0 || sn.Down != 0 {
_ = Add(sn)
}
if sc.Percent != 0 {
_ = Add(sc)
}
if sm.Percent != 0 {
_ = Add(sm)
}
}
// 获取统计数据
func (s *StatsInfo) GetData(action string, scope string) (res []interface{}, err error) {
if scope == "rt" {
return s.GetRealTime(action), nil
}
statsMaps := make(map[string]interface{})
currSec := fmt.Sprintf("%02d", time.Now().Second())
// 获取时间段数据
sd := s.getScopeDetail(scope)
timeList := s.getTimeList(sd)
res = make([]interface{}, len(timeList))
// 获取数据库查询条件
where := s.getStatsWhere(sd)
if where == "" {
return nil, errors.New("不支持的数据库类型: " + base.Cfg.DbType)
}
// 查询数据表
switch action {
case "online":
statsRes := []StatsOnline{}
FindWhere(&statsRes, 0, 0, where, sd.fsTime, sd.feTime)
for _, v := range statsRes {
t := v.CreatedAt.Format(LayoutTimeFormatMin)
statsMaps[t] = v
}
case "network":
statsRes := []StatsNetwork{}
FindWhere(&statsRes, 0, 0, where, sd.fsTime, sd.feTime)
for _, v := range statsRes {
t := v.CreatedAt.Format(LayoutTimeFormatMin)
statsMaps[t] = v
}
case "cpu":
statsRes := []StatsCpu{}
FindWhere(&statsRes, 0, 0, where, sd.fsTime, sd.feTime)
for _, v := range statsRes {
t := v.CreatedAt.Format(LayoutTimeFormatMin)
statsMaps[t] = v
}
case "mem":
statsRes := []StatsMem{}
FindWhere(&statsRes, 0, 0, where, sd.fsTime, sd.feTime)
for _, v := range statsRes {
t := v.CreatedAt.Format(LayoutTimeFormatMin)
statsMaps[t] = v
}
}
// 整合数据
for i, v := range timeList {
if mv, ok := statsMaps[v]; ok {
res[i] = mv
continue
}
t, _ := time.ParseInLocation(LayoutTimeFormat, v+":"+currSec, time.Local)
switch action {
case "online":
res[i] = StatsOnline{CreatedAt: t}
case "network":
res[i] = StatsNetwork{CreatedAt: t}
case "cpu":
res[i] = StatsCpu{CreatedAt: t}
case "mem":
res[i] = StatsMem{CreatedAt: t}
}
}
return
}
// 获取日期范围的明细值
func (s *StatsInfo) getScopeDetail(scope string) (sd *ScopeDetail) {
sd = &ScopeDetail{}
t := time.Now()
sd.eTime = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 59, t.Nanosecond(), time.Local)
sd.minutes = 0
switch scope {
case "1h":
sd.sTime = sd.eTime.Add(-time.Minute * 60)
sd.minutes = 1
case "24h":
sd.sTime = sd.eTime.AddDate(0, 0, -1)
sd.minutes = 5
case "7d":
sd.sTime = sd.eTime.AddDate(0, 0, -7)
sd.minutes = 30
case "30d":
sd.sTime = sd.eTime.AddDate(0, 0, -30)
sd.minutes = 150
}
if sd.minutes != 0 {
sd.sTime = sd.sTime.Add(-time.Minute * time.Duration(sd.minutes))
}
sd.fsTime = sd.sTime.Format(LayoutTimeFormat)
sd.feTime = sd.eTime.Format(LayoutTimeFormat)
// UTC
switch base.Cfg.DbType {
case "sqlite3", "postgres":
sd.fsTime = sd.sTime.UTC().Format(LayoutTimeFormat)
sd.feTime = sd.eTime.UTC().Format(LayoutTimeFormat)
}
return
}
// 针对日期范围进行拆解
func (s *StatsInfo) getTimeList(sd *ScopeDetail) []string {
subSec := int64(60 * sd.minutes)
count := (sd.eTime.Unix()-sd.sTime.Unix())/subSec - 1
eTime := sd.eTime.Unix() - subSec
timeLists := make([]string, count)
for i := count - 1; i >= 0; i-- {
timeLists[i] = time.Unix(eTime, 0).Format(LayoutTimeFormatMin)
eTime = eTime - subSec
}
return timeLists
}
// 获取where条件
func (s *StatsInfo) getStatsWhere(sd *ScopeDetail) (where string) {
where = "created_at BETWEEN ? AND ?"
min := strconv.Itoa(sd.minutes)
switch base.Cfg.DbType {
case "mysql":
where += " AND floor(TIMESTAMPDIFF(SECOND, created_at, '" + sd.feTime + "') / 60) % " + min + " = 0"
case "sqlite3":
where += " AND CAST(ROUND((JULIANDAY('" + sd.feTime + "') - JULIANDAY(created_at)) * 86400) / 60 as integer) % " + min + " = 0"
case "postgres":
where += " AND floor((EXTRACT(EPOCH FROM " + sd.feTime + ") - EXTRACT(EPOCH FROM created_at)) / 60) % " + min + " = 0"
default:
where = ""
}
return
}

View File

@@ -84,3 +84,31 @@ type Policy struct {
CreatedAt time.Time `json:"created_at" xorm:"DateTime created"`
UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
}
type StatsOnline struct {
Id int `json:"id" xorm:"pk autoincr not null"`
Num int `json:"num" xorm:"Int"`
NumGroups string `json:"num_groups" xorm:"varchar(500) not null"`
CreatedAt time.Time `json:"created_at" xorm:"DateTime created index"`
}
type StatsNetwork struct {
Id int `json:"id" xorm:"pk autoincr not null"`
Up uint32 `json:"up" xorm:"Int"`
Down uint32 `json:"down" xorm:"Int"`
UpGroups string `json:"up_groups" xorm:"varchar(500) not null"`
DownGroups string `json:"down_groups" xorm:"varchar(500) not null"`
CreatedAt time.Time `json:"created_at" xorm:"DateTime created index"`
}
type StatsCpu struct {
Id int `json:"id" xorm:"pk autoincr not null"`
Percent float64 `json:"percent" xorm:"Float"`
CreatedAt time.Time `json:"created_at" xorm:"DateTime created index"`
}
type StatsMem struct {
Id int `json:"id" xorm:"pk autoincr not null"`
Percent float64 `json:"percent" xorm:"Float"`
CreatedAt time.Time `json:"created_at" xorm:"DateTime created index"`
}

View File

@@ -4,16 +4,16 @@ go 1.16
require (
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/go-ldap/ldap v3.0.3+incompatible // indirect
github.com/go-ldap/ldap/v3 v3.4.3 // indirect
github.com/go-ldap/ldap v3.0.3+incompatible
github.com/go-sql-driver/mysql v1.6.0
github.com/gocarina/gocsv v0.0.0-20220712153207-8b2118da4570 // indirect
github.com/gocarina/gocsv v0.0.0-20220712153207-8b2118da4570
github.com/golang-jwt/jwt/v4 v4.0.0
github.com/google/gopacket v1.1.19
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.2
github.com/mattn/go-sqlite3 v1.14.8
github.com/orcaman/concurrent-map v1.0.0 // indirect
github.com/orcaman/concurrent-map v1.0.0
github.com/pion/dtls/v2 v2.0.9
github.com/pion/logging v0.2.2
github.com/shirou/gopsutil v3.21.7+incompatible

View File

@@ -3,4 +3,5 @@ package sessdata
func Start() {
initIpPool()
checkSession()
saveStatsInfo()
}

View File

@@ -0,0 +1,102 @@
package sessdata
import (
"encoding/json"
"sync/atomic"
"time"
"github.com/bjdgyc/anylink/dbdata"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/mem"
)
const (
StatsCycleSec = 5 // 统计周期(秒)
AddCycleSec = 60 // 记录到数据表周期(秒)
)
func saveStatsInfo() {
go func() {
tick := time.NewTicker(time.Second * StatsCycleSec)
count := 0
for range tick.C {
up := uint32(0)
down := uint32(0)
upGroups := make(map[string]uint32)
downGroups := make(map[string]uint32)
numGroups := make(map[string]int)
onlineNum := 0
sessMux.Lock()
for _, v := range sessions {
v.mux.Lock()
if v.IsActive {
// 在线人数
onlineNum += 1
numGroups[v.Group] += 1
// 网络吞吐
userUp := atomic.LoadUint32(&v.CSess.BandwidthUpPeriod)
userDown := atomic.LoadUint32(&v.CSess.BandwidthDownPeriod)
upGroups[v.Group] += userUp
downGroups[v.Group] += userDown
up += userUp
down += userDown
}
v.mux.Unlock()
}
sessMux.Unlock()
tNow := time.Now()
// online
numData, _ := json.Marshal(numGroups)
so := &dbdata.StatsOnline{Num: onlineNum, NumGroups: string(numData), CreatedAt: tNow}
// network
upData, _ := json.Marshal(upGroups)
downData, _ := json.Marshal(downGroups)
sn := &dbdata.StatsNetwork{Up: up, Down: down, UpGroups: string(upData), DownGroups: string(downData), CreatedAt: tNow}
// cpu
sc := &dbdata.StatsCpu{Percent: getCpuPercent(), CreatedAt: tNow}
// mem
sm := &dbdata.StatsMem{Percent: getMemPercent(), CreatedAt: tNow}
count++
// 是否保存至数据库
save := count*StatsCycleSec >= AddCycleSec
// 历史数据
if save {
count = 0
}
// 设置统计数据
setStatsData(save, so, sn, sc, sm)
}
}()
}
func setStatsData(save bool, so *dbdata.StatsOnline, sn *dbdata.StatsNetwork, sc *dbdata.StatsCpu, sm *dbdata.StatsMem) {
// 实时数据
dbdata.StatsInfoIns.SetRealTime("online", so)
dbdata.StatsInfoIns.SetRealTime("network", sn)
dbdata.StatsInfoIns.SetRealTime("cpu", sc)
dbdata.StatsInfoIns.SetRealTime("mem", sm)
if !save {
return
}
dbdata.StatsInfoIns.SaveStatsInfo(so, sn, sc, sm)
}
func getCpuPercent() float64 {
cpuUsedPercent, _ := cpu.Percent(0, false)
percent := cpuUsedPercent[0]
if percent == 0 {
percent = 1
}
return decimal(percent)
}
func getMemPercent() float64 {
m, _ := mem.VirtualMemory()
return decimal(m.UsedPercent)
}
func decimal(f float64) float64 {
i := int(f * 100)
return float64(i) / 100
}