funcnode-app

funcnode-app

Переиспользуемый «скелет» долгоживущего Crystal-сервиса (shard, namespace Funcnode::App). Забирает на себя всё, что повторяется от сервиса к сервису:

  1. Конфиг — YAML-файл, загружается первым при старте; если файла нет — создаётся автоматически с дефолтами и комментариями. Существующий файл никогда не перезаписывается. Опечатки в ключах вызывают предупреждение; невалидный YAML — внятное сообщение и выход с кодом 1 (без backtrace).
  2. Логи — STDOUT/STDERR перенаправляются в файлы (append) на уровне файлового дескриптора: в файлы попадает всё, включая backtrace необработанных исключений.
  3. Жизненный цикл — HTTP-сервер, обработка сигналов остановки, graceful shutdown с ожиданием (drain) текущих запросов.
  4. Диагностика — встроенный 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-svcMY_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

Repository

funcnode-app

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • about 3 hours ago
  • July 12, 2026
License

MIT License

Links
Synced at

Sun, 12 Jul 2026 10:23:39 GMT

Languages