Files
Bridge-and-Join-s/internal/lkgateway/checks.go
T
zuevav a48f965d1b Справка и события без внутреннего; ЛК — только при модуле; справочник DEPO сразу после подготовки
- справка «Внешние системы» — внутренняя (internaldocs); «API ЛК» и проверка callback ЛК — только при модуле ЛК
- отслеживание документации НРД (doc-watcher) — только в режиме разработки
- после подготовки DEPO справочник участников пишется входом по действию человека — не ждёт паузы после
  прежнего неудачного входа («FTV_M2M_HANDBOOK не создана»)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 00:30:50 +03:00

256 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
import (
"context"
"errors"
"net"
"net/http"
neturl "net/url"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
"os"
"time"
)
// Status — состояние одной проверяемой подсистемы.
type Status struct {
Name string `json:"name"`
OK bool `json:"ok"`
Message string `json:"message,omitempty"`
Detail string `json:"detail,omitempty"`
// Optional — компонент не обязателен для работы с НРД. Его «не-OK»
// не делает систему «не готовой» (напр. callback в ЛК).
Optional bool `json:"optional,omitempty"`
}
// SystemStatus — все проверки.
type SystemStatus struct {
Profile string `json:"profile"`
Provider string `json:"crypto_provider"`
Checks []Status `json:"checks"`
CheckedAt time.Time `json:"checked_at"`
}
// CheckOptions — что и как проверять.
type CheckOptions struct {
PostgresDSN string // если пусто — режим in-memory, проверки нет
CryptoSocket string // путь до UDS crypto-service
NSDAdapterURL string // например http://127.0.0.1:8082
LKCallbackURL string // куда шлём callback (lk-emulator)
Profile string // имя профиля nsdadapter (guest-gost...)
CryptoProvider string // BJ_CRYPTO_PROVIDER (stub|cryptopro|...)
Timeout time.Duration // таймаут на одну проверку
}
// CheckAll выполняет все доступные проверки и возвращает SystemStatus.
func CheckAll(ctx context.Context, o CheckOptions) SystemStatus {
if o.Timeout == 0 {
o.Timeout = 2 * time.Second
}
out := SystemStatus{
Profile: o.Profile,
Provider: o.CryptoProvider,
CheckedAt: time.Now().UTC(),
}
out.Checks = append(out.Checks, checkPostgres(ctx, o))
out.Checks = append(out.Checks, checkCryptoSocket(o))
out.Checks = append(out.Checks, checkNSDAdapter(ctx, o))
out.Checks = append(out.Checks, ishVersionStatus(ctx))
if globalRC == nil || moduleAllowed(globalRC, "web-cabinet") || devMode(globalRC) { // без модуля ЛК нечего проверять
out.Checks = append(out.Checks, checkLKCallback(ctx, o))
}
return out
}
func checkPostgres(ctx context.Context, o CheckOptions) Status {
s := Status{Name: "База данных PostgreSQL"}
if o.PostgresDSN == "" {
s.OK = true
s.Optional = true
s.Message = "in-memory — данные не сохраняются между перезапусками"
return s
}
pctx, cancel := context.WithTimeout(ctx, o.Timeout)
defer cancel()
pool, err := pgxpool.New(pctx, o.PostgresDSN)
if err != nil {
s.OK = false
s.Message = "ошибка подключения: " + err.Error()
return s
}
defer pool.Close()
if err := pool.Ping(pctx); err != nil {
s.OK = false
s.Message = "не отвечает: " + err.Error()
return s
}
s.OK = true
s.Message = "подключена, репозиторий m2m_core.deals"
return s
}
func checkCryptoSocket(o CheckOptions) Status {
s := Status{Name: "crypto-service (UDS)"}
if o.CryptoSocket == "" {
s.OK = false
s.Message = "BJ_CRYPTO_SOCKET не задан"
return s
}
info, err := os.Stat(o.CryptoSocket)
if err != nil {
s.OK = false
s.Message = "сокет недоступен"
s.Detail = err.Error()
return s
}
if info.Mode()&os.ModeSocket == 0 {
s.OK = false
s.Message = "путь существует, но это не сокет"
s.Detail = o.CryptoSocket
return s
}
// Пробуем подключиться.
d := net.Dialer{Timeout: o.Timeout}
conn, err := d.Dial("unix", o.CryptoSocket)
if err != nil {
s.OK = false
s.Message = "сокет существует, но не отвечает"
s.Detail = err.Error()
return s
}
_ = conn.Close()
s.OK = true
s.Message = "сокет открыт"
s.Detail = o.CryptoSocket
if o.CryptoProvider == "stub" || o.CryptoProvider == "" {
if devMode(globalRC) {
s.Message += ", провайдер stub (реальная криптография не подключена)"
} else {
s.Message += ", средство подписи не настроено"
}
} else {
s.Message += ", провайдер " + o.CryptoProvider
}
return s
}
func checkNSDAdapter(ctx context.Context, o CheckOptions) Status {
s := Status{Name: "Интеграционный шлюз НРД"}
if o.NSDAdapterURL == "" {
s.OK = true
s.Optional = true
s.Message = "не настроен — обмен с НРД не выполняется"
if devMode(globalRC) {
s.Message = "не подключён — режим эмуляции (mock)"
}
return s
}
// У ИШ нет /healthz — проверяем рабочий эндпоинт Web API (engine/state
// отвечает 200 «Running», когда движок поднят).
st := httpHealth(ctx, o.NSDAdapterURL+"/api/admin/engine/state", o.Timeout, s)
if st.OK {
st.Message = "подключён, движок работает"
}
return st
}
func checkLKCallback(ctx context.Context, o CheckOptions) Status {
s := Status{Name: "Callback в личный кабинет", Optional: true}
if o.LKCallbackURL == "" {
s.OK = false
s.Message = "не настроен — уведомления в ЛК отключены (необязательно для работы с НРД)"
return s
}
return httpHealth(ctx, o.LKCallbackURL+"/healthz", o.Timeout, s)
}
func httpHealth(ctx context.Context, url string, timeout time.Duration, s Status) Status {
// Подключаемся по реальному url (для локальных сервисов — loopback), но в
// Detail показываем адрес/имя сервера, а не «localhost» — так оператору
// понятно, на каком узле работает компонент. Для loopback добавляем пометку,
// что порт не открыт наружу (это подпись, а не кликабельная ссылка).
disp := displayURL(url)
if isLoopbackURL(url) {
disp += " (только локально — доступ с сервера или через SSH-туннель, порт не открыт наружу)"
}
c := &http.Client{Timeout: timeout}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
s.OK = false
s.Message = "не получилось собрать запрос"
s.Detail = err.Error()
return s
}
resp, err := c.Do(req)
if err != nil {
s.OK = false
s.Message = "недоступен"
s.Detail = err.Error()
return s
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
s.OK = false
s.Message = "HTTP " + http.StatusText(resp.StatusCode)
s.Detail = disp
return s
}
s.OK = true
s.Message = "OK"
s.Detail = disp
return s
}
// displayHost возвращает отображаемое имя/адрес этого сервера: значение
// BJ_PUBLIC_HOST (если задано, напр. DNS-имя) либо системный hostname.
func displayHost() string {
if h := strings.TrimSpace(os.Getenv("BJ_PUBLIC_HOST")); h != "" {
return h
}
if h, err := os.Hostname(); err == nil && h != "" {
return h
}
return "server"
}
// isLoopbackURL сообщает, указывает ли url на loopback-хост (сервис слушает
// только на этом сервере и недоступен извне).
func isLoopbackURL(raw string) bool {
u, err := neturl.Parse(raw)
if err != nil {
return false
}
switch u.Hostname() {
case "localhost", "127.0.0.1", "0.0.0.0", "::1":
return true
}
return false
}
// displayURL заменяет loopback-хост (localhost/127.0.0.1/0.0.0.0/[::1]) в url на
// displayHost(). Внешние адреса не трогает. Порт и путь сохраняются.
func displayURL(raw string) string {
u, err := neturl.Parse(raw)
if err != nil || u.Host == "" {
return raw
}
host := u.Hostname()
switch host {
case "localhost", "127.0.0.1", "0.0.0.0", "::1":
newHost := displayHost()
if p := u.Port(); p != "" {
u.Host = net.JoinHostPort(newHost, p)
} else {
u.Host = newHost
}
return u.String()
}
return raw
}
// ErrUnknown — общий placeholder.
var ErrUnknown = errors.New("lkgateway: unknown error")