Files
dobotai-platform/cmd/dobotai/operational_status.go
T
2026-08-13 18:33:52 +08:00

165 lines
6.8 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 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
}