Проблема
Для API в стиле запрос/ответ мониторинг доступности давно решён: отправить запрос, проверить статус-код — и готово. Серверные стриминговые RPC (server-side streaming RPCs) ломают эту модель. Стрим — это канал, который сервер держит открытым и по которому со временем отправляет сообщения. Поэтому сокет может быть поднят, хэндшейк может завершиться успешно, health-эндпоинт может быть зелёным — а данные при этом не поступают вообще.
В реальных продакшен-топологиях всё ещё сложнее: прежде чем стрим доберётся до браузера или сервиса-потребителя, он проходит несколько промежуточных узлов:
client ──▶ LB ──▶ Envoy (grpc-web / HTTP/2) ──▶ backend service ──▶ upstream feed
Каждый узел — потенциальное место, где стрим может тихо умереть, сохраняя при этом полную видимость здоровья:
-
idle-timeout на балансировщике или прокси незаметно убивает долгоживущие соединения;
-
трансляция Envoy / grpc-web может буферизовать или дропать фреймы, пока TCP-сессия остаётся активной;
-
бэкенд держит стрим открытым, но апстрим-фид замолчал — серверу нечего отправлять;
-
стрим генерирует фреймы, но после неудачного деплоя они содержат мусор — пустые payload’ы, отсутствующие поля.
Ни один из этих сценариев не виден HTTP 200-пробе или gRPC health check. Единственная проверка, которая отвечает на вопрос «действительно ли этот стрим отдаёт валидные данные?» — это клиент, который открывает стрим, читает несколько фреймов и проверяет их содержимое. Именно этим и занимается данный чекер.
Что делает одна проверка
Для каждого стрима из конфигурации при каждом запуске чекер:
-
загружает
.proto-файл во время выполнения (без кодогенерации, без устаревших стабов — чтобы добавить стрим, достаточно указать путь к файлу); -
открывает серверный стриминговый RPC как настоящий клиент (TLS или plaintext, с опциональными auth-метаданными);
-
собирает
minFramesфреймов (по умолчанию 1; установите 3–5, чтобы проверить устойчивый поток, а не единичный первый пакет) в рамках временного бюджета, замеряя время до первого фрейма и максимальный разрыв между фреймами; -
валидирует payload каждого фрейма по заданным правилам — обязательные поля, точное совпадение, регулярные выражения;
-
отменяет стрим и записывает результат в метрики Prometheus.
Один неудачный стрим не прерывает весь запуск; каждый режим отказа получает собственную метку результата:
ok · timeout · insufficient_frames · validation_failed · error (с gRPC-статус-кодом).
Как это выглядит в продакшене
Дашборд Grafana на основе метрик чекера — наблюдение за шестью продакшен-стриминговыми методами в течение недели: процент успешных проверок по методам, количество запросов и топ-10 по длительности стрима:
Быстрый старт (60 секунд, без инфраструктуры)
git clone https://github.com/youngpabl0/grpc-streams-checker.git
cd grpc-streams-checker && npm ci
# terminal 1 — demo gRPC server with a PriceStream
node examples/demo-server.js
# terminal 2 — run the checker against it
cp streams.example.json streams.json
node src/index.js --once
# → {"stream":"price_stream","result":"ok","frames":3,"firstFrameMs":223,...}
Сломайте сервер и понаблюдайте, как чекер это замечает:
node examples/demo-server.js --silent # stream opens, no frames → result: timeout
node examples/demo-server.js --broken # frames with empty fields → result: validation_failed
node examples/demo-server.js --slow=2000 # sluggish producer → max_inter_frame_ms grows
Флаг --once завершает процесс с ненулевым кодом, если хотя бы один стрим завершился с ошибкой — удобно использовать как CI-шлюз или cron-пробу.
Конфигурация
{
"addr": "localhost:50051", // default target for all streams
"dc": "dev", // datacenter label on every metric
"tls": false, // TLS on by default; false = plaintext
"intervalSeconds": 30, // how often to re-check (daemon mode)
"firstFrameTimeoutMs": 5000, // budget for collecting minFrames
"minFrames": 3, // frames to demand per check (sustained flow)
"protoIncludeDirs": ["protos"], // resolve `import` statements inside protos
"auth": { // optional; ${VARS} expand from the environment
"token": "${GRPC_TOKEN}", // → authorization: Bearer <token>
"metadata": { "x-tenant": "${TENANT_ID}" }
},
"streams": [
{
"name": "price_stream", // metric label + --only key
"proto": "protos/price.proto", // path to the .proto file
"package": "example.v1",
"service": "PriceService",
"method": "PriceStream", // a server-side streaming RPC
"request": { "symbol": "BTCUSD" }, // request message for the call
"minFrames": 3, // per-stream override
"timeoutMs": 10000, // per-stream override
"addr": "other-host:443", // per-stream override
"tls": true, // per-stream override
"expect": { // payload validation, per frame
"fields": ["symbol", "price", "ts"], // must exist & be non-empty
"match": { "symbol": "BTCUSD" } // exact match…
// "match": { "symbol": "/^BTC/" } // …or /regex/
}
}
]
}
Переменные окружения для переопределения настроек: CONFIG, ADDR, DC, PORT (по умолчанию 4777), TIMEOUT_MS, INTERVAL_SECONDS, MIN_FRAMES, LOG_LEVEL, LOG_PRETTY=true.
Для более сложной аутентификации — JWT, AWS SigV4, mTLS — расширьте единственную точку расширения в src/auth.js.
Подключение .proto-файлов
Чекер загружает исходные .proto-файлы во время выполнения — компилировать стабы не нужно. Три распространённых способа разместить их рядом с чекером:
-
Скопировать напрямую — поместите файлы в директорию
protos/в вашем деплойменте и укажите"proto": "protos/your_service.proto". Если ваши.proto-файлы импортируют общие определения, перечислите корневые директории в"protoIncludeDirs", чтобы импорты разрешались корректно. -
Git submodule / subtree — если в вашей организации есть монорепозиторий с
.proto-файлами (что встречается часто), зафиксируйте его версию:git submodule add <proto-repo-url> protos. Тогда чекер всегда тестирует именно ту версию контракта, которую вы зафиксировали — обновите сабмодуль, чтобы протестировать новый контракт. -
Встроить в образ — в своём Dockerfile добавьте слой
COPY protos/ /app/protos/, или монтируйте директорию во время выполнения:-v $PWD/protos:/app/protos:ro.
Встроенные типы Google (well-known types, google/protobuf/.proto) входят в состав protobufjs и разрешаются автоматически; protoIncludeDirs нужен только для *ваших общих импортов.
Метрики
Доступны по адресу :4777/metrics (/healthz — для пробы живости). Метки на каждой метрике стрима: stream, method (RPC-метод — используется в дашбордах с разбивкой по методам), addr, dc.
Процент успешных проверок по методу — запрос, лежащий в основе панелей дашборда выше:
sum by (method) (rate(grpc_stream_check_total{result="ok"}[5m]))
/
sum by (method) (rate(grpc_stream_check_total[5m]))
| Метрика | Тип | Значение |
|---|---|---|
|
gauge |
1 = последняя проверка прошла успешно (фреймы получены и payload валиден) |
|
counter |
проверки по результату: |
|
counter |
ошибки по gRPC-статус-коду ( |
|
gauge |
время до первого фрейма в последней проверке |
|
histogram |
то же, для вычисления перцентилей во времени |
|
gauge |
максимальный разрыв между фреймами в рамках одной проверки — индикатор устойчивости потока |
|
gauge |
количество фреймов, полученных в последней проверке |
|
counter |
общее количество полученных фреймов за всё время |
|
counter |
ошибки валидации payload: |
|
gauge |
момент последней полностью успешной проверки стрима |
|
gauge |
момент последнего завершённого запуска чекера |
Алертинг
Готовые правила Prometheus находятся в alerts/grpc-streams-checker.rules.yml — восемь алертов с уровнями серьёзности, окнами for: и описаниями в стиле runbook. Совместимы как с обычным rule_files: в Prometheus, так и с PrometheusRule для prometheus-operator. Ключевые алерты:
| Алерт | Условие | Серьёзность |
|---|---|---|
|
|
critical |
|
первый фрейм приходит дольше 2с на протяжении 5м — деградация до наступления темноты |
warning |
|
разрыв между фреймами > 5с — продюсер не успевает |
warning |
|
ошибки валидации — данные испортились после деплоя |
critical |
|
повторяющиеся gRPC-ошибки по статус-коду |
warning |
|
ни один стрим не прошёл проверку за последние 5м |
critical |
|
сам чекер остановился — тишина не означает здоровье |
critical |
Docker
docker build -t grpc-streams-checker .
docker run -p 4777:4777 \
-v "$PWD/streams.json:/app/streams.json:ro" \
-v "$PWD/protos:/app/protos:ro" \
-e GRPC_TOKEN \
grpc-streams-checker
Kubernetes
Готовые манифесты находятся в deploy/kubernetes/:
-
checker.yaml— ConfigMap’ы (streams.json + proto-файлы), Deployment с пробами liveness/readiness, консервативными ресурсами (запросы25m/96Mi, лимиты200m/256Mi— между запусками чекер простаивает), усиленным securityContext (не-root, read-only rootfs, без capabilities) и Service, открывающий/metrics. -
servicemonitor.yaml— ServiceMonitor и обёртка PrometheusRule для prometheus-operator; группы правил берутся изalerts/grpc-streams-checker.rules.yml.
kubectl apply -f deploy/kubernetes/checker.yaml
# prometheus-operator users:
kubectl apply -f deploy/kubernetes/servicemonitor.yaml
Токены аутентификации должны храниться в Secret, экспортируемом как переменная окружения (GRPC_TOKEN) — конфигурационный файл ссылается на неё как ${GRPC_TOKEN}, поэтому никакие учётные данные никогда не попадают в ConfigMap.
Паттерн обобщается
Тот же подход — «открой, потребуй фреймы, проверь payload» — работает для любого долгоживущего транспорта с push-семантикой: WebSocket, SSE, консьюмеры очередей сообщений. Ценность состоит в том, чтобы превратить вопрос «данные действительно текут и они валидны?» в размеченные метрики, по которым вы сами себя будите, а не узнаёте об этом от пользователей.
На чём построен
Этот инструмент — тонкий, опinionated слой над отличными open-source проектами:
-
@grpc/grpc-js— чистый JS-клиент gRPC, выполняющий каждую проверку. -
@grpc/proto-loader— загрузка.proto-файлов во время выполнения (черезprotobufjs); именно он устраняет необходимость в кодогенерации. -
prom-client— метрики Prometheus для Node. -
pino— быстрое структурированное JSON-логирование. -
express— обслуживает/metricsи/healthz.
Лицензия
Apache License 2.0 — свободное использование, модификация и распространение; сохраняйте уведомление об авторских правах и файл NOTICE. Copyright 2026 Daniil Romashov.