diff --git a/features/maintenance/maintenance.go b/features/maintenance/maintenance.go index e3be001c..489d7475 100644 --- a/features/maintenance/maintenance.go +++ b/features/maintenance/maintenance.go @@ -186,7 +186,7 @@ func Config(w http.ResponseWriter, r *http.Request) { Key string `json:"key"` Value string `json:"value"` } - err := services.JSONBody(r, &bodyArgs) + err := services.ShouldBindJSON(r, &bodyArgs) if err != nil { log.Error("io.ReadAll is failed:", err) services.ResponseNotFound404UriNotExist(w, r) @@ -292,7 +292,7 @@ func TopOps(w http.ResponseWriter, r *http.Request) { Ops string `json:"ops"` Pid string `json:"pid"` } - err := services.JSONBody(r, &bodyArgs) + err := services.ShouldBindJSON(r, &bodyArgs) if err != nil { log.Error("io.ReadAll is failed:", err) services.ResponseNotFound404UriNotExist(w, r) diff --git a/features/monitor/monitor/model.go b/features/monitor/monitor/model.go new file mode 100644 index 00000000..8997a226 --- /dev/null +++ b/features/monitor/monitor/model.go @@ -0,0 +1,55 @@ +package monitor + +import "time" + +type MonitorBase struct { + ID uint `xorm:"id" json:"id"` + CreatedAt time.Time `xorm:"created_at" json:"createdAt"` + UpdatedAt time.Time `xorm:"updated_at" json:"updatedAt"` + + Cpu float64 `xorm:"cpu" json:"cpu"` + + LoadUsage float64 `xorm:"load_usage" json:"loadUsage"` + CpuLoad1 float64 `xorm:"cpu_load1" json:"cpuLoad1"` + CpuLoad5 float64 `xorm:"cpu_load5" json:"cpuLoad5"` + CpuLoad15 float64 `xorm:"cpu_load15" json:"cpuLoad15"` + + Memory float64 `xorm:"memory" json:"memory"` + + DbSize uint `xorm:"db_size" json:"dbSize"` +} + +type MonitorIO struct { + ID uint `xorm:"id" json:"id"` + CreatedAt time.Time `xorm:"created_at" json:"createdAt"` + UpdatedAt time.Time `xorm:"updated_at" json:"updatedAt"` + + Name string `xorm:"name" json:"name"` + Read uint64 `xorm:"read" json:"read"` + Write uint64 `xorm:"write" json:"write"` + Count uint64 `xorm:"count" json:"count"` + Time uint64 `xorm:"time" json:"time"` +} + +type MonitorNetwork struct { + ID uint `xorm:"id" json:"id"` + CreatedAt time.Time `xorm:"created_at" json:"createdAt"` + UpdatedAt time.Time `xorm:"updated_at" json:"updatedAt"` + + Name string `xorm:"name" json:"name"` + Up float64 `xorm:"up" json:"up"` + Down float64 `xorm:"down" json:"down"` +} + +type MonitorSearch struct { + Param string `json:"param" validate:"required,oneof=all cpu memory load io network"` + Info string `json:"info"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` +} + +type MonitorData struct { + Param string `json:"param" validate:"required,oneof=cpu memory load io network"` + Date []time.Time `json:"date"` + Value []interface{} `json:"value"` +} diff --git a/features/monitor/monitor/monitor.go b/features/monitor/monitor/monitor.go new file mode 100644 index 00000000..d9815668 --- /dev/null +++ b/features/monitor/monitor/monitor.go @@ -0,0 +1,124 @@ +package monitor + +import ( + "net/http" + "sort" + "time" + + "ems.agt/lib/dborm" + "ems.agt/lib/services" + "ems.agt/restagent/config" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/net" +) + +// 点击【主机 - 监控】菜单,进入监控报表,直观的了解服务器的运行状态,包含【平均负载】、【CPU性能监控】、【内存使用监控】、【磁盘IO监控】、【网络IO监控】 + +// 可以查看昨天,今天,最近7天,最近30天,自定义时间的监控指标情况。 +// 默认监控是开启的,可以在【面板设置 - 监控】页面中根据需求对监控进行开启和关闭。 +// 监控数据默认保存30天,可以自行修改,也可手动清理该日志。 + +var ( + // 可选网络 + UriNetOpt = config.UriPrefix + "/monitor/{apiVersion}/monitor/netoptions" + // 可选磁盘 + UriIoOpt = config.UriPrefix + "/monitor/{apiVersion}/monitor/iooptions" + // 加载 + UriLoad = config.UriPrefix + "/monitor/{apiVersion}/monitor/load" +) + +// Netoptions 可选网络 +func Netoptions(w http.ResponseWriter, r *http.Request) { + netStat, _ := net.IOCounters(true) + var options []string + options = append(options, "all") + for _, net := range netStat { + options = append(options, net.Name) + } + sort.Strings(options) + services.ResponseWithJson(w, 200, options) +} + +// Iooptions 可选磁盘 +func Iooptions(w http.ResponseWriter, r *http.Request) { + diskStat, _ := disk.IOCounters() + var options []string + options = append(options, "all") + for _, net := range diskStat { + options = append(options, net.Name) + } + sort.Strings(options) + services.ResponseWithJson(w, 200, options) +} + +// LoadMonitor 载入监控 +func LoadMonitor(w http.ResponseWriter, r *http.Request) { + // json 請求參數獲取 + var bodyArgs MonitorSearch + err := services.ShouldBindJSON(r, &bodyArgs) + if err != nil || dborm.DbClient.XEngine == nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + loc := time.Now().Location() + bodyArgs.StartTime = bodyArgs.StartTime.In(loc) + bodyArgs.EndTime = bodyArgs.EndTime.In(loc) + + var backdatas []MonitorData + if bodyArgs.Param == "all" || bodyArgs.Param == "cpu" || bodyArgs.Param == "memory" || bodyArgs.Param == "load" { + var bases []MonitorBase + err := dborm.DbClient.XEngine.SQL("SELECT * FROM monitor_base"). + Where("created_at > ? AND created_at < ?", bodyArgs.StartTime, bodyArgs.EndTime). + Find(&bases) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + var itemData MonitorData + itemData.Param = "base" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + if bodyArgs.Param == "all" || bodyArgs.Param == "io" { + var bases []MonitorIO + err := dborm.DbClient.XEngine.SQL("SELECT * FROM monitor_io"). + Where("created_at > ? AND created_at < ?", bodyArgs.StartTime, bodyArgs.EndTime). + Find(&bases) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + var itemData MonitorData + itemData.Param = "io" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + if bodyArgs.Param == "all" || bodyArgs.Param == "network" { + var bases []MonitorNetwork + err := dborm.DbClient.XEngine.SQL("SELECT * FROM monitor_network"). + Where("name = ? AND created_at > ? AND created_at < ?", bodyArgs.Info, bodyArgs.StartTime, bodyArgs.EndTime). + Find(&bases) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + var itemData MonitorData + itemData.Param = "network" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + services.ResponseWithJson(w, 200, backdatas) +} diff --git a/features/monitor/monitor/task.go b/features/monitor/monitor/task.go new file mode 100644 index 00000000..8c6736d7 --- /dev/null +++ b/features/monitor/monitor/task.go @@ -0,0 +1,233 @@ +package monitor + +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/restagent/config" + "github.com/robfig/cron/v3" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/load" + "github.com/shirou/gopsutil/v3/mem" + "github.com/shirou/gopsutil/v3/net" +) + +type MonitorService struct{} + +type IMonitorService interface { + Run() +} + +func NewIMonitorService() IMonitorService { + return &MonitorService{} +} + +func (m *MonitorService) Run() { + // monitorStatus, _ := dborm.XormGetConfig("SystemMonitor", "MonitorStatus") + // if monitorStatus["value"] == "disable" { + // return + // } + var itemModel MonitorBase + itemModel.CreatedAt = time.Now() + itemModel.UpdatedAt = time.Now() + + totalPercent, _ := cpu.Percent(3*time.Second, false) + if len(totalPercent) == 1 { + itemModel.Cpu = totalPercent[0] + } + cpuCount, _ := cpu.Counts(false) + + loadInfo, _ := load.Avg() + itemModel.CpuLoad1 = loadInfo.Load1 + itemModel.CpuLoad5 = loadInfo.Load5 + itemModel.CpuLoad15 = loadInfo.Load15 + itemModel.LoadUsage = loadInfo.Load1 / (float64(cpuCount*2) * 0.75) * 100 + + memoryInfo, _ := mem.VirtualMemory() + itemModel.Memory = memoryInfo.UsedPercent + + var dataSize int + conf := config.GetYamlConfig() + result, err := dborm.DbClient.XEngine.QueryString("SELECT SUM(data_length) AS data_size FROM information_schema.tables WHERE TABLE_SCHEMA = ?;", conf.Database.Name) + if err != nil { + dataSize = 0 + } else { + v, _ := strconv.Atoi(result[0]["data_size"]) + dataSize = v + } + itemModel.DbSize = uint(dataSize) + + _, errx := dborm.DbClient.XEngine.Table("monitor_base").Insert(itemModel) + if errx != nil { + log.Errorf("Insert basic monitoring data failed, err: %v", err) + } + + go loadDiskIO() + go loadNetIO() + + // 删除保留的记录 + // monitorStoreDays, _ := dborm.XormGetConfig("SystemMonitor", "MonitorStoreDays") + // if monitorStoreDays["value"] != "" { + // return + // } + // storeDays, err := strconv.Atoi(MonitorStoreDays.Value) + // if err != nil { + // timeForDelete := time.Now().AddDate(0, 0, -storeDays) + // DelMonitorBase(timeForDelete) + // DelMonitorIO(timeForDelete) + // DelMonitorNet(timeForDelete) + // } + +} + +func loadDiskIO() { + ioStat, _ := disk.IOCounters() + + time.Sleep(60 * time.Second) + + ioStat2, _ := disk.IOCounters() + var ioList []MonitorIO + for _, io2 := range ioStat2 { + for _, io1 := range ioStat { + if io2.Name == io1.Name { + var itemIO MonitorIO + itemIO.CreatedAt = time.Now() + itemIO.UpdatedAt = time.Now() + + itemIO.Name = io1.Name + if io2.ReadBytes != 0 && io1.ReadBytes != 0 && io2.ReadBytes > io1.ReadBytes { + itemIO.Read = uint64(float64(io2.ReadBytes-io1.ReadBytes) / 60) + } + if io2.WriteBytes != 0 && io1.WriteBytes != 0 && io2.WriteBytes > io1.WriteBytes { + itemIO.Write = uint64(float64(io2.WriteBytes-io1.WriteBytes) / 60) + } + + if io2.ReadCount != 0 && io1.ReadCount != 0 && io2.ReadCount > io1.ReadCount { + itemIO.Count = uint64(float64(io2.ReadCount-io1.ReadCount) / 60) + } + writeCount := uint64(0) + if io2.WriteCount != 0 && io1.WriteCount != 0 && io2.WriteCount > io1.WriteCount { + writeCount = uint64(float64(io2.WriteCount-io1.WriteCount) / 60) + } + if writeCount > itemIO.Count { + itemIO.Count = writeCount + } + + if io2.ReadTime != 0 && io1.ReadTime != 0 && io2.ReadTime > io1.ReadTime { + itemIO.Time = uint64(float64(io2.ReadTime-io1.ReadTime) / 60) + } + writeTime := uint64(0) + if io2.WriteTime != 0 && io1.WriteTime != 0 && io2.WriteTime > io1.WriteTime { + writeTime = uint64(float64(io2.WriteTime-io1.WriteTime) / 60) + } + if writeTime > itemIO.Time { + itemIO.Time = writeTime + } + ioList = append(ioList, itemIO) + break + } + } + } + _, err := dborm.DbClient.XEngine.Table("monitor_io").Insert(ioList) + if err != nil { + log.Errorf("Insert io monitoring data failed, err: %v", err) + } +} + +func loadNetIO() { + netStat, _ := net.IOCounters(true) + netStatAll, _ := net.IOCounters(false) + + time.Sleep(60 * time.Second) + + netStat2, _ := net.IOCounters(true) + var netList []MonitorNetwork + for _, net2 := range netStat2 { + for _, net1 := range netStat { + if net2.Name == net1.Name { + var itemNet MonitorNetwork + itemNet.CreatedAt = time.Now() + itemNet.UpdatedAt = time.Now() + + itemNet.Name = net1.Name + + if net2.BytesSent != 0 && net1.BytesSent != 0 && net2.BytesSent > net1.BytesSent { + itemNet.Up = float64(net2.BytesSent-net1.BytesSent) / 1024 / 60 + } + if net2.BytesRecv != 0 && net1.BytesRecv != 0 && net2.BytesRecv > net1.BytesRecv { + itemNet.Down = float64(net2.BytesRecv-net1.BytesRecv) / 1024 / 60 + } + netList = append(netList, itemNet) + break + } + } + } + netStatAll2, _ := net.IOCounters(false) + for _, net2 := range netStatAll2 { + for _, net1 := range netStatAll { + if net2.Name == net1.Name { + var itemNet MonitorNetwork + itemNet.Name = net1.Name + if net2.BytesSent != 0 && net1.BytesSent != 0 && net2.BytesSent > net1.BytesSent { + itemNet.Up = float64(net2.BytesSent-net1.BytesSent) / 1024 / 60 + } + + if net2.BytesRecv != 0 && net1.BytesRecv != 0 && net2.BytesRecv > net1.BytesRecv { + itemNet.Down = float64(net2.BytesRecv-net1.BytesRecv) / 1024 / 60 + } + netList = append(netList, itemNet) + break + } + } + } + + rows, err := dborm.DbClient.XEngine.Table("monitor_network").Insert(netList) + if err != nil { + log.Errorf("Insert network monitoring data failed, err: %v", err) + } + fmt.Println(rows, err) +} + +var c *cron.Cron +var monitorCronID int + +func init() { + c = cron.New() + monitorCronID = 0 +} + +// StartMonitor 开始监控任务 removeBefore删除上次任务,间隔interval分钟 +func StartMonitor(removeBefore bool, interval string) error { + if removeBefore { + c.Remove(cron.EntryID(monitorCronID)) + } + + // 读取配置 + if interval == "" { + v, err := dborm.XormGetConfig("SystemMonitor", "sampleTime") + if err != nil { + return err + } + data := make(map[string]any) + err = json.Unmarshal([]byte(v["value_json"].(string)), &data) + if err != nil { + log.Error("json StartMonitor:%s", err.Error()) + return err + } + interval = data["sampleTime"].(string) + } + + imservice := NewIMonitorService() + monitorID, err := c.AddJob(fmt.Sprintf("@every %sm", interval), imservice) + if err != nil { + return err + } + imservice.Run() + monitorCronID = int(monitorID) + return nil +} diff --git a/features/monitor/process.go b/features/monitor/process.go deleted file mode 100644 index 863ec866..00000000 --- a/features/monitor/process.go +++ /dev/null @@ -1,51 +0,0 @@ -package monitor - -import ( - "net/http" - - websocket2 "ems.agt/lib/websocket" - "ems.agt/restagent/config" - "github.com/gorilla/websocket" -) - -var ( - // 進程 - UriWs = config.UriPrefix + "/monitor/{apiVersion}/process/ws" -) - -var wsUpgrade = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, -} - -func ProcessWs(w http.ResponseWriter, r *http.Request) { - ws, err := wsUpgrade.Upgrade(w, r, nil) - if err != nil { - return - } - wsClient := websocket2.NewWsClient("processClient", ws) - go wsClient.Read() - go wsClient.Write() -} - -// @Tags Process -// @Summary Stop Process -// @Description 停止进程 -// @Param request body request.ProcessReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /process/stop [post] -// @x-panel-log {"bodyKeys":["PID"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"结束进程 [PID]","formatEN":"结束进程 [PID]"} -func StopProcess(w http.ResponseWriter, r *http.Request) { - // var req request.ProcessReq - // if err := c.ShouldBindJSON(&req); err != nil { - // helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - // return - // } - // if err := processService.StopProcess(req); err != nil { - // helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - // return - // } - // helper.SuccessWithOutData(c) -} diff --git a/features/monitor/psnet/psnet.go b/features/monitor/psnet/psnet.go new file mode 100644 index 00000000..8f38fcd8 --- /dev/null +++ b/features/monitor/psnet/psnet.go @@ -0,0 +1,62 @@ +package psnet + +import ( + "net/http" + + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/lib/wsinfo" + "ems.agt/restagent/config" + "github.com/gorilla/websocket" + "github.com/shirou/gopsutil/process" +) + +var ( + // websockte通信 + UriWs = config.UriPrefix + "/monitor/{apiVersion}/psnet/ws" + // 停止进程 + UriStop = config.UriPrefix + "/monitor/{apiVersion}/psnet/stop" +) + +// 进程管理 +var wsUpgrade = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// ProcessWs +func ProcessWs(w http.ResponseWriter, r *http.Request) { + ws, err := wsUpgrade.Upgrade(w, r, nil) + if err != nil { + return + } + wsClient := wsinfo.NewWsClient("processClient", ws) + go wsClient.Read() + go wsClient.Write() +} + +// 停止进程 {"PID":30040} +func StopProcess(w http.ResponseWriter, r *http.Request) { + // json 請求參數獲取 + var bodyArgs struct { + PID int32 `json:"PID" validate:"required"` + } + err := services.ShouldBindJSON(r, &bodyArgs) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + proc, err := process.NewProcess(bodyArgs.PID) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + if err := proc.Kill(); err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + services.ResponseStatusOK200Null(w) +} diff --git a/lib/dborm/dborm.go b/lib/dborm/dborm.go index 55edab18..1d8cd066 100644 --- a/lib/dborm/dborm.go +++ b/lib/dborm/dborm.go @@ -746,20 +746,17 @@ func XormCheckLoginUser(name, password, cryptArgo string) (bool, *User, error) { if t.Before(time.Now()) { errMsg := "密码到期时间" // 读取配置信息 - result, err := xEngine.QueryString("SELECT value_json FROM omc_db.config WHERE id=20;") + result, err := XormGetConfig("Security", "pwdStrong") if err != nil { return false, nil, err } - if len(result) > 0 { - data := make(map[string]any) - err := json.Unmarshal([]byte(result[0]["value_json"]), &data) - if err != nil { - log.Error("json Unmarshal:%s", errMsg, data) - return false, nil, err - } - errMsg = data["outTimeMsg"].(string) - + data := make(map[string]any) + err = json.Unmarshal([]byte(result["value_json"].(string)), &data) + if err != nil { + log.Error("json Unmarshal:%s", errMsg) + return false, nil, err } + errMsg = data["outTimeMsg"].(string) log.Error("PasswordExpiration:%s", errMsg) return false, nil, errors.New(errMsg) } @@ -800,6 +797,18 @@ func XormIsExistUser(accid string) (bool, error) { return exist, nil } +func XormGetConfig(moduleName, configTag string) (map[string]any, error) { + result, err := DbClient.XEngine.QueryInterface("select * from config where module_name=? and config_tag=?", moduleName, configTag) + if err != nil { + log.Error("Failed to get config:", err) + return nil, err + } + if len(result) > 0 { + return result[0], nil + } + return map[string]any{}, nil +} + func XormGetConfigValue(moduleName, configTag string) (string, error) { var value string _, err := xEngine.Table("config"). diff --git a/lib/routes/routes.go b/lib/routes/routes.go index 9d78821d..425e0cf0 100644 --- a/lib/routes/routes.go +++ b/lib/routes/routes.go @@ -11,6 +11,8 @@ import ( "ems.agt/features/file" "ems.agt/features/fm" "ems.agt/features/mml" + "ems.agt/features/monitor/monitor" + "ems.agt/features/monitor/psnet" "ems.agt/features/nbi" "ems.agt/features/pm" "ems.agt/features/security" @@ -216,6 +218,16 @@ func init() { // AAAA Register("GET", aaaa.CustomUriAAAASSO, aaaa.GetSSOFromAAAA, nil) + + // 进程网络 + Register("GET", psnet.UriWs, psnet.ProcessWs, nil) + Register("POST", psnet.UriStop, psnet.StopProcess, nil) + + // 主机CPU内存监控 + Register("POST", monitor.UriLoad, monitor.LoadMonitor, nil) + Register("GET", monitor.UriNetOpt, monitor.Netoptions, nil) + Register("GET", monitor.UriIoOpt, monitor.Iooptions, nil) + } // To resolv rest POST/PUT/DELETE/PATCH cross domain diff --git a/lib/services/requset.go b/lib/services/requset.go index 0fff11f0..8fa8e00c 100644 --- a/lib/services/requset.go +++ b/lib/services/requset.go @@ -10,7 +10,7 @@ import ( ) // 读取json请求结构团体 -func JSONBody(r *http.Request, args any) error { +func ShouldBindJSON(r *http.Request, args any) error { body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) if err != nil { return err diff --git a/lib/websocket/client.go b/lib/wsinfo/client.go similarity index 97% rename from lib/websocket/client.go rename to lib/wsinfo/client.go index 368eeeee..36201cdd 100644 --- a/lib/websocket/client.go +++ b/lib/wsinfo/client.go @@ -1,4 +1,4 @@ -package websocket +package wsinfo import ( "github.com/gorilla/websocket" diff --git a/lib/websocket/process_data.go b/lib/wsinfo/process_data.go similarity index 99% rename from lib/websocket/process_data.go rename to lib/wsinfo/process_data.go index 95d17d23..5891540a 100644 --- a/lib/websocket/process_data.go +++ b/lib/wsinfo/process_data.go @@ -1,4 +1,4 @@ -package websocket +package wsinfo import ( "encoding/json" diff --git a/restagent/restagent.go b/restagent/restagent.go index 86811313..d87f1a50 100644 --- a/restagent/restagent.go +++ b/restagent/restagent.go @@ -14,6 +14,7 @@ import ( "ems.agt/features/dbrest" "ems.agt/features/fm" + "ems.agt/features/monitor/monitor" "ems.agt/features/pm" "ems.agt/restagent/config" ) @@ -102,6 +103,9 @@ func main() { router := routes.NewRouter() + // 开启监控采集 + monitor.StartMonitor(false, "") + for _, rest := range conf.Rest { // ipv4 goroutines if rest.IPv4 != "" {