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:
@@ -0,0 +1,378 @@
|
||||
// Package sysinfo читает нагрузку хоста и контейнера для дашборда инстанса:
|
||||
// процессор, память, диск и размеры файлов. Значения берутся из cgroup v2
|
||||
// (контейнер ограничен профилем установки), а если лимитов нет — из /proc.
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// userHZ — тиков в секунду для /proc/stat: в Linux это всегда 100, независимо
|
||||
// от CONFIG_HZ ядра.
|
||||
const userHZ = 100
|
||||
|
||||
// Paths — пути к источникам данных. Вынесены в структуру, чтобы тесты
|
||||
// подменяли их временными файлами, а не зависели от окружения.
|
||||
type Paths struct {
|
||||
ProcStat string
|
||||
MemInfo string
|
||||
CgroupDir string
|
||||
WallClock func() time.Time
|
||||
}
|
||||
|
||||
// DefaultPaths возвращает источники по умолчанию для Linux-контейнера.
|
||||
func DefaultPaths() Paths {
|
||||
return Paths{
|
||||
ProcStat: "/proc/stat",
|
||||
MemInfo: "/proc/meminfo",
|
||||
CgroupDir: "/sys/fs/cgroup",
|
||||
WallClock: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// CPUSample — снимок процессорного времени.
|
||||
type CPUSample struct {
|
||||
// UsageMicros — накопленное использованное время.
|
||||
UsageMicros float64
|
||||
// TotalMicros — накопленное доступное время (режим хоста); в режиме
|
||||
// cgroup не используется, там ёмкость считается по квоте и времени.
|
||||
TotalMicros float64
|
||||
// QuotaPerSecond — доступное время в микросекундах за секунду (cgroup).
|
||||
QuotaPerSecond float64
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// Sampler считает загрузку процессора между вызовами: вызовы раз в секунду
|
||||
// дают нагрузку за последнюю секунду, редкие вызовы — среднее за паузу.
|
||||
type Sampler struct {
|
||||
paths Paths
|
||||
|
||||
mu sync.Mutex
|
||||
last *CPUSample
|
||||
}
|
||||
|
||||
// NewSampler создаёт сэмплер процессорного времени.
|
||||
func NewSampler(paths Paths) *Sampler {
|
||||
return &Sampler{paths: paths}
|
||||
}
|
||||
|
||||
// CPUPercent возвращает загрузку в процентах от доступного времени и число
|
||||
// ядер. Первый вызов без предыдущего снимка возвращает 0: дельты ещё нет.
|
||||
func (s *Sampler) CPUPercent() (percent float64, cores float64, source string, err error) {
|
||||
sample, cores, source, err := s.readCPU()
|
||||
if err != nil {
|
||||
return 0, cores, source, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
previous := s.last
|
||||
s.last = &sample
|
||||
s.mu.Unlock()
|
||||
|
||||
if previous == nil {
|
||||
return 0, cores, source, nil
|
||||
}
|
||||
used := sample.UsageMicros - previous.UsageMicros
|
||||
if used < 0 {
|
||||
used = 0
|
||||
}
|
||||
elapsed := sample.At.Sub(previous.At).Seconds()
|
||||
if elapsed <= 0 {
|
||||
return 0, cores, source, nil
|
||||
}
|
||||
|
||||
var capacity float64
|
||||
if sample.QuotaPerSecond > 0 {
|
||||
// Контейнер с лимитом: доступное время = квота × прошедшее время.
|
||||
capacity = sample.QuotaPerSecond * elapsed
|
||||
} else {
|
||||
// Вся машина: доступное время = прирост суммарных тиков всех ядер.
|
||||
capacity = sample.TotalMicros - previous.TotalMicros
|
||||
}
|
||||
if capacity <= 0 {
|
||||
return 0, cores, source, nil
|
||||
}
|
||||
return used / capacity * 100, cores, source, nil
|
||||
}
|
||||
|
||||
// readCPU предпочитает cgroup v2: он показывает нагрузку контейнера и его
|
||||
// лимит, а /proc/stat — всю машину целиком.
|
||||
func (s *Sampler) readCPU() (CPUSample, float64, string, error) {
|
||||
if sample, cores, ok := s.readCgroupCPU(); ok {
|
||||
return sample, cores, "cgroup", nil
|
||||
}
|
||||
sample, cores, err := s.readProcStat()
|
||||
if err != nil {
|
||||
return CPUSample{}, 0, "", err
|
||||
}
|
||||
return sample, cores, "host", nil
|
||||
}
|
||||
|
||||
// readCgroupCPU читает cpu.stat и cpu.max (cgroup v2). Квота задаётся парой
|
||||
// «quota period»; значение «max» означает отсутствие лимита.
|
||||
func (s *Sampler) readCgroupCPU() (CPUSample, float64, bool) {
|
||||
usage, err := readMicros(filepath.Join(s.paths.CgroupDir, "cpu.stat"), "usage_usec")
|
||||
if err != nil {
|
||||
return CPUSample{}, 0, false
|
||||
}
|
||||
cores, ok := s.cpuQuota()
|
||||
if !ok {
|
||||
return CPUSample{}, 0, false
|
||||
}
|
||||
return CPUSample{
|
||||
UsageMicros: usage,
|
||||
QuotaPerSecond: cores * 1e6,
|
||||
At: s.now(),
|
||||
}, cores, true
|
||||
}
|
||||
|
||||
// cpuQuota возвращает число доступных ядер по cpu.max.
|
||||
func (s *Sampler) cpuQuota() (float64, bool) {
|
||||
raw, err := os.ReadFile(filepath.Join(s.paths.CgroupDir, "cpu.max"))
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
fields := strings.Fields(string(raw))
|
||||
if len(fields) != 2 || fields[0] == "max" {
|
||||
return 0, false
|
||||
}
|
||||
quota, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
period, err := strconv.ParseFloat(fields[1], 64)
|
||||
if err != nil || period <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
cores := quota / period
|
||||
if cores <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
return cores, true
|
||||
}
|
||||
|
||||
// readProcStat считает загрузку всей машины по первой строке /proc/stat.
|
||||
func (s *Sampler) readProcStat() (CPUSample, float64, error) {
|
||||
file, err := os.Open(s.paths.ProcStat)
|
||||
if err != nil {
|
||||
return CPUSample{}, 0, fmt.Errorf("sysinfo.cpu: %w", err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 5 || fields[0] != "cpu" {
|
||||
continue
|
||||
}
|
||||
var total, idle float64
|
||||
for index, field := range fields[1:] {
|
||||
value, err := strconv.ParseFloat(field, 64)
|
||||
if err != nil {
|
||||
return CPUSample{}, 0, fmt.Errorf("sysinfo.cpu: %w", err)
|
||||
}
|
||||
total += value
|
||||
// idle и iowait — время, когда процессор не работал.
|
||||
if index == 3 || index == 4 {
|
||||
idle += value
|
||||
}
|
||||
}
|
||||
if total <= 0 {
|
||||
return CPUSample{}, 0, errors.New("sysinfo.cpu: пустой счётчик")
|
||||
}
|
||||
return CPUSample{
|
||||
UsageMicros: ticksToMicros(total - idle),
|
||||
TotalMicros: ticksToMicros(total),
|
||||
At: s.now(),
|
||||
}, float64(runtime.NumCPU()), nil
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return CPUSample{}, 0, fmt.Errorf("sysinfo.cpu: %w", err)
|
||||
}
|
||||
return CPUSample{}, 0, errors.New("sysinfo.cpu: строка cpu не найдена")
|
||||
}
|
||||
|
||||
func (s *Sampler) now() time.Time {
|
||||
if s.paths.WallClock == nil {
|
||||
return time.Now()
|
||||
}
|
||||
return s.paths.WallClock()
|
||||
}
|
||||
|
||||
// Memory — занятая и доступная память в байтах.
|
||||
type Memory struct {
|
||||
UsedBytes int64
|
||||
TotalBytes int64
|
||||
Source string
|
||||
}
|
||||
|
||||
// Percent возвращает заполнение памяти в процентах.
|
||||
func (m Memory) Percent() float64 {
|
||||
if m.TotalBytes <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(m.UsedBytes) / float64(m.TotalBytes) * 100
|
||||
}
|
||||
|
||||
// ReadMemory отдаёт лимит и потребление контейнера (cgroup v2), а при его
|
||||
// отсутствии — память всей машины по /proc/meminfo.
|
||||
func ReadMemory(paths Paths) (Memory, error) {
|
||||
if memory, ok := readCgroupMemory(paths.CgroupDir); ok {
|
||||
return memory, nil
|
||||
}
|
||||
return readMemInfo(paths.MemInfo)
|
||||
}
|
||||
|
||||
func readCgroupMemory(dir string) (Memory, bool) {
|
||||
current, err := readInt(filepath.Join(dir, "memory.current"))
|
||||
if err != nil {
|
||||
return Memory{}, false
|
||||
}
|
||||
total, err := readInt(filepath.Join(dir, "memory.max"))
|
||||
if err != nil || total <= 0 {
|
||||
// memory.max = "max" или файла нет: лимит не выставлен.
|
||||
return Memory{}, false
|
||||
}
|
||||
return Memory{UsedBytes: current, TotalBytes: total, Source: "cgroup"}, true
|
||||
}
|
||||
|
||||
func readMemInfo(path string) (Memory, error) {
|
||||
file, err := os.Open(path) //nolint:gosec // путь задаёт сам инстанс (по умолчанию /proc/meminfo)
|
||||
if err != nil {
|
||||
return Memory{}, fmt.Errorf("sysinfo.memory: %w", err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
var total, available int64
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
value, err := strconv.ParseInt(fields[1], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSuffix(fields[0], ":") {
|
||||
case "MemTotal":
|
||||
total = value * 1024
|
||||
case "MemAvailable":
|
||||
available = value * 1024
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return Memory{}, fmt.Errorf("sysinfo.memory: %w", err)
|
||||
}
|
||||
if total <= 0 {
|
||||
return Memory{}, errors.New("sysinfo.memory: MemTotal не найден")
|
||||
}
|
||||
return Memory{UsedBytes: total - available, TotalBytes: total, Source: "host"}, nil
|
||||
}
|
||||
|
||||
// Disk — занятое и общее место раздела, на котором лежат данные инстанса.
|
||||
type Disk struct {
|
||||
UsedBytes int64
|
||||
TotalBytes int64
|
||||
FreeBytes int64
|
||||
}
|
||||
|
||||
// ReadDisk считает место по пути данных (в контейнере это примонтированный
|
||||
// каталог хоста).
|
||||
func ReadDisk(path string) (Disk, error) {
|
||||
var stats syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stats); err != nil {
|
||||
return Disk{}, fmt.Errorf("sysinfo.disk: %w", err)
|
||||
}
|
||||
blockSize := toInt64From32(stats.Bsize)
|
||||
total := saturatingMul(toInt64(stats.Blocks), blockSize)
|
||||
free := saturatingMul(toInt64(stats.Bavail), blockSize)
|
||||
return Disk{UsedBytes: total - free, TotalBytes: total, FreeBytes: free}, nil
|
||||
}
|
||||
|
||||
// FileBytes суммирует размеры существующих файлов: отсутствующие считаются
|
||||
// нулём, поэтому WAL и SHM можно передавать всегда.
|
||||
func FileBytes(paths ...string) int64 {
|
||||
var total int64
|
||||
for _, path := range paths {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
total += info.Size()
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// toInt64 переводит беззнаковый размер в int64 с насыщением: значения statfs
|
||||
// для реальных разделов далеки от предела, но переполнение недопустимо.
|
||||
func toInt64(value uint64) int64 {
|
||||
if value > math.MaxInt64 {
|
||||
return math.MaxInt64
|
||||
}
|
||||
return int64(value)
|
||||
}
|
||||
|
||||
// toInt64From32 переводит 32-битное беззнаковое значение (размер блока statfs).
|
||||
func toInt64From32(value uint32) int64 {
|
||||
return int64(value)
|
||||
}
|
||||
|
||||
// saturatingMul умножает с насыщением: размер раздела не должен переполняться.
|
||||
func saturatingMul(a, b int64) int64 {
|
||||
if a <= 0 || b <= 0 {
|
||||
return 0
|
||||
}
|
||||
if a > math.MaxInt64/b {
|
||||
return math.MaxInt64
|
||||
}
|
||||
return a * b
|
||||
}
|
||||
|
||||
func ticksToMicros(ticks float64) float64 {
|
||||
return ticks / userHZ * 1e6
|
||||
}
|
||||
|
||||
func readInt(path string) (int64, error) {
|
||||
raw, err := os.ReadFile(path) //nolint:gosec // путь задаёт сам инстанс (cgroup/proc или временный каталог в тестах)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.ParseInt(strings.TrimSpace(string(raw)), 10, 64)
|
||||
}
|
||||
|
||||
func readMicros(path, key string) (float64, error) {
|
||||
file, err := os.Open(path) //nolint:gosec // путь задаёт сам инстанс (cgroup/proc)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) != 2 || fields[0] != key {
|
||||
continue
|
||||
}
|
||||
value, err := strconv.ParseFloat(fields[1], 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, fmt.Errorf("sysinfo: %s не найден в %s", key, path)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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("без источников ожидалась ошибка")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user