improve monitoring resilience
This commit is contained in:
@@ -4,3 +4,5 @@ MINECRAFT_HOST=example.org
|
|||||||
MINECRAFT_PORT=25565
|
MINECRAFT_PORT=25565
|
||||||
POLL_INTERVAL_SECONDS=30
|
POLL_INTERVAL_SECONDS=30
|
||||||
STATUS_FILE_PATH=data/status.json
|
STATUS_FILE_PATH=data/status.json
|
||||||
|
REQUEST_TIMEOUT_SECONDS=5
|
||||||
|
OFFLINE_AFTER_FAILURES=2
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ MINECRAFT_HOST=example.org
|
|||||||
MINECRAFT_PORT=25565
|
MINECRAFT_PORT=25565
|
||||||
POLL_INTERVAL_SECONDS=30
|
POLL_INTERVAL_SECONDS=30
|
||||||
STATUS_FILE_PATH=data/status.json
|
STATUS_FILE_PATH=data/status.json
|
||||||
|
REQUEST_TIMEOUT_SECONDS=5
|
||||||
|
OFFLINE_AFTER_FAILURES=2
|
||||||
```
|
```
|
||||||
3) Запустіть бота:
|
3) Запустіть бота:
|
||||||
```bash
|
```bash
|
||||||
@@ -82,6 +84,8 @@ docker-compose up -d
|
|||||||
- Усі параметри задаються через змінні оточення (див. `.env.example`).
|
- Усі параметри задаються через змінні оточення (див. `.env.example`).
|
||||||
- `POLL_INTERVAL_SECONDS` — частота опитування.
|
- `POLL_INTERVAL_SECONDS` — частота опитування.
|
||||||
- `STATUS_FILE_PATH` — куди писати JSON. Скрипт сам створить директорію, якщо її немає.
|
- `STATUS_FILE_PATH` — куди писати JSON. Скрипт сам створить директорію, якщо її немає.
|
||||||
|
- `REQUEST_TIMEOUT_SECONDS` — таймаут запиту до сервера (щоб не ловити «вічний» конект).
|
||||||
|
- `OFFLINE_AFTER_FAILURES` — скільки послідовних помилок/таймаутів вважати перед тим, як оголосити офлайн.
|
||||||
|
|
||||||
## Перевірка
|
## Перевірка
|
||||||
- Запустіть бота і в Telegram відправте `/start`, потім `/status`, щоб побачити поточні дані.
|
- Запустіть бота і в Telegram відправте `/start`, потім `/status`, щоб побачити поточні дані.
|
||||||
|
|||||||
16
config.py
16
config.py
@@ -12,6 +12,8 @@ class Settings:
|
|||||||
minecraft_port: int
|
minecraft_port: int
|
||||||
poll_interval_seconds: int
|
poll_interval_seconds: int
|
||||||
status_file_path: str
|
status_file_path: str
|
||||||
|
request_timeout_seconds: int
|
||||||
|
offline_after_failures: int
|
||||||
|
|
||||||
|
|
||||||
def load_settings() -> Settings:
|
def load_settings() -> Settings:
|
||||||
@@ -23,6 +25,8 @@ def load_settings() -> Settings:
|
|||||||
port = os.getenv("MINECRAFT_PORT", "25565")
|
port = os.getenv("MINECRAFT_PORT", "25565")
|
||||||
interval = os.getenv("POLL_INTERVAL_SECONDS", "30")
|
interval = os.getenv("POLL_INTERVAL_SECONDS", "30")
|
||||||
status_file_path = os.getenv("STATUS_FILE_PATH", "data/status.json")
|
status_file_path = os.getenv("STATUS_FILE_PATH", "data/status.json")
|
||||||
|
request_timeout = os.getenv("REQUEST_TIMEOUT_SECONDS", "5")
|
||||||
|
offline_after_failures = os.getenv("OFFLINE_AFTER_FAILURES", "2")
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
raise RuntimeError("TELEGRAM_BOT_TOKEN is required")
|
raise RuntimeError("TELEGRAM_BOT_TOKEN is required")
|
||||||
@@ -41,6 +45,16 @@ def load_settings() -> Settings:
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise RuntimeError("POLL_INTERVAL_SECONDS must be an integer") from exc
|
raise RuntimeError("POLL_INTERVAL_SECONDS must be an integer") from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_timeout = int(request_timeout)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise RuntimeError("REQUEST_TIMEOUT_SECONDS must be an integer") from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_offline_after = int(offline_after_failures)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise RuntimeError("OFFLINE_AFTER_FAILURES must be an integer") from exc
|
||||||
|
|
||||||
return Settings(
|
return Settings(
|
||||||
telegram_bot_token=token,
|
telegram_bot_token=token,
|
||||||
telegram_chat_id=int(chat_id),
|
telegram_chat_id=int(chat_id),
|
||||||
@@ -48,4 +62,6 @@ def load_settings() -> Settings:
|
|||||||
minecraft_port=parsed_port,
|
minecraft_port=parsed_port,
|
||||||
poll_interval_seconds=parsed_interval,
|
poll_interval_seconds=parsed_interval,
|
||||||
status_file_path=status_file_path,
|
status_file_path=status_file_path,
|
||||||
|
request_timeout_seconds=parsed_timeout,
|
||||||
|
offline_after_failures=parsed_offline_after,
|
||||||
)
|
)
|
||||||
|
|||||||
2
main.py
2
main.py
@@ -29,6 +29,8 @@ async def main() -> None:
|
|||||||
status_file=Path(settings.status_file_path),
|
status_file=Path(settings.status_file_path),
|
||||||
poll_interval_seconds=settings.poll_interval_seconds,
|
poll_interval_seconds=settings.poll_interval_seconds,
|
||||||
activity=activity,
|
activity=activity,
|
||||||
|
request_timeout_seconds=settings.request_timeout_seconds,
|
||||||
|
offline_after_failures=settings.offline_after_failures,
|
||||||
)
|
)
|
||||||
dp.include_router(setup_handlers(monitor, activity))
|
dp.include_router(setup_handlers(monitor, activity))
|
||||||
|
|
||||||
|
|||||||
@@ -35,16 +35,25 @@ class MinecraftMonitor:
|
|||||||
status_file: Path,
|
status_file: Path,
|
||||||
poll_interval_seconds: int,
|
poll_interval_seconds: int,
|
||||||
activity: ChatActivityTracker,
|
activity: ChatActivityTracker,
|
||||||
|
request_timeout_seconds: int,
|
||||||
|
offline_after_failures: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.server = JavaServer(host, port=port)
|
self.server = JavaServer(host, port=port)
|
||||||
self.status_file = status_file
|
self.status_file = status_file
|
||||||
self.poll_interval_seconds = poll_interval_seconds
|
self.poll_interval_seconds = poll_interval_seconds
|
||||||
|
self.request_timeout_seconds = request_timeout_seconds
|
||||||
|
self.offline_after_failures = max(1, offline_after_failures)
|
||||||
self._previous_snapshot: Optional[ServerSnapshot] = None
|
self._previous_snapshot: Optional[ServerSnapshot] = None
|
||||||
self.activity = activity
|
self.activity = activity
|
||||||
|
self._consecutive_failures = 0
|
||||||
|
|
||||||
async def fetch_status(self) -> ServerSnapshot:
|
async def fetch_status(self) -> ServerSnapshot:
|
||||||
try:
|
try:
|
||||||
response = await self.server.async_status()
|
response = await asyncio.wait_for(
|
||||||
|
self.server.async_status(),
|
||||||
|
timeout=self.request_timeout_seconds,
|
||||||
|
)
|
||||||
|
self._consecutive_failures = 0
|
||||||
motd = getattr(response.description, "to_plain", lambda: None)()
|
motd = getattr(response.description, "to_plain", lambda: None)()
|
||||||
player_names = [player.name for player in response.players.sample or []]
|
player_names = [player.name for player in response.players.sample or []]
|
||||||
return ServerSnapshot(
|
return ServerSnapshot(
|
||||||
@@ -57,7 +66,20 @@ class MinecraftMonitor:
|
|||||||
player_names=player_names,
|
player_names=player_names,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Treat any exception as offline state but keep last known data safe.
|
self._consecutive_failures += 1
|
||||||
|
if self._consecutive_failures < self.offline_after_failures:
|
||||||
|
# Возвращаем последнее известное состояние, помечая время обновления.
|
||||||
|
if self._previous_snapshot:
|
||||||
|
return ServerSnapshot(
|
||||||
|
online=self._previous_snapshot.online,
|
||||||
|
motd=self._previous_snapshot.motd,
|
||||||
|
version=self._previous_snapshot.version,
|
||||||
|
latency_ms=self._previous_snapshot.latency_ms,
|
||||||
|
players_online=self._previous_snapshot.players_online,
|
||||||
|
players_max=self._previous_snapshot.players_max,
|
||||||
|
player_names=self._previous_snapshot.player_names,
|
||||||
|
)
|
||||||
|
# После порога считаем сервер офлайн.
|
||||||
return ServerSnapshot(
|
return ServerSnapshot(
|
||||||
online=False,
|
online=False,
|
||||||
motd=None,
|
motd=None,
|
||||||
|
|||||||
Reference in New Issue
Block a user