4cbefa41d5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
173 lines
6.4 KiB
Go
173 lines
6.4 KiB
Go
package lkgateway
|
|
|
|
// Сторож связи ИШ ↔ НРД. ИШ может отвечать bj, но сам не достукиваться до НРД (контур НРД отдаёт
|
|
// 404/500/502): тогда пакеты получают ERROR, входящие не приходят. В REST ИШ такого признака нет,
|
|
// поэтому смотрим журнал ИШ (если он на этом сервере): итог последних опросов НРД
|
|
// «GetPackageListFull (GET) -> OK | #код». НРД не отвечает nsdAlertAfter — оповещение; снова
|
|
// отвечает — оповещение с простоем и числом недоставленных переводов (их нужно отправить повторно).
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
nsdAlertAfter = 10 * time.Minute
|
|
nsdRemindEvery = 2 * time.Hour
|
|
nsdLogTail = 512 << 10
|
|
)
|
|
|
|
var igateLogFile = "/var/lib/igate/.local/share/NSD Integration Gateway/logs-svc/log.txt"
|
|
|
|
// PID_363248 2026-09-25 15:42:59.1873 T16 DEBUG [WSL] WSL TEST3 GOST: GetPackageListFull (GET) -> #404 (HTTP Error 404: NotFound)
|
|
var nsdPollRe = regexp.MustCompile(`^PID_\d+ (\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ .*WSL ([^:]+): GetPackageListFull \(GET\) -> (OK|#\d+[^\r\n]*)`)
|
|
|
|
type nsdPoll struct {
|
|
At time.Time
|
|
Contour string
|
|
OK bool
|
|
Result string
|
|
}
|
|
|
|
// nsdLastPolls — итоги опросов НРД из хвоста журнала ИШ: последний опрос и первый неудачный после
|
|
// последнего успешного (начало текущего сбоя; nil — сбоя нет).
|
|
func nsdLastPolls(tail []byte) (last, firstFail *nsdPoll) {
|
|
for _, ln := range strings.Split(string(tail), "\n") {
|
|
m := nsdPollRe.FindStringSubmatch(strings.TrimRight(ln, "\r"))
|
|
if m == nil {
|
|
continue
|
|
}
|
|
at, err := time.ParseInLocation("2006-01-02 15:04:05", m[1], time.Local)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
p := &nsdPoll{At: at, Contour: m[2], OK: m[3] == "OK", Result: m[3]}
|
|
last = p
|
|
if p.OK {
|
|
firstFail = nil
|
|
} else if firstFail == nil {
|
|
firstFail = p
|
|
}
|
|
}
|
|
return last, firstFail
|
|
}
|
|
|
|
func readTail(path string, n int64) ([]byte, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
st, err := f.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
off := st.Size() - n
|
|
if off < 0 {
|
|
off = 0
|
|
}
|
|
if _, err := f.Seek(off, io.SeekStart); err != nil {
|
|
return nil, err
|
|
}
|
|
return io.ReadAll(f)
|
|
}
|
|
|
|
var nsdWatch struct {
|
|
sync.Mutex
|
|
DownSince time.Time // первый неудачный опрос после последнего успешного
|
|
LastErr string
|
|
Contour string
|
|
Alerted bool
|
|
LastRemind time.Time
|
|
LastPollAt time.Time // последний опрос НРД шлюзом (по журналу)
|
|
LastPollOK time.Time // последний успешный
|
|
}
|
|
|
|
// nsdLinkStatus — для страницы статуса: не отвечает ли НРД шлюзу.
|
|
func nsdLinkStatus() (down bool, detail string) {
|
|
nsdWatch.Lock()
|
|
defer nsdWatch.Unlock()
|
|
if nsdWatch.DownSince.IsZero() {
|
|
return false, ""
|
|
}
|
|
return true, fmt.Sprintf("НРД (%s) не отвечает шлюзу с %s: %s", nsdWatch.Contour, nsdWatch.DownSince.Format("02.01 15:04"), nsdWatch.LastErr)
|
|
}
|
|
|
|
// nsdLinkTick — вызывается сторожем ИШ, когда сам ИШ отвечает и он на этом сервере.
|
|
func nsdLinkTick(rc *RuntimeConfig, now time.Time) {
|
|
tail, err := readTail(igateLogFile, nsdLogTail)
|
|
if err != nil {
|
|
return
|
|
}
|
|
last, firstFail := nsdLastPolls(tail)
|
|
if last == nil {
|
|
return
|
|
}
|
|
w := &nsdWatch
|
|
w.Lock()
|
|
w.LastPollAt = last.At
|
|
if last.OK {
|
|
w.LastPollOK = last.At
|
|
} else if firstFail != nil && w.LastPollOK.IsZero() {
|
|
// в хвосте журнала нет успешного опроса после сбоя — ищем последний успешный до него
|
|
for _, ln := range strings.Split(string(tail), "\n") {
|
|
if m := nsdPollRe.FindStringSubmatch(strings.TrimRight(ln, "\r")); m != nil && m[3] == "OK" {
|
|
if at, err := time.ParseInLocation("2006-01-02 15:04:05", m[1], time.Local); err == nil {
|
|
w.LastPollOK = at
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if last.OK {
|
|
since, alerted := w.DownSince, w.Alerted
|
|
w.DownSince, w.LastErr, w.Alerted = time.Time{}, "", false
|
|
w.Unlock()
|
|
if !since.IsZero() {
|
|
log.Printf("nsd-watch: НРД (%s) снова отвечает шлюзу, простой %s", last.Contour, humanDur(last.At.Sub(since)))
|
|
}
|
|
if alerted {
|
|
body := fmt.Sprintf("Контур НРД %s снова отвечает интеграционному шлюзу (простой %s, с %s).", last.Contour, humanDur(last.At.Sub(since)), since.Format("02.01 15:04"))
|
|
if n := sendFailCount(); n > 0 {
|
|
body += fmt.Sprintf(" Недоставленных переводов: %d — отправьте их повторно из карточки заявки («Отправить повторно»).", n)
|
|
}
|
|
raiseSystemAlert(rc, "НРД снова на связи", body)
|
|
}
|
|
return
|
|
}
|
|
if w.DownSince.IsZero() {
|
|
w.DownSince = firstFail.At
|
|
log.Printf("nsd-watch: НРД (%s) не отвечает шлюзу: %s", last.Contour, last.Result)
|
|
}
|
|
w.LastErr, w.Contour = last.Result, last.Contour
|
|
down := now.Sub(w.DownSince)
|
|
alert := !w.Alerted && down >= nsdAlertAfter
|
|
remind := w.Alerted && now.Sub(w.LastRemind) >= nsdRemindEvery
|
|
if alert || remind {
|
|
w.Alerted, w.LastRemind = true, now
|
|
}
|
|
since, lastErr := w.DownSince, w.LastErr
|
|
w.Unlock()
|
|
if !alert && !remind {
|
|
return
|
|
}
|
|
title := "НРД не отвечает шлюзу"
|
|
if remind {
|
|
title = "НРД всё ещё не отвечает шлюзу"
|
|
}
|
|
raiseSystemAlert(rc, title, fmt.Sprintf("Интеграционный шлюз работает, но контур НРД %s не отвечает %s (с %s): %s. Пока так, отправки в НРД не доходят (заявка остаётся «Не отправлена»), входящие не поступают. Это сторона НРД — при затяжном сбое обратитесь в поддержку НРД. Когда связь восстановится, bj сообщит.",
|
|
last.Contour, humanDur(down), since.Format("02.01 15:04"), lastErr))
|
|
}
|
|
|
|
func sendFailCount() int {
|
|
sendFails.Lock()
|
|
defer sendFails.Unlock()
|
|
sendFailLoad()
|
|
return len(sendFails.m)
|
|
}
|