feat(servers): implement saervers monitoring

This commit is contained in:
2025-06-17 12:50:50 +03:00
parent 0e2e02622d
commit 42f2b68848
7 changed files with 217 additions and 5 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View 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)
}

View 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)
}
}

View 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
View 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
}