Блог и Новости is*hosting - Хостинг-провайдер Нового Поколения

CLI для мониторинга VPS на Go: практическое руководство

Written by Команда is*hosting | 07.10.2025 8:00:01

У вас парк VPS. Пять, пятьдесят — неважно. В какой-то момент каждый инженер упирается в одно и то же: где кончилось место, кто загрузил CPU на максимум, а кто простаивает?

Ставить на каждый сервер тяжелый мониторинг ради пары метрик — удовольствие сомнительное. Особенно поймут DevOps-инженеры, отвечающие за десяток проектов, и независимые разработчикм с сетью edge-нод…

Поэтому мы покажем, как быстро собрать свой инструмент: легкий CLI на Go, который достает ключевые показатели прямо из API is*hosting. Минимум лишнего — только то, что нужно видеть в 3 ночи, когда сервер «вдруг» решил подвиснуть.

Что потребуется:

  • Базовые знания Go (без лишних сложностей).
  • Аккаунт в is*hosting.
  • Хотя бы один активный VPS.

Подготовка VPS

Если у вас уже есть VPS от is*hosting — этот шаг можно пропустить и сразу переходить к созданию API-ключа. Если нет, закажите новый: выберите подходящий тариф VPS и пройдите процесс заказа в панели управления.

После этого откройте Client Area и перейдите в раздел API в меню слева. Нажмите кнопку Add API key, придумайте название для ключа (например, default для первого ключа) и сохраните его.

Для целей этого проекта достаточно сохранить ключ в переменной окружения. В реальных условиях лучше использовать менеджер секретов.

На своей машине задайте переменную окружения с именем ISHOSTING_API_KEY. Именно к ней будет обращаться код:

Теперь ваш VPS готов к мониторингу. Следующий шаг — настройка проекта на Go.

Настройка проекта на Go

Создайте новый каталог для проекта, перейдите в него и инициализируйте модуль Go:


go mod init <module-path>

где <module-path> может быть чем-то вроде github.com/yourusername/vps-monitor, если вы планируете поделиться им позже, или просто vps-monitor если он останется на вашем компьютере.

⚠️ Используйте Go 1.21 или новее — именно в этой версии добавлена встроенная функция min(), которая применяется в примерах.

Теперь создайте пустой файл main.go, который мы будем постепенно наполнять кодом. Первым шагом добавим импорты и константу:


package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/url"
    "os"
    "text/tabwriter"
    "time"
)

const (
    baseURL = "https://api.ishosting.com"
)

Далее определим несколько типов, начиная с VPSList. Этот тип представляет ответ API на запрос к эндпоинту /vps/list. (Подробнее о нем чуть позже.)

Чтобы не писать типы вручную, можно воспользоваться сокращенным путем: получить JSON-ответ из документации API по эндпоинту /vps/list и прогнать его через инструмент вроде JSON-to-Go, который автоматически создаст описание структур.

Если нужно сгенерировать код для большего числа эндпоинтов is*hosting API, можно использовать его полную спецификацию совместно с инструментами openapi-codegen или ogen.

Ниже приведен тип VPSList — это срез структур с вложенными структурами:


type VPSList []struct {
    ID         string     `json:"id"`
    Name       string     `json:"name"`
    Tags       []string   `json:"tags"`
    Location   Location   `json:"location"`
    Plan       Plan       `json:"plan"`
    Platform   Platform   `json:"platform"`
    Status     Status     `json:"status"`
    CreatedAt  int        `json:"created_at"`
    UpdatedAt  int        `json:"updated_at"`
}

type Location struct {
    Name string `json:"name"`
    Code string `json:"code"`
}

type Plan struct {
    Name string `json:"name"`
    Code string `json:"code"`
}

type Platform struct {
    Name string `json:"name"`
    Code string `json:"code"`
}

type Status struct {
    Name     string  `json:"name"`
    Code     string  `json:"code"`
    Message  string  `json:"message"`
    Uptime   int     `json:"uptime"`
    CPU      CPU     `json:"cpu"`
    RAM      RAM     `json:"ram"`
    Drive    Drive   `json:"drive"`
    Network  Network `json:"network"`
    State    State   `json:"state"`
}

type CPU struct {
    Value   string `json:"value"`
    Message any    `json:"message"`
}

type RAM struct {
    Value   string `json:"value"`
    Message any    `json:"message"`
}

type Drive struct {
    Value   string `json:"value"`
    Message string `json:"message"`
}

type Network struct {
    Value   string `json:"value"`
    Message any    `json:"message"`
}

type State struct {
    Name    string `json:"name"`
    Code    string `json:"code"`
    Message any    `json:"message"`
}

Для работы с ошибками определим новый тип HTTPStatusError. Он позволяет захватывать HTTP-статус (код и строку) вместе с телом ответа, которое может содержать дополнительную информацию об ошибке.


type HTTPStatusError struct {
    Code   int    // e.g., 200
    Status string // e.g., "200 OK"
    Body   []byte
}

func (e HTTPStatusError) Error() string {
    return fmt.Sprintf("HTTP %s", e.Status)
}

А вот отсюда сожно приступить к реализации API-клиента для конечной точки /vps/list.

Отправка аутентифицированных GET-запросов

Определим минималистичный клиент для работы с API is*hosting. Он хранит базовый URL, API-ключ и HTTP-клиент с таймаутом 30 секунд. Затем добавим метод Get() для отправки запросов с заголовком X-Api-Token, а также Accept и нестандартным User-Agent.


type IshostingClient struct {
    baseURL string
    apiKey  string
    client  *http.Client
}

func NewIshostingClient(baseURL, key string) *IshostingClient {
    return &IshostingClient{
        baseURL: baseURL,
        apiKey:  key,
        client: &http.Client{
            Timeout: 30 * time.Second,
        },
    }
}

IshostingClient управляет базовым URL (на случай, если он изменится), API-ключом и встроенным HTTP-клиентом с таймаутом 30 секунд, чтобы запросы не зависали бесконечно.

Метод Get() принимает полный URL (база + эндпоинт + query-параметры), выставляет заголовок X-Api-Token для аутентификации, а также Accept и User-Agent.

Нюанс: некоторые системы безопасности помечают дефолтный User-Agent Go как «подозрительный», поэтому задайте уникальный User-Agent.

Метод отправляет запрос, читает тело ответа и возвращает его. Если код статуса указывает на ошибку, возвращается HTTPStatusError с кодом, строкой статуса и телом ответа.


func (c *IshostingClient) Get(query string) (body []byte, err error) {
    req, err := http.NewRequest("GET", query, nil)
    if err != nil {
        return nil, fmt.Errorf("cannot create request: %w", err)
    }

    // set request headers for authentication and accepted format
    req.Header.Add("X-Api-Token", c.apiKey)
    req.Header.Add("Accept", "application/json")
    req.Header.Add("User-Agent", "go-ishosting-api-client/1.0")

    // send the request
    resp, err := c.client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("cannot send request: %w", err)
    }
    defer resp.Body.Close()

    // read the response
    body, err = io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("reading response failed: %w", err)
    }

    // check for HTTP errors
    if resp.StatusCode >= 400 {
        return nil, HTTPStatusError{
            Code:   resp.StatusCode,
            Status: resp.Status,
            Body:   body,
        }
    }

    return body, nil
}

Этот метод уже позволяет отправлять запросы к эндпоинтам is*hosting, но использовать его напрямую не планируется — дальше мы напишем специальный метод для вызова /vps/list.

Получение списка VPS и статусов

Эндпоинт /vps/list возвращает список всех ваших VPS вместе с полезной мониторинговой информацией — текущей загрузкой CPU, RAM, диска и т. п. Одного API-запроса достаточно, чтобы получить срез состояния по всем серверам.

Метод VpsList() собирает эти данные и возвращает их в виде VPSList. Для производительности API разбивает большие выборки на «страницы». Параметром limit задается количество VPS на страницу; чтобы получить следующую страницу, увеличивайте offset на 1.

Чтобы избежать ошибок при ручной сборке URL-строк, используем пакет net/url и формируем запрос типобезопасно:


func (c *IshostingClient) VpsList(limit, offset int) (list VPSList, err error) {
    // construct the query URL, starting with the base URL
    query, err := url.Parse(c.baseURL)
    if err != nil {
        return nil, fmt.Errorf("url.Parse: %w", err)
    }

    // add the endpoint
    query = query.JoinPath("/vps/list")

    // add query parameters
    params := url.Values{}
    params.Add("limit", fmt.Sprint(limit))
    if offset > 0 {
        params.Add("offset", fmt.Sprint(offset))
    }
    query.RawQuery = params.Encode()

    // pass the query URL to Get()
    body, err := c.Get(query.String())
    if err != nil {
        return nil, fmt.Errorf("GET /vps/list: %w", err)
    }

    // turn the returned JSON information into a VPSList slice
    err = json.Unmarshal(body, &list)
    if err != nil {
        return nil, fmt.Errorf("unmarshal VPS list: %w", err)
    }

    return list, nil
}

С методами Get() и VpsList() у нас есть все, чтобы получать мониторинговые данные; дальше — про вывод результатов в терминал.

Отображение данных в терминале

Эндпоинт /vps/list возвращает JSON-объект, но для мониторинга удобнее видеть результаты прямо в терминале. Для этого напишем функцию PrintVPSStats(), которая с помощью пакета text/tabwriter выведет таблицу со всеми ключевыми параметрами:


func PrintVPSStats(vpsList VPSList) {
    w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
    defer w.Flush()

    // Print header
    fmt.Fprintln(w, "ID\tName\tLocation\tStatus Name\tStatus Message\tCPU\tRAM\tDrive\tNetwork\tState")
    fmt.Fprintln(w, "---\t---\t---\t---\t---\t---\t---\t---\t---\t---")

    // Print each VPS information
    for _, vps := range vpsList {
        fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
            vps.ID,
            vps.Name,
            vps.Location.Name,
            vps.Status.Name,
            vps.Status.Message,
            vps.Status.CPU.Value,
            vps.Status.RAM.Value,
            vps.Status.Drive.Value,
            vps.Status.Network.Value,
            vps.Status.State.Name,
        )
    }
}

Финальное связывание и первый тестовый запуск

Осталось всего ничего:

  • считать API-ключ из переменной окружения;
  • создать IshostingClient;
  • вызвать VpsList();
  • вывести результаты таблицей.

Все это делается в main():


func main() {
    key := os.Getenv("ISHOSTING_API_KEY")
    if len(key) == 0 {
        log.Fatal("Please set ISHOSTING_API_KEY")
    }

    c := NewIshostingClient(baseURL, key)
    list, err := c.VpsList(10, 0)
    if err != nil {
        var httpErr HTTPStatusError
        if errors.As(err, &httpErr) {
            log.Fatalf("HTTP %s with body %s", httpErr.Status, string(httpErr.Body)[:min(len(httpErr.Body), 100)])
        }
        log.Fatalf("cannot list VPSs: %v", err)
    }

    PrintVPSStats(list)
}

Код готов к запуску. Можно либо последовательно вставить его в ваш main.go, либо взять целиком из песочницы Go (учтите, что без переменной окружения пример там не запустится, и не вставляйте API-ключ в публичные сервисы).

Долгожданный запуск:


go run .

Если у вас, например, один VPS в Германии и другой VPS в Нидерландах, вывод будет похож на:


ID           Name                 Location             Status Name  Status Message                CPU   RAM  Drive   Network      State
---          ---                  ---                  ---           ---                          ---   ---  ---     ---          ---
DE_12356212_3  HSE Group Servers  Germany              Running       Uptime: 0 Days, 18:01:34     0.1%  52%  19 Gb   ~34,5 Mb/s   Active
NL_12356212_3  HSE Group Servers  Nederland            Running       Uptime: 0 Days, 18:01:34     2.4%  34%  12 Gb   ~5,1 Mb/s    Active

Выводы и следующие шаги

Несколько десятков строк Go — и у вас уже свой мониторинг VPS. Работает быстро, показывает честные цифры, без красивых дашбордов и лишней нагрузки. Хотите больше? Добавьте:

  • Постраничную выборку — цикл для получения информации о более чем 10 VPS (батчами по 10).
  • Цветной вывод — подсветка статусов и порогов загрузки в терминале.
  • Автообновление — периодический опрос (каждые n минут) с обновлением экрана.

Это база. Дальше — ваш выбор: расширять под продакшен или оставить минимализм. Главное, что теперь вы сами управляете мониторингом, а не наоборот.