package lkgateway import ( "context" "errors" "net" "net/http" "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, 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 == "" { s.Message += ", провайдер stub (реальная криптография не подключена)" } 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 = "не подключён — режим эмуляции (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 { 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 = url return s } s.OK = true s.Message = "OK" s.Detail = url return s } // ErrUnknown — общий placeholder. var ErrUnknown = errors.New("lkgateway: unknown error")