U monitor

This commit is contained in:
zhenorzz 2021-10-15 15:58:24 +08:00
parent fa84c0c7f1
commit e0392c4175
15 changed files with 169 additions and 160 deletions

View File

@ -3,9 +3,7 @@ package controller
import (
"github.com/zhenorzz/goploy/core"
"github.com/zhenorzz/goploy/model"
"net"
"strconv"
"time"
"github.com/zhenorzz/goploy/service"
)
// Monitor struct
@ -44,15 +42,13 @@ func (Monitor) GetTotal(gp *core.Goploy) *core.Response {
// Check one monitor
func (Monitor) Check(gp *core.Goploy) *core.Response {
type ReqData struct {
Domain string `json:"domain" validate:"required"`
Port int `json:"port" validate:"min=0,max=65535"`
URL string `json:"url" validate:"required"`
}
var reqData ReqData
if err := verify(gp.Body, &reqData); err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
_, err := net.DialTimeout("tcp", reqData.Domain+":"+strconv.Itoa(reqData.Port), 5*time.Second)
if err != nil {
if err := (service.Gnet{URL: reqData.URL}.Ping()); err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
return &core.Response{Message: "Connected"}
@ -62,8 +58,7 @@ func (Monitor) Check(gp *core.Goploy) *core.Response {
func (Monitor) Add(gp *core.Goploy) *core.Response {
type ReqData struct {
Name string `json:"name" validate:"required"`
Domain string `json:"domain" validate:"required"`
Port int `json:"port" validate:"min=0,max=65535"`
URL string `json:"url" validate:"required"`
Second int `json:"second" validate:"gt=0"`
Times uint16 `json:"times" validate:"gt=0"`
NotifyType uint8 `json:"notifyType" validate:"gt=0"`
@ -79,8 +74,7 @@ func (Monitor) Add(gp *core.Goploy) *core.Response {
id, err := model.Monitor{
NamespaceID: gp.Namespace.ID,
Name: reqData.Name,
Domain: reqData.Domain,
Port: reqData.Port,
URL: reqData.URL,
Second: reqData.Second,
Times: reqData.Times,
NotifyType: reqData.NotifyType,
@ -104,7 +98,7 @@ func (Monitor) Edit(gp *core.Goploy) *core.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Name string `json:"name" validate:"required"`
Domain string `json:"domain" validate:"required"`
URL string `json:"url" validate:"required"`
Port int `json:"port" validate:"min=0,max=65535"`
Second int `json:"second" validate:"gt=0"`
Times uint16 `json:"times" validate:"gt=0"`
@ -120,8 +114,7 @@ func (Monitor) Edit(gp *core.Goploy) *core.Response {
err := model.Monitor{
ID: reqData.ID,
Name: reqData.Name,
Domain: reqData.Domain,
Port: reqData.Port,
URL: reqData.URL,
Second: reqData.Second,
Times: reqData.Times,
NotifyType: reqData.NotifyType,

View File

@ -2,9 +2,7 @@ package controller
import (
"database/sql"
"github.com/patrickmn/go-cache"
"net/http"
"strconv"
"time"
"github.com/zhenorzz/goploy/core"
@ -37,9 +35,9 @@ func (User) Login(gp *core.Goploy) *core.Response {
return &core.Response{Code: core.AccountDisabled, Message: "Account is disabled"}
}
namespaceList, err := core.GetNamespace(userData.ID)
namespaceList, err := model.Namespace{UserID: userData.ID}.GetAllByUserID()
if err != nil {
return &core.Response{Code: core.Error, Message: "尚未分配空间,请联系管理员"}
return &core.Response{Code: core.Error, Message: "No space assigned, please contact the administrator"}
}
token, err := userData.CreateToken()
@ -47,15 +45,7 @@ func (User) Login(gp *core.Goploy) *core.Response {
return &core.Response{Code: core.Error, Message: err.Error()}
}
model.User{ID: userData.ID, LastLoginTime: time.Now().Format("20060102150405")}.UpdateLastLoginTime()
core.Cache.Set("userInfo:"+strconv.Itoa(int(userData.ID)), &userData, cache.DefaultExpiration)
namespaceList, err = model.Namespace{UserID: userData.ID}.GetAllByUserID()
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
core.Cache.Set("namespace:"+strconv.Itoa(int(userData.ID)), &namespaceList, cache.DefaultExpiration)
_ = model.User{ID: userData.ID, LastLoginTime: time.Now().Format("20060102150405")}.UpdateLastLoginTime()
cookie := http.Cookie{Name: core.LoginCookieName, Value: token, Path: "/", MaxAge: 86400, HttpOnly: true}
http.SetCookie(gp.ResponseWriter, &cookie)

View File

@ -1,47 +0,0 @@
package core
import (
"strconv"
"time"
"github.com/zhenorzz/goploy/model"
"github.com/patrickmn/go-cache"
)
// Cache uint
var Cache = cache.New(24*time.Hour, 48*time.Hour)
// GetUserInfo return model.User and error
func GetUserInfo(userID int64) (model.User, error) {
var userData model.User
var err error
if x, found := Cache.Get("userInfo:" + strconv.Itoa(int(userID))); found {
userData = *x.(*model.User)
} else {
userData, err = model.User{ID: userID}.GetData()
if err != nil {
return userData, err
}
Cache.Set("userInfo:"+strconv.Itoa(int(userID)), &userData, cache.DefaultExpiration)
}
return userData, nil
}
// GetNamespace return model.Namespaces and error
func GetNamespace(userID int64) (model.Namespaces, error) {
var namespaceList model.Namespaces
var err error
if x, found := Cache.Get("namespace:" + strconv.Itoa(int(userID))); found {
namespaceList = *x.(*model.Namespaces)
} else {
namespaceList, err = model.Namespace{UserID: userID}.GetAllByUserID()
if err != nil {
return namespaceList, err
}
Cache.Set("namespace:"+strconv.Itoa(int(userID)), &namespaceList, cache.DefaultExpiration)
}
return namespaceList, nil
}

View File

@ -148,12 +148,10 @@ func (rt *Router) checkLogin(w http.ResponseWriter, r *http.Request) (*Goploy, *
return nil, &Response{Code: Deny, Message: "Invalid namespace"}
}
namespaceList, err := GetNamespace(int64(claims["id"].(float64)))
namespaceList, err := model.Namespace{UserID: int64(claims["id"].(float64))}.GetAllByUserID()
if err != nil {
return nil, &Response{Code: Deny, Message: "Get namespace list error"}
}
if len(namespaceList) == 0 {
} else if len(namespaceList) == 0 {
return nil, &Response{Code: Deny, Message: "No available namespace"}
}
@ -166,8 +164,7 @@ func (rt *Router) checkLogin(w http.ResponseWriter, r *http.Request) (*Goploy, *
if namespace == (model.Namespace{}) {
return nil, &Response{Code: NamespaceInvalid, Message: "Namespace no permission, please login again"}
}
userInfo, err = GetUserInfo(int64(claims["id"].(float64)))
userInfo, err = model.User{ID: int64(claims["id"].(float64))}.GetData()
if err != nil {
return nil, &Response{Code: Deny, Message: "Get user information error"}
}

View File

@ -1,6 +1,6 @@
FROM alpine
LABEL maintainer="zhenorzz@gmail.com"
ARG GOPLOY_VER=v1.3.5
ARG GOPLOY_VER=v1.3.6
ENV GOPLOY_VER=${GOPLOY_VER}
ENV MYSQL_PORT=3306

View File

@ -1,5 +1,19 @@
# change log
## 1.3.6
*2021-10-15*
### New features
- monitor support http
### Optimization
- delete cache
### Bug fixes
- fix task block
- fix symlink rollback
## 1.3.5
*2021-09-18*

View File

@ -36,7 +36,7 @@ var (
s string
)
const appVersion = "1.3.5"
const appVersion = "1.3.6"
func init() {
flag.StringVar(&core.AssetDir, "asset-dir", "", "default: ./")

View File

@ -13,8 +13,7 @@ type Monitor struct {
ID int64 `json:"id"`
NamespaceID int64 `json:"namespaceId"`
Name string `json:"name"`
Domain string `json:"domain"`
Port int `json:"port"`
URL string `json:"url"`
Second int `json:"second"`
Times uint16 `json:"times"`
NotifyType uint8 `json:"notifyType"`
@ -33,7 +32,7 @@ type Monitors []Monitor
// GetList -
func (m Monitor) GetList(pagination Pagination) (Monitors, error) {
rows, err := sq.
Select("id, name, domain, port, second, times, notify_type, notify_target, notify_times, description, error_content, state, insert_time, update_time").
Select("id, name, url, second, times, notify_type, notify_target, notify_times, description, error_content, state, insert_time, update_time").
From(monitorTable).
Where(sq.Eq{
"namespace_id": m.NamespaceID,
@ -53,8 +52,7 @@ func (m Monitor) GetList(pagination Pagination) (Monitors, error) {
if err := rows.Scan(
&monitor.ID,
&monitor.Name,
&monitor.Domain,
&monitor.Port,
&monitor.URL,
&monitor.Second,
&monitor.Times,
&monitor.NotifyType,
@ -95,13 +93,13 @@ func (m Monitor) GetTotal() (int64, error) {
func (m Monitor) GetData() (Monitor, error) {
var monitor Monitor
err := sq.
Select("id, name, domain, ip, port, second, times, notify_type, notify_target, notify_times, state").
Select("id, name, url, second, times, notify_type, notify_target, notify_times, state").
From(monitorTable).
Where(sq.Eq{"id": m.ID}).
OrderBy("id DESC").
RunWith(DB).
QueryRow().
Scan(&monitor.ID, &monitor.Name, &monitor.Domain, &monitor.Port, &monitor.Second, &monitor.Times, &monitor.NotifyType, &monitor.NotifyTarget, &monitor.NotifyTimes, &monitor.State)
Scan(&monitor.ID, &monitor.Name, &monitor.URL, &monitor.Second, &monitor.Times, &monitor.NotifyType, &monitor.NotifyTarget, &monitor.NotifyTimes, &monitor.State)
if err != nil {
return monitor, errors.New("数据查询失败")
}
@ -111,7 +109,7 @@ func (m Monitor) GetData() (Monitor, error) {
// GetAllByState -
func (m Monitor) GetAllByState() (Monitors, error) {
rows, err := sq.
Select("id, name, domain, port, second, times, notify_type, notify_target, notify_times, description").
Select("id, name, url, second, times, notify_type, notify_target, notify_times, description").
From(monitorTable).
Where(sq.Eq{
"state": m.State,
@ -128,8 +126,7 @@ func (m Monitor) GetAllByState() (Monitors, error) {
if err := rows.Scan(
&monitor.ID,
&monitor.Name,
&monitor.Domain,
&monitor.Port,
&monitor.URL,
&monitor.Second,
&monitor.Times,
&monitor.NotifyType,
@ -148,8 +145,8 @@ func (m Monitor) GetAllByState() (Monitors, error) {
func (m Monitor) AddRow() (int64, error) {
result, err := sq.
Insert(monitorTable).
Columns("namespace_id", "name", "domain", "port", "second", "times", "notify_type", "notify_target", "notify_times", "description", "error_content").
Values(m.NamespaceID, m.Name, m.Domain, m.Port, m.Second, m.Times, m.NotifyType, m.NotifyTarget, m.NotifyTimes, m.Description, "").
Columns("namespace_id", "name", "url", "second", "times", "notify_type", "notify_target", "notify_times", "description", "error_content").
Values(m.NamespaceID, m.Name, m.URL, m.Second, m.Times, m.NotifyType, m.NotifyTarget, m.NotifyTimes, m.Description, "").
RunWith(DB).
Exec()
if err != nil {
@ -165,8 +162,7 @@ func (m Monitor) EditRow() error {
Update(monitorTable).
SetMap(sq.Eq{
"name": m.Name,
"domain": m.Domain,
"port": m.Port,
"url": m.URL,
"second": m.Second,
"times": m.Times,
"notify_type": m.NotifyType,
@ -208,11 +204,11 @@ func (m Monitor) TurnOff(errorContent string) error {
_, err := sq.
Update(monitorTable).
SetMap(sq.Eq{
"state": Disable,
"state": Disable,
"error_content": errorContent,
}).
Where(sq.Eq{"id": m.ID}).
RunWith(DB).
Exec()
return err
}
}

3
model/sql/1.3.6.sql Normal file
View File

@ -0,0 +1,3 @@
ALTER TABLE `goploy`.`monitor`
DROP COLUMN `port`,
CHANGE COLUMN `domain` `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL AFTER `name`;

View File

@ -130,8 +130,7 @@ CREATE TABLE IF NOT EXISTS `monitor` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`namespace_id` int(10) unsigned NOT NULL,
`name` varchar(255) NOT NULL,
`domain` varchar(50) NOT NULL,
`port` smallint(5) unsigned NOT NULL DEFAULT '80',
`url` varchar(255) NOT NULL,
`second` int(10) unsigned NOT NULL DEFAULT '1' COMMENT 'How many seconds to run',
`times` smallint(5) unsigned NOT NULL DEFAULT '1' COMMENT 'How many times of failures',
`description` varchar(255) NOT NULL DEFAULT '',
@ -222,7 +221,7 @@ CREATE TABLE IF NOT EXISTS `namespace_user` (
UNIQUE KEY `uk_namespace_user` (`namespace_id`,`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `goploy`.`system_config` (
CREATE TABLE IF NOT EXISTS `system_config` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(255) NOT NULL DEFAULT '',
`value` varchar(255) NOT NULL DEFAULT '',
@ -233,5 +232,5 @@ CREATE TABLE IF NOT EXISTS `goploy`.`system_config` (
REPLACE INTO `user`(`id`, `account`, `password`, `name`, `contact`, `state`, `super_manager`) VALUES (1, 'admin', '$2a$10$89ZJ2xeJj35GOw11Qiucr.phaEZP4.kBX6aKTs7oWFp1xcGBBgijm', '超管', '', 1, 1);
REPLACE INTO `namespace`(`id`, `name`) VALUES (1, 'goploy');
REPLACE INTO `namespace_user`(`id`, `namespace_id`, `user_id`, `role`) VALUES (1, 1, 1, 'admin');
REPLACE INTO `system_config` (`key`, `value`) VALUES ('version', '1.3.4');
REPLACE INTO `system_config` (`key`, `value`) VALUES ('version', '1.3.6');

39
service/NetService.go Normal file
View File

@ -0,0 +1,39 @@
package service
import (
"errors"
"net"
"net/http"
"net/url"
"strconv"
"time"
)
// Gnet -
type Gnet struct {
URL string
}
func (gnet Gnet) Ping() error {
urlParse, err := url.Parse(gnet.URL)
if err != nil {
return err
}
switch urlParse.Scheme {
case "tcp":
conn, err := net.DialTimeout("tcp", urlParse.Host, 5*time.Second)
if err != nil {
return err
}
_ = conn.Close()
case "http", "https":
if resp, err := http.Get(urlParse.String()); err != nil {
return err
} else if resp.StatusCode != 200 {
return errors.New("Unexpected response status code: " + strconv.Itoa(resp.StatusCode))
}
default:
return errors.New("URL scheme " + urlParse.Scheme + " does not supported")
}
return nil
}

View File

@ -4,41 +4,49 @@ import (
"bytes"
"database/sql"
"encoding/json"
"github.com/patrickmn/go-cache"
"github.com/zhenorzz/goploy/core"
"github.com/zhenorzz/goploy/model"
"github.com/zhenorzz/goploy/service"
"github.com/zhenorzz/goploy/ws"
"net"
"net/http"
"strconv"
"time"
)
type MonitorCache struct {
errorTimes int
notifyTimes int
time int64
}
var monitorCaches = map[int64]MonitorCache{}
func monitorTask() {
monitors, err := model.Monitor{State: model.Enable}.GetAllByState()
if err != nil && err != sql.ErrNoRows {
core.Log(core.ERROR, "get monitor list error, detail:"+err.Error())
}
monitorIDs := map[int64]struct{}{}
for _, monitor := range monitors {
monitorCache := map[string]int64{"errorTimes": 0, "notifyTimes": 0, "time": 0}
if x, found := core.Cache.Get("monitor:" + strconv.Itoa(int(monitor.ID))); found {
monitorCache = x.(map[string]int64)
monitorIDs[monitor.ID] = struct{}{}
monitorCache, ok := monitorCaches[monitor.ID]
if !ok {
monitorCaches[monitor.ID] = MonitorCache{}
}
now := time.Now().Unix()
if int(now-monitorCache["time"]) > monitor.Second {
monitorCache["time"] = now
conn, err := net.DialTimeout("tcp", monitor.Domain+":"+strconv.Itoa(monitor.Port), 5*time.Second)
if err != nil {
monitorCache["errorTimes"]++
if int(now-monitorCache.time) > monitor.Second {
monitorCache.time = now
if err := (service.Gnet{URL: monitor.URL}.Ping()); err != nil {
monitorCache.errorTimes++
core.Log(core.ERROR, "monitor "+monitor.Name+" encounter error, "+err.Error())
if monitor.Times == uint16(monitorCache["errorTimes"]) {
monitorCache["errorTimes"] = 0
monitorCache["notifyTimes"]++
if monitor.Times == uint16(monitorCache.errorTimes) {
monitorCache.errorTimes = 0
monitorCache.notifyTimes++
notice(monitor, err)
if monitor.NotifyTimes == uint16(monitorCache["notifyTimes"]) {
monitorCache["notifyTimes"] = 0
monitor.TurnOff(err.Error())
if monitor.NotifyTimes == uint16(monitorCache.notifyTimes) {
monitorCache.notifyTimes = 0
_ = monitor.TurnOff(err.Error())
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeMonitor,
Message: ws.MonitorMessage{MonitorID: monitor.ID, State: ws.MonitorTurnOff, ErrorContent: err.Error()},
@ -46,10 +54,15 @@ func monitorTask() {
}
}
} else {
monitorCache["errorTimes"] = 0
conn.Close()
monitorCache.errorTimes = 0
}
core.Cache.Set("monitor:"+strconv.Itoa(int(monitor.ID)), monitorCache, cache.DefaultExpiration)
monitorCaches[monitor.ID] = monitorCache
}
}
for cacheID := range monitorCaches {
if _, ok := monitorIDs[cacheID]; !ok {
delete(monitorCaches, cacheID)
}
}
}
@ -74,7 +87,7 @@ func notice(monitor model.Monitor, err error) {
},
}
b, _ := json.Marshal(msg)
http.Post(monitor.NotifyTarget, "application/json", bytes.NewBuffer(b))
_, _ = http.Post(monitor.NotifyTarget, "application/json", bytes.NewBuffer(b))
} else if monitor.NotifyType == model.NotifyDingTalk {
type markdown struct {
Title string `json:"title"`
@ -94,7 +107,7 @@ func notice(monitor model.Monitor, err error) {
},
}
b, _ := json.Marshal(msg)
http.Post(monitor.NotifyTarget, "application/json", bytes.NewBuffer(b))
_, _ = http.Post(monitor.NotifyTarget, "application/json", bytes.NewBuffer(b))
} else if monitor.NotifyType == model.NotifyFeiShu {
type message struct {
Title string `json:"title"`
@ -109,14 +122,14 @@ func notice(monitor model.Monitor, err error) {
Text: text,
}
b, _ := json.Marshal(msg)
http.Post(monitor.NotifyTarget, "application/json", bytes.NewBuffer(b))
_, _ = http.Post(monitor.NotifyTarget, "application/json", bytes.NewBuffer(b))
} else if monitor.NotifyType == model.NotifyCustom {
type message struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
MonitorName string `json:"monitorName"`
Domain string `json:"domain"`
URL string `json:"url"`
Port int `json:"port"`
Second int `json:"second"`
Times uint16 `json:"times"`
@ -128,11 +141,10 @@ func notice(monitor model.Monitor, err error) {
Message: "Monitor:" + monitor.Name + "can not access",
}
msg.Data.MonitorName = monitor.Name
msg.Data.Domain = monitor.Domain
msg.Data.Port = monitor.Port
msg.Data.URL = monitor.URL
msg.Data.Second = monitor.Second
msg.Data.Times = monitor.Times
b, _ := json.Marshal(msg)
http.Post(monitor.NotifyTarget, "application/json", bytes.NewBuffer(b))
_, _ = http.Post(monitor.NotifyTarget, "application/json", bytes.NewBuffer(b))
}
}

View File

@ -2,6 +2,7 @@ package task
import (
"context"
"sync"
"sync/atomic"
"time"
)
@ -19,16 +20,32 @@ func ticker() {
// create ticker
minute := time.Tick(time.Minute)
second := time.Tick(time.Second)
for {
select {
case <-second:
monitorTask()
case <-minute:
projectTask()
case <-stop:
return
var wg sync.WaitGroup
wg.Add(2)
go func() {
for {
select {
case <-second:
monitorTask()
case <-stop:
wg.Done()
return
}
}
}
}()
go func() {
for {
select {
case <-minute:
projectTask()
case <-stop:
wg.Done()
return
}
}
}()
wg.Wait()
}
func Shutdown(ctx context.Context) error {

View File

@ -6,8 +6,7 @@ export class MonitorData {
id: number
namespaceId: number
name: string
domain: string
port: number
url: string
second: number
times: number
notifyType: number
@ -49,8 +48,7 @@ export class MonitorAdd extends Request {
readonly method = 'post'
public param: {
name: string
domain: string
port: number
url: string
second: number
times: number
notifyType: number
@ -70,8 +68,7 @@ export class MonitorEdit extends Request {
public param: {
id: number
name: string
domain: string
port: number
url: string
second: number
times: number
notifyType: number
@ -99,8 +96,10 @@ export class MonitorCheck extends Request {
readonly url = '/monitor/check'
readonly method = 'post'
readonly timeout = 100000
public param: ID
constructor(param: ID) {
public param: {
url: string
}
constructor(param: MonitorCheck['param']) {
super()
this.param = param
}

View File

@ -132,15 +132,15 @@
<el-form-item :label="$t('name')" prop="name">
<el-input v-model="formData.name" autocomplete="off" />
</el-form-item>
<el-form-item label="Domain/IP" prop="domain">
<el-input
v-model="formData.domain"
autocomplete="off"
placeholder="Skip http(s)"
/>
</el-form-item>
<el-form-item label="port" prop="port">
<el-input v-model.number="formData.port" autocomplete="off" />
<el-form-item prop="url">
<template #label>
URL
<el-tooltip placement="top">
<template #content>scheme:opaque[?query][#fragment]</template>
<i class="el-icon-question" />
</el-tooltip>
</template>
<el-input v-model="formData.url" autocomplete="off" placeholder="" />
</el-form-item>
<el-form-item :label="$t('interval') + '(s)'" prop="second">
<el-input v-model.number="formData.second" autocomplete="off" />
@ -232,8 +232,7 @@ export default defineComponent({
formData: {
id: 0,
name: '',
domain: '',
port: 80,
url: '',
second: 3,
times: 1,
notifyType: 1,
@ -243,9 +242,7 @@ export default defineComponent({
},
formRules: {
name: [{ required: true, message: 'Name required', trigger: 'blur' }],
domain: [
{ required: true, message: 'Host or IP required', trigger: 'blur' },
],
url: [{ required: true, message: 'URL required', trigger: 'blur' }],
port: [
{
type: 'number',