
## Логи сервиса

`GET /v1/infra/servers/:id/logs`

Возвращает логи BLACKHOLE-сервера через системную утилиту `journalctl`. Без параметра `service` читается весь системный журнал (все сервисы и общесистемные сообщения). Для логов конкретного systemd-юнита — передайте `service` (по умолчанию при деплое создаётся `app.service`, если не указали другое имя). Поддерживает фильтрацию по количеству строк, временному окну и подстроке.

## Параметры

| Параметр | В | Тип | Обяз. | По умолч. | Описание |
|----------|---|-----|:-----:|-----------|----------|
| `id` | path | string (UUID) | да | — | ID BLACKHOLE-сервера |
| `service` | query | string | нет | — | Имя systemd-юнита: `app`, `crm-dashboard`, и т. п. Не указывайте расширение `.service`. По умолчанию читается весь системный журнал |
| `lines` | query | number | нет | 50 | Количество строк, 1–500 |
| `since` | query | string | нет | — | Временная метка в формате утилиты `journalctl`: `"1 hour ago"`, `"2026-04-22 10:00:00"`, `"10 minutes ago"` |
| `grep` | query | string | нет | — | Подстрока для фильтра, до 200 символов |

## Примеры

### curl — личный ключ

```bash
# Логи приложения за последний час с фильтром по "error"
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/logs?service=app&lines=100&since=1%20hour%20ago&grep=error"

# Весь системный журнал, последние 10 строк (для отладки cloud-init)
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/logs?lines=10"
```

### curl — OAuth-приложение

```bash
curl -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/logs?service=app&lines=50"
```

### JavaScript — личный ключ

```javascript
const params = new URLSearchParams({
  service: 'app',
  lines: '100',
  since: '30 minutes ago',
})

const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/logs?${params}`,
  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
)
const { data } = await res.json()
data.logs.forEach(line => console.log(line))
```

### JavaScript — OAuth-приложение

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/logs?service=app&lines=50`,
  {
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | Всегда `true` при успехе |
| `data.service` | string \| null | Эхо параметра `?service=`, или `null` если не передавался |
| `data.logs` | array\<string\> | **Основное поле.** Строки логов в порядке поступления (новые — в конце массива) |
| `data.lines` | array\<string\> | Алиас `data.logs` (тот же массив). Сохранён для обратной совместимости — в новом коде используйте `data.logs` |
| `data.requestedLines` | number | Эхо `?lines=` параметра (валидировано 1–500, default 50) |
| `data.returnedLines` | number | Длина `data.logs` |
| `data.since` | string \| null | Эхо `?since=`, или `null` |
| `data.grep` | string \| null | Эхо `?grep=`, или `null` |
| `data.hint` | string | Опционально. Диагностика когда `data.logs.length === 0` |

## Пример ответа

```json
{
  "success": true,
  "data": {
    "service": null,
    "logs": [
      "Apr 22 11:17:02 epd65hdv07p8g89c0c06 CRON[1406]: pam_unix(cron:session): session closed for user root",
      "Apr 22 11:17:27 epd65hdv07p8g89c0c06 vibe-agent[1098]: 2026/04/22 11:17:27 [exec] id= timeout=10s workdir=\"\" command=\"uname -a\"",
      "Apr 22 11:17:28 epd65hdv07p8g89c0c06 cloud-init[975]: Reading package lists..."
    ],
    "lines": [
      "Apr 22 11:17:02 epd65hdv07p8g89c0c06 CRON[1406]: pam_unix(cron:session): session closed for user root",
      "Apr 22 11:17:27 epd65hdv07p8g89c0c06 vibe-agent[1098]: 2026/04/22 11:17:27 [exec] id= timeout=10s workdir=\"\" command=\"uname -a\"",
      "Apr 22 11:17:28 epd65hdv07p8g89c0c06 cloud-init[975]: Reading package lists..."
    ],
    "requestedLines": 50,
    "returnedLines": 3,
    "since": null,
    "grep": null
  }
}
```

## Пример ответа при ошибке

400 — сервер в OPEN-режиме:

```json
{
  "success": false,
  "error": {
    "code": "NOT_BLACKHOLE",
    "message": "Deployment API only available for BLACKHOLE servers"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | Нарушена схема: `lines` вне 1–500, `grep` длиннее 200 символов |
| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |
| 402 | `ACCOUNT_FROZEN` | Баланс Вайбкод заморожен. Запрос отбивается до операции, спящий сервер при этом не будится — пополните баланс и повторите |
| 403 | `SERVER_WAKE_BLOCKED` | Сервер спит, а пробуждение запрещено платформой — завершённый пробный период или административная блокировка. Чтение журнала такой сервер не поднимает, повтор не поможет, пока запрет не снят. Разбор запрета — [Разбудить сервер](/docs/infra/lifecycle/wake) |
| 403 | `WRONG_KEY` | Сервер существует, но вызывающему не принадлежит. Операции над содержимым приложения — выкладка, команда, загрузка файла и чтение логов — открыты по любому из трёх оснований: управляющий ключ сервера, ключ, приложение которого привязано к этому серверу (`Application.serverId`), либо членство в команде разработки этого сервера (обе роли, «Разработчик» и «Администратор»). Токены доступа и загрузка значка требуют именно управляющего ключа. Управление самой машиной открыто ещё и роли «Администратор» команды. В ответе — `hint` с восстановлением из двух шагов. Подробнее — [Восстановление доступа к серверу](/docs/infra/server-access-recovery) |
| 404 | `NOT_FOUND` | Сервера с таким `id` нет или он удалён. Сервер, который существует, но вам не принадлежит, отвечает `403 WRONG_KEY` — код различает «нет такого сервера» и «нет прав на него» |
| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Спящий сервер этот код не описывает — его платформа будит сама. Запустите остановленный сервер вызовом [`/start`](/docs/infra/lifecycle/start) либо восстановите туннель вызовом [`/repair`](/docs/infra/lifecycle/repair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |
| 409 | `EXEC_BUSY` | Только у приложения в галактике (`kind: "GALAXY_APP"`): общий exec-канал хоста занят — журнал читается через него же. Ответ несёт заголовок `Retry-After` и поля `retryable: true` / `retryAfter`, повторите с этим интервалом. Освободить общий канал владелец приложения не может, при устойчивой занятости обратитесь в поддержку |
| 409 | `WAKE_IN_PROGRESS` | Спящий сервер уже будит другой запрос. Дождитесь его завершения и повторите |
| 422 | `VM_MISSING` | У записи сервера нет виртуальной машины у облачного провайдера, поэтому будить нечего. Удалите сервер и создайте новый |
| 502 | `WAKE_FAILED` | Во время пробуждения спящего сервера он перешёл в неожиданное состояние. Повторите запрос |
| 502 | `PROVIDER_ERROR` | Облачный провайдер вернул ошибку при запуске виртуальной машины спящего сервера. Туннель здесь ни при чём — вызов [`/repair`](/docs/infra/lifecycle/repair) не поможет |
| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |
| 502 | `TUNNEL_NOT_FOUND` / `GATEWAY_UNREACHABLE: …` | Нет живого туннеля до сервера либо Gateway недоступен — вызовите [`/repair`](/docs/infra/lifecycle/repair) и повторите. `GATEWAY_UNREACHABLE` несёт детали после двоеточия, поэтому сопоставляйте `error.code` по префиксу, а не по точному совпадению |
| 503 | `GATEWAY_TIMEOUT: …` | Gateway не ответил вовремя. Код тоже несёт детали после двоеточия — сравнивайте по префиксу |
| 503 | `WAKE_TIMEOUT` | Спящий сервер не поднялся за отведённые 6.5 минуты — машина не запустилась или туннель не подключился. Сервер возвращается в `sleeping`, поэтому повторный запрос безопасен. См. «Известные особенности» |
| 502 | `LOGS_UNAVAILABLE` | Агент не смог прочитать системный журнал: нет такого юнита, неверный фильтр, отказано в доступе или `journalctl` вернул ошибку |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

- **Спящую отдельную виртуальную машину чтение журнала будит.** Если сервер (`kind: "STANDALONE"`) в статусе `sleeping`, платформа запускает пробуждение и ждёт его в том же запросе — до 6.5 минут, и только потом читает журнал. Клиентский таймаут запроса это время обязан пережить. Если сервер не поднялся, приходит `503 WAKE_TIMEOUT` и сервер возвращается в `sleeping`, повторный запрос безопасен. У galaxy-приложения чтение ведёт себя иначе — см. раздел «Galaxy-приложения» ниже.
- **Формат строк — как выдаёт `journalctl`.** Каждая строка уже содержит время, имя хоста, имя процесса и его PID — дополнительная обработка на клиенте не нужна, выводите как есть.
- **Фильтр `grep` — простая подстрока, не регулярное выражение.** Для сложных паттернов выгружайте больше строк и фильтруйте на клиенте.
- **Логи агента туннеля видны без `service`** — под именем `vibe-agent`. Полезно для диагностики, когда приложение не отвечает через Deploy API. Просто не передавайте параметр `service` — увидите весь системный журнал, включая `vibe-agent`, cloud-init и другие общесистемные сообщения.
- **Лимит 500 строк помножен на ограничение 10 вызовов в минуту** — за минуту можно прочитать максимум 5000 строк. Для большего объёма логов — несколько вызовов с разными `since` и аккуратно к лимиту.

## Galaxy-приложения

Если сервер — galaxy-приложение (`kind=GALAXY_APP`), эндпоинт возвращает не системный журнал хоста, а stdout/stderr самого контейнера приложения (`docker logs`). Параметр `service` для galaxy-приложений не применяется.

- **Чтение не будит машину.** Пока хост-галактика спит или недоступен, ответ приходит со статусом `200`, пустым массивом `data.logs` и двумя диагностическими полями — `data.hint` и `data.recovery`. Поднимите приложение вызовом [`POST /v1/infra/servers/:id/wake`](/docs/infra/lifecycle/wake) и повторите запрос: пробуждение запускает тот же контейнер, поэтому строки, записанные до сна, остаются в журнале. Холодная галактика загружается минутами, её бюджет приходит в поле `recovery.coldBootBudgetSeconds`, а готовность проверяется опросом [`GET /v1/infra/servers/:id`](/docs/infra/servers/get) по полю `reachability.effectiveStatus`. Разбор всех полей ответа и порядок действий — [Сон и пробуждение Galaxy-приложения](/docs/infra/galaxy-sleep).
- **`since` — длительность или RFC3339, не относительная текстовая форма.** Для galaxy-приложений `since` принимает только длительность в секундах/минутах/часах (`10m`, `2h`, `24h`) или метку времени RFC3339 (`2026-06-24T10:00:00Z`) — для интервала в сутки используйте `24h`. Относительные текстовые формы `journalctl` (`"1 hour ago"`, `"10 minutes ago"`) допустимы только для Black Hole-серверов — для galaxy-приложения такое значение вернёт `400 VALIDATION_ERROR`.
- **`grep`** работает так же — простая подстрока, фильтрация на стороне платформы.

## Смотрите также

- [Полный деплой](./deploy.md)
- [Выполнить команду](./exec.md)
- [Метрики туннеля](./metrics.md)
