funcnode-app
funcnode-app
Переиспользуемый «скелет» долгоживущего Crystal-сервиса (shard, namespace Funcnode::App). Забирает на себя всё, что повторяется от сервиса к сервису:
- Конфиг — YAML-файл, загружается первым при старте; если файла нет — создаётся автоматически с дефолтами и комментариями. Существующий файл никогда не перезаписывается. Опечатки в ключах вызывают предупреждение; невалидный YAML — внятное сообщение и выход с кодом 1 (без backtrace).
- Логи — STDOUT/STDERR перенаправляются в файлы (append) на уровне файлового дескриптора: в файлы попадает всё, включая backtrace необработанных исключений.
- Жизненный цикл — HTTP-сервер, обработка сигналов остановки, graceful shutdown с ожиданием (drain) текущих запросов.
- Диагностика — встроенный
GET /healthс версией приложения.
Приложение задаёт только: имя, версию, свой конфиг-тип, набор роутов и (опционально) протокол сервера. Роутер, БД, сериализация тел, аутентификация и метрики — слой приложений, не библиотеки. Зависимости — только stdlib (http/server, log, yaml). Кроссплатформенно: разработка на Windows, production на Linux; корректно при сборке с -Dpreview_mt.
Подробная документация (включая справочник API и changelog) — в doc/index.html.
Установка
shard.yml приложения:
dependencies:
funcnode-app:
git: <url-репозитория-funcnode-app>
version: ~> 0.2
Использование
require "funcnode-app"
module Hello
VERSION = "0.1.0"
struct Config < Funcnode::App::BaseConfig
# Свой ключ приложения (дефолт работает без файла конфига)
getter greeting : String = "hello"
end
class App < Funcnode::App::Application(Config)
def name : String
"hello"
end
def version : String
VERSION
end
def handle(ctx : HTTP::Server::Context)
ctx.response.content_type = "application/json"
case {ctx.request.method, ctx.request.path}
when {"GET", "/greet"}
ctx.response.print %({"message":"#{config.greeting}"})
else
ctx.response.status = :not_found
ctx.response.print %({"error":"not_found"})
end
end
def default_config_yaml : String
super + <<-YAML
# Приветствие для GET /greet
greeting: hello
YAML
end
end
end
Hello::App.new.run
Запускаемый вариант этого примера — examples/hello.cr.
Что настраивается
Обязательные методы (компилятор не даст забыть): name, version, handle.
Опциональные переопределения с дефолтами:
| Метод | Дефолт |
|---|---|
config_path |
./config/<name>.yaml |
default_config_yaml |
ключ logs_dir с комментариями; расширять через super + "..." |
env_prefix |
name.upcase, не-алфанумерика → _ (my-svc → MY_SVC) |
host |
ENV <PREFIX>_HOST, иначе 127.0.0.1 |
port |
ENV <PREFIX>_PORT, иначе 8080 |
drain_timeout |
ENV <PREFIX>_DRAIN_TIMEOUT (сек), иначе 25 c — держать МЕНЬШЕ TimeoutStopSec systemd-unit'а |
build_server |
HTTP::Server.new — точка расширения под свою handler-цепочку |
bind |
bind_tcp(host, port) — переопределить для unix-сокета, TLS, нескольких адресов |
health_path |
"/health"; nil — отключить перехват healthcheck |
handle_health |
{"status":"ok","version":"..."} |
on_start / on_stop |
хуки: после bind до listen (пулы, миграции) / после drain перед выходом |
GET <health_path> перехватывается до handle — приложение получает его бесплатно и не может случайно потерять.
Остановка: Process.on_terminate (POSIX — SIGINT/SIGTERM, Windows — Ctrl+C/закрытие консоли) либо программный вызов shutdown (публичный, идемпотентный). После остановки сервер перестаёт принимать новые соединения и ждёт «в полёте»-запросы до drain_timeout.
Эксплуатационные заметки (production)
- systemd +
ProtectSystem=strict: процессу нужно явно разрешить запись —ReadWritePaths=<workdir>/logsиReadWritePaths=<workdir>/config. Оба пути обязаны существовать до старта, иначе systemd не соберёт mount namespace и сервис упадёт сstatus=226/NAMESPACE, не запустив бинарник.ExecStartPreпроблему НЕ решает (namespace собирается и для pre-команд) — папки создаются один раз при настройке сервера. - Деплой поверх работающего ELF:
scpна путь исполняемого сейчас бинарника падает сText file busy. Порядок: stop → scp → start, либо scp во временное имя +mv(mv заменяет запись в каталоге — можно поверх работающего). - Диагностика «старый бинарник»:
curl http://127.0.0.1:<port>/health— полеversion. - Ротация логов: библиотека держит файлы открытыми и не переоткрывает их — logrotate использовать с
copytruncate. (Переоткрытие по SIGUSR1/SIGHUP — возможное будущее развитие.) - Сообщения
created default config/cannot create default configуходят в STDERR до редиректа вывода — на сервере ищи их вjournalctl, не в файлах логов.
Разработка
crystal spec # тесты
crystal build --no-codegen examples/hello.cr # проверка примера
crystal run examples/hello.cr # smoke: /health, /greet, Ctrl+C
Лицензия
MIT
funcnode-app
- 0
- 0
- 0
- 0
- 0
- about 3 hours ago
- July 12, 2026
MIT License
Sun, 12 Jul 2026 10:23:39 GMT