package check import ( "context" "fmt" "os/exec" "regexp" "strconv" "strings" "time" "github.com/jackc/pgx/v5/pgxpool" "shop/internal/version" ) type Status string const ( StatusOK Status = "ok" StatusWarn Status = "warn" StatusError Status = "error" ) type Item struct { Name string `json:"name"` Status Status `json:"status"` Detail string `json:"detail"` Expected string `json:"expected,omitempty"` } type Report struct { AppVersion string `json:"app_version"` GoVersion string `json:"go_version"` Items []Item `json:"checks"` } func (r Report) Healthy() bool { for _, it := range r.Items { if it.Status == StatusError { return false } } return true } func AppInfo() Report { return Report{ AppVersion: version.AppVersion, GoVersion: version.GoRuntime(), Items: []Item{ { Name: "go_runtime", Status: goRuntimeStatus(), Detail: version.GoRuntime(), Expected: ">=" + version.MinGoVersion, }, }, } } func goRuntimeStatus() Status { v := strings.TrimPrefix(version.GoRuntime(), "go") major, minor, ok := parseGoVersion(v) if !ok { return StatusWarn } expMajor, expMinor, _ := parseGoVersion(version.MinGoVersion) if major > expMajor || (major == expMajor && minor >= expMinor) { return StatusOK } return StatusWarn } func parseGoVersion(v string) (major, minor int, ok bool) { parts := strings.Split(v, ".") if len(parts) < 2 { return 0, 0, false } major, err1 := strconv.Atoi(parts[0]) minor, err2 := strconv.Atoi(parts[1]) return major, minor, err1 == nil && err2 == nil } func WithDatabase(ctx context.Context, pool *pgxpool.Pool) (Report, error) { r := AppInfo() dbItems, err := Database(ctx, pool) if err != nil { return r, err } r.Items = append(r.Items, dbItems...) return r, nil } func Database(ctx context.Context, pool *pgxpool.Pool) ([]Item, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() var pgVersion string if err := pool.QueryRow(ctx, `SHOW server_version`).Scan(&pgVersion); err != nil { return []Item{{ Name: "database", Status: StatusError, Detail: err.Error(), }}, nil } major, ok := postgresMajor(pgVersion) pgStatus := StatusOK detail := pgVersion expected := fmt.Sprintf("%d.x", version.ExpectedPostgresMajor) if !ok { pgStatus = StatusWarn detail = pgVersion + " (не удалось определить major)" } else if major != version.ExpectedPostgresMajor { pgStatus = StatusWarn detail = fmt.Sprintf("%s (ожидается PostgreSQL %d)", pgVersion, version.ExpectedPostgresMajor) } return []Item{ {Name: "database", Status: StatusOK, Detail: "подключено"}, {Name: "postgresql", Status: pgStatus, Detail: detail, Expected: expected}, }, nil } var pgMajorRe = regexp.MustCompile(`^(\d+)`) func postgresMajor(v string) (int, bool) { m := pgMajorRe.FindStringSubmatch(strings.TrimSpace(v)) if len(m) < 2 { return 0, false } n, err := strconv.Atoi(m[1]) return n, err == nil } func ToolVersions(ctx context.Context) []Item { var items []Item if out, err := run(ctx, "docker", "version", "--format", "{{.Server.Version}}"); err == nil { items = append(items, Item{Name: "docker", Status: StatusOK, Detail: strings.TrimSpace(out)}) } else { items = append(items, Item{Name: "docker", Status: StatusWarn, Detail: "не найден"}) } if out, err := run(ctx, "docker", "compose", "version", "--short"); err == nil { items = append(items, Item{Name: "docker_compose", Status: StatusOK, Detail: strings.TrimSpace(out)}) } else { items = append(items, Item{Name: "docker_compose", Status: StatusWarn, Detail: "не найден"}) } return items } func run(ctx context.Context, name string, args ...string) (string, error) { ctx, cancel := context.WithTimeout(ctx, 8*time.Second) defer cancel() cmd := exec.CommandContext(ctx, name, args...) out, err := cmd.Output() return string(out), err } func Merge(reports ...Report) Report { if len(reports) == 0 { return AppInfo() } out := reports[0] for _, r := range reports[1:] { if out.AppVersion == "" { out.AppVersion = r.AppVersion } if out.GoVersion == "" { out.GoVersion = r.GoVersion } out.Items = append(out.Items, r.Items...) } return out }