feat(instance): метрики и healthcheck инстанса для дашборда

Раздел «Инстанс» получает живые показатели сервера (AGENT.md 7.19).

- `internal/sysinfo`: загрузка процессора по cgroup v2 (`cpu.stat`/`cpu.max`)
  с откатом на `/proc/stat`, память по лимиту контейнера либо `/proc/meminfo`,
  место на разделе данных и размер базы вместе с журналом WAL;
- `GET /api/v1/instance/metrics` (только администратор инстанса): метрики,
  состояние (`ok|warn|failed`), проверки app/database/disk/storage/voice/
  gateway, версия и время работы; недоступные источники не ломают ответ, а
  попадают в проверку `metrics`;
- тяжёлые проверки (БД, SFU, запись) кэшируются на 5 секунд: ручку можно
  опрашивать раз в секунду;
- `voice.IssueRoomList` и `AdminClient.Ping` проверяют доступность RoomService
  и валидность ключей LiveKit (та же связка, что ломала модерацию);
- у метрик свой лимит частоты, они исключены из общего лимита API
  (`RateLimiter.MiddlewareExcept`), иначе открытый раздел съедал бы половину
  бюджета запросов;
- тесты: `internal/sysinfo` (cgroup, /proc, память, диск, размеры файлов) и
  `internal/server/api_metrics_test.go` (доступ, состав метрик, кэш, 429).
This commit is contained in:
2026-09-20 21:26:17 +03:00
parent 8ae8ca1a1d
commit 4c3736b5fb
8 changed files with 1128 additions and 19 deletions
+21
View File
@@ -124,6 +124,27 @@ func (l *RateLimiter) Middleware(keyFn func(*http.Request) string) Middleware {
}
}
// MiddlewareExcept ограничивает частоту запросов везде, кроме перечисленных
// путей: у живого дашборда инстанса свой лимит, иначе опрос раз в секунду
// съедал бы половину общего бюджета API (AGENT.md 8.6).
func (l *RateLimiter) MiddlewareExcept(keyFn func(*http.Request) string, except ...string) Middleware {
skip := make(map[string]struct{}, len(except))
for _, path := range except {
skip[path] = struct{}{}
}
limited := l.Middleware(keyFn)
return func(next http.Handler) http.Handler {
handler := limited(next)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := skip[r.URL.Path]; ok {
next.ServeHTTP(w, r)
return
}
handler.ServeHTTP(w, r)
})
}
}
// WriteRateLimited отдаёт 429 в конверте API с подсказкой по паузе (§8.5, §8.6).
func WriteRateLimited(w http.ResponseWriter, retryAfter time.Duration) {
milliseconds := retryAfter.Milliseconds()