165 lines
6.8 KiB
Go
165 lines
6.8 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/adminweb"
|
||
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/store/postgres"
|
||
)
|
||
|
||
type adminComponent = adminweb.Component
|
||
type operationalMetrics = postgres.OperationalMetrics
|
||
|
||
type operationalMetricsSource interface {
|
||
Ping(context.Context) error
|
||
ReadOperationalMetrics(context.Context, string) (postgres.OperationalMetrics, error)
|
||
}
|
||
|
||
type operationalStatusProvider struct {
|
||
metrics operationalMetricsSource
|
||
tenant string
|
||
weknoraHealthURL string
|
||
httpClient *http.Client
|
||
now func() time.Time
|
||
version string
|
||
streamRunning bool
|
||
questionRunning bool
|
||
faqRunning bool
|
||
supportRunning bool
|
||
questionBound bool
|
||
faqBound bool
|
||
supportBound bool
|
||
ossConfigured bool
|
||
}
|
||
|
||
func (p *operationalStatusProvider) Components(ctx context.Context) ([]adminweb.Component, error) {
|
||
now := time.Now()
|
||
if p.now != nil {
|
||
now = p.now()
|
||
}
|
||
components := []adminweb.Component{{Name: "运行版本", State: "ok", Code: "version_ok", Detail: "版本:" + safeVersion(p.version), LastSuccess: now, CheckedAt: now}}
|
||
|
||
metrics := operationalMetrics{}
|
||
if p.metrics == nil || p.metrics.Ping(ctx) != nil {
|
||
components = append(components, adminweb.Component{Name: "PostgreSQL", State: "error", Code: "database_unavailable", Detail: "数据库健康检查失败", CheckedAt: now})
|
||
} else {
|
||
var err error
|
||
metrics, err = p.metrics.ReadOperationalMetrics(ctx, p.tenant)
|
||
if err != nil {
|
||
components = append(components, adminweb.Component{Name: "PostgreSQL", State: "error", Code: "metrics_query_failed", Detail: "数据库可连接,但状态聚合失败", LastSuccess: now, CheckedAt: now})
|
||
} else {
|
||
components = append(components, adminweb.Component{Name: "PostgreSQL", State: "ok", Code: "database_ok", Detail: "连接与状态聚合正常", LastSuccess: now, CheckedAt: now})
|
||
}
|
||
}
|
||
|
||
components = append(components,
|
||
booleanComponent("钉钉Stream", p.streamRunning, "stream_running", "stream_stopped", "正式Stream消费者运行中", "正式Stream消费者未运行", now),
|
||
p.weknoraComponent(ctx, now),
|
||
outboxComponent("问题表Outbox", metrics.QuestionPending, metrics.QuestionFailed, metrics.LastQuestionSuccess, now),
|
||
outboxComponent("FAQ Outbox", metrics.FAQPending, metrics.FAQFailed, metrics.LastFAQSuccess, now),
|
||
supportComponent(p, metrics, now),
|
||
dispatcherComponent("问题表Dispatcher", p.questionBound, p.questionRunning, now),
|
||
dispatcherComponent("FAQ Dispatcher", p.faqBound, p.faqRunning, now),
|
||
booleanComponent("Private OSS图片", p.ossConfigured, "oss_configured", "oss_unconfigured", "图片出站配置完整", "图片出站未配置", now),
|
||
)
|
||
return components, nil
|
||
}
|
||
|
||
func (p *operationalStatusProvider) weknoraComponent(ctx context.Context, now time.Time) adminweb.Component {
|
||
component := adminweb.Component{Name: "WeKnora", State: "error", Code: "health_probe_failed", Detail: "健康探针失败", CheckedAt: now}
|
||
if strings.TrimSpace(p.weknoraHealthURL) == "" {
|
||
component.Code = "health_url_missing"
|
||
component.Detail = "健康探针地址未配置"
|
||
return component
|
||
}
|
||
client := p.httpClient
|
||
if client == nil {
|
||
client = &http.Client{Timeout: 3 * time.Second}
|
||
}
|
||
reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||
defer cancel()
|
||
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, p.weknoraHealthURL, nil)
|
||
if err != nil {
|
||
return component
|
||
}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return component
|
||
}
|
||
defer resp.Body.Close()
|
||
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10))
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
component.Code = "health_http_error"
|
||
component.Detail = fmt.Sprintf("健康探针HTTP %d", resp.StatusCode)
|
||
return component
|
||
}
|
||
component.State, component.Code, component.Detail = "ok", "health_ok", "HTTP健康探针正常"
|
||
component.LastSuccess = now
|
||
return component
|
||
}
|
||
|
||
func outboxComponent(name string, pending, failed int, lastSuccess, now time.Time) adminweb.Component {
|
||
component := adminweb.Component{Name: name, State: "ok", Code: "outbox_empty", Detail: "pending=0,failed=0", LastSuccess: lastSuccess, CheckedAt: now}
|
||
if failed > 0 {
|
||
component.State, component.Code = "error", "outbox_failed"
|
||
} else if pending > 0 {
|
||
component.State, component.Code = "warning", "outbox_pending"
|
||
}
|
||
component.Detail = fmt.Sprintf("pending=%d,failed=%d", pending, failed)
|
||
return component
|
||
}
|
||
|
||
func supportComponent(p *operationalStatusProvider, metrics operationalMetrics, now time.Time) adminweb.Component {
|
||
component := adminweb.Component{Name: "技术支持通知", CheckedAt: now, LastSuccess: metrics.LastSupportSuccess}
|
||
switch {
|
||
case !p.supportBound:
|
||
component.State, component.Code, component.Detail = "disabled", "binding_missing", "技术支持群未绑定"
|
||
case !p.faqBound:
|
||
component.State, component.Code, component.Detail = "blocked", "faq_binding_required", "已绑定支持群;需先激活FAQ表"
|
||
case metrics.SupportFailed > 0:
|
||
component.State, component.Code = "error", "notification_failed"
|
||
component.Detail = fmt.Sprintf("pending=%d,failed=%d", metrics.SupportPending, metrics.SupportFailed)
|
||
case metrics.SupportPending > 0:
|
||
component.State, component.Code = "warning", "notification_pending"
|
||
component.Detail = fmt.Sprintf("pending=%d,failed=0", metrics.SupportPending)
|
||
case p.supportRunning:
|
||
component.State, component.Code, component.Detail = "ok", "dispatcher_running", "通知Dispatcher运行中;pending=0,failed=0"
|
||
default:
|
||
component.State, component.Code, component.Detail = "error", "dispatcher_stopped", "绑定与依赖完整,但Dispatcher未运行"
|
||
}
|
||
return component
|
||
}
|
||
|
||
func dispatcherComponent(name string, bound, running bool, now time.Time) adminweb.Component {
|
||
if !bound {
|
||
return adminweb.Component{Name: name, State: "disabled", Code: "binding_missing", Detail: "无active绑定", CheckedAt: now}
|
||
}
|
||
if !running {
|
||
return adminweb.Component{Name: name, State: "error", Code: "dispatcher_stopped", Detail: "已有active绑定,但Dispatcher未运行", CheckedAt: now}
|
||
}
|
||
return adminweb.Component{Name: name, State: "ok", Code: "dispatcher_running", Detail: "Dispatcher运行中", LastSuccess: now, CheckedAt: now}
|
||
}
|
||
|
||
func booleanComponent(name string, ok bool, successCode, failureCode, successDetail, failureDetail string, now time.Time) adminweb.Component {
|
||
if ok {
|
||
return adminweb.Component{Name: name, State: "ok", Code: successCode, Detail: successDetail, LastSuccess: now, CheckedAt: now}
|
||
}
|
||
return adminweb.Component{Name: name, State: "disabled", Code: failureCode, Detail: failureDetail, CheckedAt: now}
|
||
}
|
||
|
||
func safeVersion(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return "unknown"
|
||
}
|
||
if len(value) > 80 {
|
||
return value[:80]
|
||
}
|
||
return value
|
||
}
|