mirror of
https://github.com/MeowLynxSea/Uptimeow.git
synced 2025-07-09 19:04:37 +00:00
453 lines
13 KiB
Go
453 lines
13 KiB
Go
package api
|
||
|
||
import (
|
||
"database/sql"
|
||
"encoding/json"
|
||
"github.com/MeowLynxSea/Uptimeow/config"
|
||
"github.com/MeowLynxSea/Uptimeow/internal/rcon"
|
||
_ "github.com/glebarez/sqlite"
|
||
"github.com/gorilla/websocket"
|
||
"github.com/robfig/cron/v3"
|
||
"github.com/wanghuiyt/ding"
|
||
"log"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
var GlobalConfig config.ConfigData
|
||
var saveCron = cron.New()
|
||
var isOnline bool
|
||
var tps, tps5, tps15 float64
|
||
var onlinePlayer, maxPlayer int
|
||
var playerList []string
|
||
var db *sql.DB
|
||
var warnLevel int
|
||
|
||
const (
|
||
warnLevelNormal = 0
|
||
warnLevelWarning = 1
|
||
warnLevelCritical = 2
|
||
)
|
||
|
||
type ServerData struct {
|
||
Time time.Time `json:"time"`
|
||
IsOnline bool `json:"is_online"`
|
||
Tps float64 `json:"tps"`
|
||
OnlinePlayer int `json:"online_player"`
|
||
MaxPlayer int `json:"max_player"`
|
||
}
|
||
|
||
// Response 是发送给WebSocket客户端的响应结构
|
||
type Response struct {
|
||
Code int `json:"code"`
|
||
Data []ServerData `json:"data"`
|
||
}
|
||
|
||
type ServerInfo struct {
|
||
ServerName string `json:"server_name"`
|
||
ServerAddress string `json:"server_address"`
|
||
ServerWebsite string `json:"server_website"`
|
||
ServerDescription string `json:"server_description"`
|
||
}
|
||
|
||
type DetailedInfo struct {
|
||
Time time.Time `json:"time"`
|
||
IsOnline bool `json:"is_online"`
|
||
Tps float64 `json:"tps"`
|
||
OnlinePlayer int `json:"online_player"`
|
||
MaxPlayer int `json:"max_player"`
|
||
PlayerList string `json:"player_list,omitempty"`
|
||
}
|
||
|
||
func pushDingTalkBot(message string, msgtype string) {
|
||
if GlobalConfig.Warn.DingTalkBot.Enabled {
|
||
dingMsger := ding.Webhook{
|
||
AccessToken: GlobalConfig.Warn.DingTalkBot.AccessToken,
|
||
Secret: GlobalConfig.Warn.DingTalkBot.Secret,
|
||
}
|
||
if GlobalConfig.Warn.DingTalkBot.AtMobile != "" {
|
||
err := dingMsger.SendMessageText(message, GlobalConfig.Warn.DingTalkBot.AtMobile)
|
||
if err != nil {
|
||
log.Println("[ERROR] 钉钉机器人推送失败,原因: ", err)
|
||
} else {
|
||
log.Println("[INFO] 钉钉机器人推送[" + msgtype + "]成功")
|
||
}
|
||
} else {
|
||
err := dingMsger.SendMessageText(message)
|
||
if err != nil {
|
||
log.Println("[ERROR] 钉钉机器人推送失败,原因: ", err)
|
||
} else {
|
||
log.Println("[INFO] 钉钉机器人推送[" + msgtype + "]成功")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func init() {
|
||
GlobalConfig = config.Load()
|
||
warnLevel = 0
|
||
|
||
pushDingTalkBot("【成功】Uptimeow 监控已上线", "成功消息")
|
||
|
||
db, err := sql.Open("sqlite", "data/history.db")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// 确保数据库连接是有效的
|
||
err = db.Ping()
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// 创建表data
|
||
createTableSQL := `
|
||
CREATE TABLE IF NOT EXISTS data (
|
||
time_index DATETIME NOT NULL PRIMARY KEY,
|
||
online BOOLEAN,
|
||
tps INTEGER,
|
||
online_player INTEGER,
|
||
max_player INTEGER,
|
||
player_list TEXT
|
||
);
|
||
`
|
||
_, err = db.Exec(createTableSQL)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
saveCron.AddFunc("@every 10s", func() {
|
||
currentTime := time.Now()
|
||
// log.Println("[DEBUG] Saving data to database")
|
||
err = db.Ping()
|
||
if err != nil {
|
||
log.Println(err)
|
||
}
|
||
if !isOnline {
|
||
_, err = db.Exec("INSERT INTO data (time_index, online, tps, online_player, max_player, player_list) VALUES (?, ?, ?, ?, ?, ?)", currentTime.Format("2006-01-02 15:04:05"), 0, tps, 0, 0, "")
|
||
} else {
|
||
if tps != 0 {
|
||
_, err = db.Exec("INSERT INTO data (time_index, online, tps, online_player, max_player, player_list) VALUES (?, ?, ?, ?, ?, ?)", currentTime.Format("2006-01-02 15:04:05"), isOnline, tps, onlinePlayer, maxPlayer, strings.Join(playerList, ","))
|
||
}
|
||
}
|
||
if err != nil {
|
||
log.Println("[ERROR] Failed to insert data into database:] ", err)
|
||
}
|
||
|
||
switch warnLevel {
|
||
case warnLevelNormal:
|
||
if !isOnline && GlobalConfig.Warn.EnabledType.Offline {
|
||
warnLevel = warnLevelCritical
|
||
pushDingTalkBot("【紧急】服务器离线\n经监测,服务器已离线,请尽快处理\n时间:"+currentTime.Format("2006-01-02 15:04:05"), "异常告警")
|
||
break
|
||
}
|
||
if tps < GlobalConfig.Warn.EnabledType.LowTps.Threold && GlobalConfig.Warn.EnabledType.LowTps.Enabled && tps != 0 {
|
||
warnLevel = warnLevelWarning
|
||
pushDingTalkBot("【警告】TPS过低报警\n服务器TPS低于设定值("+strconv.FormatFloat(GlobalConfig.Warn.EnabledType.LowTps.Threold, 'f', 2, 64)+")\n当前TPS:"+strconv.FormatFloat(tps, 'f', 2, 64)+"\n时间:"+currentTime.Format("2006-01-02 15:04:05"), "异常告警")
|
||
}
|
||
case warnLevelWarning:
|
||
if !isOnline && GlobalConfig.Warn.EnabledType.Offline {
|
||
warnLevel = warnLevelCritical
|
||
pushDingTalkBot("【紧急】服务器离线\n经监测,服务器已离线,请尽快处理\n时间:"+currentTime.Format("2006-01-02 15:04:05"), "异常告警")
|
||
break
|
||
}
|
||
if tps >= GlobalConfig.Warn.EnabledType.LowTps.Threold && GlobalConfig.Warn.EnabledType.LowTps.Enabled {
|
||
warnLevel = warnLevelNormal
|
||
pushDingTalkBot("【恢复】服务器TPS恢复正常\n时间:"+currentTime.Format("2006-01-02 15:04:05"), "成功消息")
|
||
}
|
||
case warnLevelCritical:
|
||
if isOnline && GlobalConfig.Warn.EnabledType.Offline {
|
||
warnLevel = warnLevelNormal
|
||
pushDingTalkBot("【恢复】服务器已恢复在线\n时间:"+currentTime.Format("2006-01-02 15:04:05"), "成功消息")
|
||
}
|
||
}
|
||
})
|
||
|
||
saveCron.Start()
|
||
|
||
isOnline = false
|
||
go rcon.InitRcon(callback)
|
||
}
|
||
|
||
func toInt(value interface{}) int {
|
||
switch v := value.(type) {
|
||
case int:
|
||
return v
|
||
case int64:
|
||
return int(v)
|
||
case float64:
|
||
return int(v)
|
||
case string:
|
||
i, _ := strconv.Atoi(v)
|
||
return i
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
var upgrader = websocket.Upgrader{
|
||
ReadBufferSize: 1024,
|
||
WriteBufferSize: 1024,
|
||
CheckOrigin: func(r *http.Request) bool {
|
||
// 允许所有跨域请求,或者你可以在这里添加更复杂的验证逻辑
|
||
return true
|
||
},
|
||
}
|
||
|
||
// WebSocketHandler 处理WebSocket连接
|
||
func WebSocketHandler(w http.ResponseWriter, r *http.Request) {
|
||
localDB, err := sql.Open("sqlite", "data/history.db")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
defer localDB.Close()
|
||
|
||
// 确保数据库连接是有效的
|
||
err = localDB.Ping()
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// 将HTTP连接升级为WebSocket连接
|
||
conn, err := upgrader.Upgrade(w, r, nil)
|
||
if err != nil {
|
||
log.Println("Error upgrading to WebSocket:", err)
|
||
return
|
||
}
|
||
defer conn.Close()
|
||
|
||
// 在这里实现WebSocket的消息处理逻辑
|
||
for {
|
||
_, message, err := conn.ReadMessage()
|
||
if err != nil {
|
||
log.Println("Error reading message:", err)
|
||
break
|
||
}
|
||
|
||
var resp Response
|
||
clientTimeFormat := "2006/01/02 15:04:05"
|
||
switch {
|
||
case string(message)[:13] == "earlier than ":
|
||
// 解析时间并获取数据
|
||
// log.Println("Received earlier than request")
|
||
reqTime, err := time.Parse(clientTimeFormat, strings.TrimPrefix(string(message), "earlier than "))
|
||
if err != nil {
|
||
log.Println("Error parsing time:", err)
|
||
continue
|
||
}
|
||
resp.Data, err = getEarlierData(localDB, reqTime)
|
||
if err != nil {
|
||
log.Println("Error getting data from database:", err)
|
||
continue
|
||
}
|
||
case string(message)[:11] == "later than ":
|
||
// 解析时间并获取数据
|
||
// log.Println("Received later than request")
|
||
reqTime, err := time.Parse(clientTimeFormat, strings.TrimPrefix(string(message), "later than "))
|
||
if err != nil {
|
||
log.Println("Error parsing time:", err)
|
||
continue
|
||
}
|
||
resp.Data, err = getLaterData(localDB, reqTime)
|
||
if err != nil {
|
||
log.Println("Error getting data from database:", err)
|
||
continue
|
||
}
|
||
default:
|
||
log.Println("Received unknown command " + string(message))
|
||
continue
|
||
}
|
||
|
||
if err != nil {
|
||
log.Println("Error getting data from database:", err)
|
||
continue
|
||
}
|
||
|
||
resp.Code = 200
|
||
// 序列化数据为JSON
|
||
jsonResp, err := json.Marshal(resp)
|
||
if err != nil {
|
||
log.Println("Error marshaling response:", err)
|
||
continue
|
||
}
|
||
|
||
// 发送数据给客户端
|
||
if err := conn.WriteMessage(websocket.TextMessage, jsonResp); err != nil {
|
||
log.Println("Error writing message:", err)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
func getLaterData(database *sql.DB, t time.Time) ([]ServerData, error) {
|
||
dbTime := t.Format("2006-01-02 15:04:05")
|
||
query := `SELECT time_index, online, tps, online_player, max_player
|
||
FROM data WHERE time_index > ? ORDER BY time_index ASC`
|
||
rows, err := database.Query(query, dbTime)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var data []ServerData
|
||
for rows.Next() {
|
||
var sd ServerData
|
||
if err := rows.Scan(&sd.Time, &sd.IsOnline, &sd.Tps, &sd.OnlinePlayer, &sd.MaxPlayer); err != nil {
|
||
return nil, err
|
||
}
|
||
data = append(data, sd)
|
||
}
|
||
return data, nil
|
||
}
|
||
|
||
func getEarlierData(database *sql.DB, t time.Time) ([]ServerData, error) {
|
||
dbTime := t.Format("2006-01-02 15:04:05")
|
||
query := `SELECT time_index, online, tps, online_player, max_player
|
||
FROM data WHERE time_index < ? ORDER BY time_index DESC LIMIT 60`
|
||
rows, err := database.Query(query, dbTime)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var data []ServerData
|
||
for rows.Next() {
|
||
var sd ServerData
|
||
if err := rows.Scan(&sd.Time, &sd.IsOnline, &sd.Tps, &sd.OnlinePlayer, &sd.MaxPlayer); err != nil {
|
||
return nil, err
|
||
}
|
||
data = append(data, sd)
|
||
}
|
||
reverse(data)
|
||
return data, nil
|
||
}
|
||
|
||
func reverse(slice []ServerData) {
|
||
last := len(slice) - 1
|
||
for i := 0; i < len(slice)/2; i++ {
|
||
slice[i], slice[last-i] = slice[last-i], slice[i]
|
||
}
|
||
}
|
||
|
||
func APIHandler(w http.ResponseWriter, r *http.Request) {
|
||
// 设置响应内容类型为JSON
|
||
w.Header().Set("Content-Type", "application/json")
|
||
|
||
// 解析请求参数
|
||
queryParams := r.URL.Query()
|
||
requestType := queryParams.Get("type")
|
||
|
||
// 检查请求类型是否为 server_info
|
||
switch requestType {
|
||
case "server_info":
|
||
// 准备要返回的数据
|
||
serverInfo := ServerInfo{
|
||
ServerName: GlobalConfig.ServerInfo.Name,
|
||
ServerAddress: GlobalConfig.ServerInfo.Address,
|
||
ServerWebsite: GlobalConfig.ServerInfo.Website,
|
||
ServerDescription: GlobalConfig.ServerInfo.Description,
|
||
}
|
||
|
||
// 创建响应结构
|
||
response := struct {
|
||
Code int `json:"code"`
|
||
Data ServerInfo `json:"data"`
|
||
}{
|
||
Code: 200,
|
||
Data: serverInfo,
|
||
}
|
||
|
||
// 将响应结构序列化为JSON并写入响应体
|
||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
}
|
||
case "detailed_info":
|
||
clientTimeFormat := "2006/01/02 15:04:05"
|
||
t, err := time.Parse(clientTimeFormat, queryParams.Get("time"))
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
dbTime := t.Format("2006-01-02 15:04:05")
|
||
|
||
localDB, err := sql.Open("sqlite", "data/history.db")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
defer localDB.Close()
|
||
query := `SELECT time_index, online, tps, online_player, max_player, player_list
|
||
FROM data WHERE time_index > ? ORDER BY time_index ASC`
|
||
rows, err := localDB.Query(query, dbTime)
|
||
if err != nil {
|
||
return
|
||
}
|
||
defer rows.Close()
|
||
|
||
var data []DetailedInfo
|
||
for rows.Next() {
|
||
var sd DetailedInfo
|
||
var playerList string
|
||
if err := rows.Scan(&sd.Time, &sd.IsOnline, &sd.Tps, &sd.OnlinePlayer, &sd.MaxPlayer, &playerList); err != nil {
|
||
return
|
||
}
|
||
sd.PlayerList = playerList
|
||
data = append(data, sd)
|
||
}
|
||
// 创建响应结构
|
||
response := struct {
|
||
Code int `json:"code"`
|
||
Data DetailedInfo `json:"data"`
|
||
}{
|
||
Code: 200,
|
||
Data: data[0],
|
||
}
|
||
|
||
// 将响应结构序列化为JSON并写入响应体
|
||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
}
|
||
default:
|
||
http.Error(w, "Invalid request type", http.StatusBadRequest)
|
||
}
|
||
}
|
||
|
||
func callback(data string) {
|
||
// log.Println("[DEBUG] Receive callback data: " + data)
|
||
|
||
var jsonData map[string]interface{}
|
||
err := json.Unmarshal([]byte(data), &jsonData)
|
||
if err != nil {
|
||
log.Fatalln(err)
|
||
}
|
||
|
||
switch toInt(jsonData["type"]) {
|
||
case rcon.DataType_connection_success:
|
||
isOnline = true
|
||
log.Println("[INFO] RCON connection success")
|
||
case rcon.DataType_connection_error:
|
||
isOnline = false
|
||
tps, tps5, tps15, onlinePlayer, maxPlayer, playerList = 0, 0, 0, 0, 0, []string{}
|
||
log.Println("[ERROR] RCON connection error")
|
||
case rcon.DataType_execution_error:
|
||
isOnline = false
|
||
tps, tps5, tps15, onlinePlayer, maxPlayer, playerList = 0, 0, 0, 0, 0, []string{}
|
||
log.Println("[ERROR] RCON execution error")
|
||
case rcon.DataType_data_tps:
|
||
log.Println("[DEBUG] TPS: " + strconv.FormatFloat(jsonData["data"].(map[string]interface{})["l1m"].(float64), 'f', -1, 64))
|
||
tps = jsonData["data"].(map[string]interface{})["l1m"].(float64)
|
||
tps5 = jsonData["data"].(map[string]interface{})["l5m"].(float64)
|
||
tps15 = jsonData["data"].(map[string]interface{})["l15m"].(float64)
|
||
case rcon.DataType_data_list:
|
||
log.Println("[DEBUG] Player online: " + strconv.FormatFloat(jsonData["data"].(map[string]interface{})["online_player"].(float64), 'f', -1, 64) + "/" + strconv.FormatFloat(jsonData["data"].(map[string]interface{})["max_player"].(float64), 'f', -1, 64))
|
||
onlinePlayer = int(jsonData["data"].(map[string]interface{})["online_player"].(float64))
|
||
maxPlayer = int(jsonData["data"].(map[string]interface{})["max_player"].(float64))
|
||
//read player list
|
||
playerList = []string{}
|
||
for _, player := range jsonData["data"].(map[string]interface{})["player_list"].([]interface{}) {
|
||
playerList = append(playerList, player.(string))
|
||
}
|
||
}
|
||
}
|