4c3736b5fb
Раздел «Инстанс» получает живые показатели сервера (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).
178 lines
5.9 KiB
Go
178 lines
5.9 KiB
Go
package sysinfo_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"glchat/internal/sysinfo"
|
|
)
|
|
|
|
// Тесты работают на временных файлах: значения cgroup и /proc подменяются,
|
|
// поэтому проверки не зависят от окружения и работают в контейнере CI.
|
|
|
|
func writeFile(t *testing.T, dir, name, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatalf("запись %s: %v", name, err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestCPUPercentUsesCgroupQuota(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "cpu.max", "200000 100000\n") // 2 ядра
|
|
writeFile(t, dir, "cpu.stat", "usage_usec 1000000\nuser_usec 900000\nsystem_usec 100000\n")
|
|
|
|
now := time.Unix(1_700_000_000, 0)
|
|
paths := sysinfo.Paths{
|
|
ProcStat: filepath.Join(dir, "stat"),
|
|
MemInfo: filepath.Join(dir, "meminfo"),
|
|
CgroupDir: dir,
|
|
WallClock: func() time.Time { return now },
|
|
}
|
|
sampler := sysinfo.NewSampler(paths)
|
|
|
|
// Первый вызов только запоминает снимок.
|
|
if percent, _, source, err := sampler.CPUPercent(); err != nil || percent != 0 || source != "cgroup" {
|
|
t.Fatalf("первый замер = %v, %s, %v", percent, source, err)
|
|
}
|
|
|
|
// За секунду израсходована половина квоты (1 с из 2 ядро-секунд) → 50 %.
|
|
now = now.Add(time.Second)
|
|
writeFile(t, dir, "cpu.stat", "usage_usec 2000000\n")
|
|
percent, cores, source, err := sampler.CPUPercent()
|
|
if err != nil {
|
|
t.Fatalf("второй замер: %v", err)
|
|
}
|
|
if cores != 2 || source != "cgroup" {
|
|
t.Fatalf("ядер=%v источник=%s", cores, source)
|
|
}
|
|
if percent < 49 || percent > 51 {
|
|
t.Fatalf("загрузка = %.2f %%, ожидалось ~50 %%", percent)
|
|
}
|
|
}
|
|
|
|
func TestCPUPercentFallsBackToProcStat(t *testing.T) {
|
|
dir := t.TempDir()
|
|
statPath := writeFile(t, dir, "stat", "cpu 100 0 100 800 0 0 0 0 0 0\n")
|
|
writeFile(t, dir, "meminfo", "MemTotal: 1000 kB\nMemAvailable: 400 kB\n")
|
|
|
|
now := time.Unix(1_700_000_000, 0)
|
|
sampler := sysinfo.NewSampler(sysinfo.Paths{
|
|
ProcStat: statPath,
|
|
MemInfo: filepath.Join(dir, "meminfo"),
|
|
CgroupDir: filepath.Join(dir, "нет-cgroup"),
|
|
WallClock: func() time.Time { return now },
|
|
})
|
|
|
|
if _, _, source, err := sampler.CPUPercent(); err != nil || source != "host" {
|
|
t.Fatalf("источник = %s, %v", source, err)
|
|
}
|
|
|
|
// За интервал прибавилось 150 тиков занятости и 150 простоя → 50 %.
|
|
now = now.Add(time.Second)
|
|
writeFile(t, dir, "stat", "cpu 175 0 175 950 0 0 0 0 0 0\n")
|
|
percent, cores, source, err := sampler.CPUPercent()
|
|
if err != nil {
|
|
t.Fatalf("второй замер: %v", err)
|
|
}
|
|
if source != "host" || cores <= 0 {
|
|
t.Fatalf("источник=%s ядер=%v", source, cores)
|
|
}
|
|
if percent < 49 || percent > 51 {
|
|
t.Fatalf("загрузка = %.2f %%, ожидалось ~50 %%", percent)
|
|
}
|
|
}
|
|
|
|
func TestReadMemoryPrefersCgroupLimit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "memory.current", "15728640\n")
|
|
writeFile(t, dir, "memory.max", "805306368\n")
|
|
writeFile(t, dir, "meminfo", "MemTotal: 3479592 kB\nMemAvailable: 1000000 kB\n")
|
|
|
|
memory, err := sysinfo.ReadMemory(sysinfo.Paths{
|
|
MemInfo: filepath.Join(dir, "meminfo"),
|
|
CgroupDir: dir,
|
|
ProcStat: filepath.Join(dir, "stat"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("память: %v", err)
|
|
}
|
|
if memory.Source != "cgroup" || memory.TotalBytes != 805306368 || memory.UsedBytes != 15728640 {
|
|
t.Fatalf("память = %+v", memory)
|
|
}
|
|
if percent := memory.Percent(); percent < 1.9 || percent > 2.0 {
|
|
t.Fatalf("заполнение = %.2f %%", percent)
|
|
}
|
|
}
|
|
|
|
func TestReadMemoryFallsBackToMemInfo(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "meminfo", "MemTotal: 1000 kB\nMemAvailable: 250 kB\n")
|
|
|
|
memory, err := sysinfo.ReadMemory(sysinfo.Paths{
|
|
MemInfo: filepath.Join(dir, "meminfo"),
|
|
CgroupDir: filepath.Join(dir, "нет-cgroup"),
|
|
ProcStat: filepath.Join(dir, "stat"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("память: %v", err)
|
|
}
|
|
if memory.Source != "host" || memory.TotalBytes != 1024_000 || memory.UsedBytes != 768_000 {
|
|
t.Fatalf("память = %+v", memory)
|
|
}
|
|
}
|
|
|
|
func TestReadMemoryUnlimitedCgroupMax(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "memory.current", "1000\n")
|
|
writeFile(t, dir, "memory.max", "max\n")
|
|
writeFile(t, dir, "meminfo", "MemTotal: 2000 kB\nMemAvailable: 500 kB\n")
|
|
|
|
memory, err := sysinfo.ReadMemory(sysinfo.Paths{
|
|
MemInfo: filepath.Join(dir, "meminfo"),
|
|
CgroupDir: dir,
|
|
ProcStat: filepath.Join(dir, "stat"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("память: %v", err)
|
|
}
|
|
if memory.Source != "host" {
|
|
t.Fatalf("при memory.max=max ожидался хостовый режим, получено %+v", memory)
|
|
}
|
|
}
|
|
|
|
func TestReadDiskAndFileBytes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
disk, err := sysinfo.ReadDisk(dir)
|
|
if err != nil {
|
|
t.Fatalf("диск: %v", err)
|
|
}
|
|
if disk.TotalBytes <= 0 || disk.UsedBytes <= 0 || disk.FreeBytes <= 0 {
|
|
t.Fatalf("диск = %+v", disk)
|
|
}
|
|
if disk.UsedBytes+disk.FreeBytes > disk.TotalBytes {
|
|
t.Fatalf("занято+свободно больше общего: %+v", disk)
|
|
}
|
|
|
|
writeFile(t, dir, "glchat.db", "0123456789")
|
|
writeFile(t, dir, "glchat.db-wal", "01234")
|
|
if got := sysinfo.FileBytes(filepath.Join(dir, "glchat.db"), filepath.Join(dir, "glchat.db-wal"), filepath.Join(dir, "нет.db")); got != 15 {
|
|
t.Fatalf("размер файлов = %d, ожидалось 15", got)
|
|
}
|
|
}
|
|
|
|
func TestCPUPercentWithoutProcStat(t *testing.T) {
|
|
sampler := sysinfo.NewSampler(sysinfo.Paths{
|
|
ProcStat: filepath.Join(t.TempDir(), "нет"),
|
|
MemInfo: filepath.Join(t.TempDir(), "нет"),
|
|
CgroupDir: filepath.Join(t.TempDir(), "нет"),
|
|
})
|
|
if _, _, _, err := sampler.CPUPercent(); err == nil {
|
|
t.Fatal("без источников ожидалась ошибка")
|
|
}
|
|
}
|