feat(servers): implement saervers monitoring
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -18,8 +19,12 @@ func main() {
|
||||
db := database.Connect()
|
||||
defer db.Close()
|
||||
|
||||
// --- Собираем наши зависимости (Dependency Injection) ---
|
||||
userRepo := &database.UserRepository{DB: db}
|
||||
serverRepo := &database.ServerRepository{DB: db}
|
||||
serverPoller := &core.ServerPoller{Repo: serverRepo}
|
||||
|
||||
// Запускаем поллер в фоновой горутине
|
||||
go serverPoller.Start(context.Background())
|
||||
|
||||
// Сервисы
|
||||
userService := &core.UserService{Repo: userRepo}
|
||||
@@ -39,9 +44,9 @@ func main() {
|
||||
}
|
||||
// Хендлеры
|
||||
userHandler := &api.UserHandler{Service: userService}
|
||||
authHandler := &api.AuthHandler{Service: authService} // Новый хендлер
|
||||
profileHandler := &api.ProfileHandler{Service: profileService} // Новый хендлер
|
||||
|
||||
authHandler := &api.AuthHandler{Service: authService}
|
||||
profileHandler := &api.ProfileHandler{Service: profileService}
|
||||
serverHandler := &api.ServerHandler{Repo: serverRepo}
|
||||
// --- Настраиваем роутер ---
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger)
|
||||
@@ -51,7 +56,7 @@ func main() {
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Post("/register", userHandler.Register)
|
||||
r.Post("/login", authHandler.Login)
|
||||
// Здесь будет публичный эндпоинт для логина в веб-интерфейс
|
||||
r.Get("/servers", serverHandler.GetServers)
|
||||
})
|
||||
r.Route("/authserver", func(r chi.Router) {
|
||||
r.Post("/authenticate", authHandler.Authenticate)
|
||||
|
||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module gitea.mrixs.me/minecraft-platform/backend
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/Tnze/go-mc v1.20.2 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -1,3 +1,5 @@
|
||||
github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q=
|
||||
github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
|
||||
22
internal/api/server_handler.go
Normal file
22
internal/api/server_handler.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
||||
)
|
||||
|
||||
type ServerHandler struct {
|
||||
Repo *database.ServerRepository
|
||||
}
|
||||
|
||||
func (h *ServerHandler) GetServers(w http.ResponseWriter, r *http.Request) {
|
||||
servers, err := h.Repo.GetAllWithStatus(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get servers", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(servers)
|
||||
}
|
||||
88
internal/core/server_poller.go
Normal file
88
internal/core/server_poller.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||
"github.com/Tnze/go-mc/bot/basic"
|
||||
"github.com/Tnze/go-mc/net"
|
||||
)
|
||||
|
||||
type ServerPoller struct {
|
||||
Repo *database.ServerRepository
|
||||
}
|
||||
|
||||
func (p *ServerPoller) Start(ctx context.Context) {
|
||||
log.Println("Starting server poller...")
|
||||
ticker := time.NewTicker(60 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
p.pollAllServers(ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
p.pollAllServers(ctx)
|
||||
case <-ctx.Done():
|
||||
log.Println("Stopping server poller...")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServerPoller) pollAllServers(ctx context.Context) {
|
||||
servers, err := p.Repo.GetAllEnabledServers(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Poller: failed to get servers: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range servers {
|
||||
go p.pollServer(ctx, s)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServerPoller) pollServer(ctx context.Context, server *models.GameServer) {
|
||||
resp, delay, err := net.PingAndListTimeout(server.Address, 5*time.Second)
|
||||
if err != nil {
|
||||
log.Printf("Poller: failed to ping %s (%s): %v", server.Name, server.Address, err)
|
||||
return
|
||||
}
|
||||
|
||||
var status basic.ServerList
|
||||
if err := json.Unmarshal(resp, &status); err != nil {
|
||||
log.Printf("Poller: failed to unmarshal status for %s: %v", server.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
// MOTD может быть сложным объектом, извлекаем текст
|
||||
var motdText string
|
||||
if s, ok := status.Description.(string); ok {
|
||||
motdText = s
|
||||
} else {
|
||||
if m, ok := status.Description.(map[string]interface{}); ok {
|
||||
if t, ok := m["text"].(string); ok {
|
||||
motdText = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateData := &models.ServerStatus{
|
||||
StatusJSON: string(resp),
|
||||
Motd: motdText,
|
||||
PlayerCount: status.Players.Online,
|
||||
MaxPlayers: status.Players.Max,
|
||||
VersionName: status.Version.Name,
|
||||
Ping: delay.Milliseconds(),
|
||||
}
|
||||
|
||||
if err := p.Repo.UpdateServerStatus(ctx, server.ID, updateData); err != nil {
|
||||
log.Printf("Poller: failed to update status for %s: %v", server.Name, err)
|
||||
} else {
|
||||
log.Printf("Poller: successfully polled %s", server.Name)
|
||||
}
|
||||
}
|
||||
68
internal/database/server_repository.go
Normal file
68
internal/database/server_repository.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||
)
|
||||
|
||||
type ServerRepository struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// GetAllEnabledServers возвращает все активные серверы для опроса.
|
||||
func (r *ServerRepository) GetAllEnabledServers(ctx context.Context) ([]*models.GameServer, error) {
|
||||
rows, err := r.DB.QueryContext(ctx, "SELECT id, name, address FROM game_servers WHERE is_enabled = TRUE")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var servers []*models.GameServer
|
||||
for rows.Next() {
|
||||
s := &models.GameServer{}
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.Address); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
}
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// UpdateServerStatus обновляет данные о статусе сервера.
|
||||
func (r *ServerRepository) UpdateServerStatus(ctx context.Context, id int, status *models.ServerStatus) error {
|
||||
query := `
|
||||
UPDATE game_servers SET
|
||||
status_json = $1, last_polled_at = NOW(), motd = $2, player_count = $3,
|
||||
max_players = $4, version_name = $5, ping_backend_server = $6
|
||||
WHERE id = $7`
|
||||
_, err := r.DB.ExecContext(ctx, query,
|
||||
status.StatusJSON, status.Motd, status.PlayerCount, status.MaxPlayers,
|
||||
status.VersionName, status.Ping, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAllWithStatus возвращает все активные серверы с их текущим статусом.
|
||||
func (r *ServerRepository) GetAllWithStatus(ctx context.Context) ([]*models.GameServer, error) {
|
||||
query := `
|
||||
SELECT id, name, address, is_enabled, last_polled_at, motd,
|
||||
player_count, max_players, version_name, ping_backend_server
|
||||
FROM game_servers WHERE is_enabled = TRUE ORDER BY name`
|
||||
rows, err := r.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var servers []*models.GameServer
|
||||
for rows.Next() {
|
||||
s := &models.GameServer{}
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.Address, &s.IsEnabled, &s.LastPolledAt,
|
||||
&s.Motd, &s.PlayerCount, &s.MaxPlayers, &s.VersionName, &s.PingBackendServer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
}
|
||||
return servers, nil
|
||||
}
|
||||
26
internal/models/server.go
Normal file
26
internal/models/server.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type GameServer struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
StatusJSON *string `json:"-"`
|
||||
LastPolledAt *time.Time `json:"last_polled_at"`
|
||||
Motd *string `json:"motd"`
|
||||
PlayerCount *int `json:"player_count"`
|
||||
MaxPlayers *int `json:"max_players"`
|
||||
VersionName *string `json:"version_name"`
|
||||
PingBackendServer *int `json:"ping_proxy_server"`
|
||||
}
|
||||
|
||||
type ServerStatus struct {
|
||||
StatusJSON string
|
||||
Motd string
|
||||
PlayerCount int
|
||||
MaxPlayers int
|
||||
VersionName string
|
||||
Ping int64
|
||||
}
|
||||
Reference in New Issue
Block a user