Files

357 lines
11 KiB
Go
Raw Permalink 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 monitor
import (
"context"
"github.com/gogf/gf/v2/frame/g"
v1 "github.com/qianfree/team-api/api/admin/v1"
lcommon "github.com/qianfree/team-api/internal/logic/common"
"github.com/qianfree/team-api/internal/middleware"
do "github.com/qianfree/team-api/internal/model/do"
"github.com/qianfree/team-api/internal/service"
)
type sMonitor struct{}
func NewMonitor() *sMonitor {
return &sMonitor{}
}
func init() {
service.RegisterMonitor(NewMonitor())
}
// ===================== Monitoring Dashboard =====================
func (s *sMonitor) Dashboard(ctx context.Context, req *v1.MonitorDashboardReq) (*v1.MonitorDashboardRes, error) {
// 仪表盘含跨租户 API 用量聚合,限定平台域访问(P2-13)
if err := requireAdminScope(ctx); err != nil {
return nil, err
}
data, err := GetDashboardData(ctx, req.Minutes)
if err != nil {
return nil, err
}
return &v1.MonitorDashboardRes{Data: data}, nil
}
func (s *sMonitor) Traffic(ctx context.Context, req *v1.MonitorTrafficReq) (*v1.MonitorTrafficRes, error) {
// 流量曲线为跨租户聚合,限定平台域访问(P2-13)
if err := requireAdminScope(ctx); err != nil {
return nil, err
}
data, err := GetTrafficCurve(ctx, req.Minutes)
if err != nil {
return nil, err
}
return &v1.MonitorTrafficRes{Data: data}, nil
}
func (s *sMonitor) TrafficFlow(ctx context.Context, req *v1.MonitorTrafficFlowReq) (*v1.MonitorTrafficFlowRes, error) {
// 流量流向桑基图为跨租户聚合,限定平台域访问(P2-13)
if err := requireAdminScope(ctx); err != nil {
return nil, err
}
startDate, endDate := normalizeMonitorDateRange(req.StartDate, req.EndDate)
metric := req.Metric
if metric != "cost" && metric != "tokens" && metric != "requests" {
metric = "cost"
}
data, err := GetTrafficFlow(ctx, startDate, endDate, metric)
if err != nil {
return nil, err
}
return &v1.MonitorTrafficFlowRes{Data: data}, nil
}
func (s *sMonitor) ModelPerformance(ctx context.Context, req *v1.MonitorModelPerformanceReq) (*v1.MonitorModelPerformanceRes, error) {
// 模型性能为跨租户聚合,限定平台域访问(P2-13)
if err := requireAdminScope(ctx); err != nil {
return nil, err
}
startDate, endDate := normalizeMonitorDateRange(req.StartDate, req.EndDate)
// 渠道/模型为可选过滤(channel_id=0、model_name 空 = 不过滤),透传给聚合层拼 SQL 条件
list, err := GetModelPerformance(ctx, startDate, endDate, req.ChannelId, req.ModelName)
if err != nil {
return nil, err
}
return &v1.MonitorModelPerformanceRes{List: list}, nil
}
func (s *sMonitor) ModelChannels(ctx context.Context, req *v1.MonitorModelChannelsReq) (*v1.MonitorModelChannelsRes, error) {
// 模型渠道性能为跨租户聚合,限定平台域访问(P2-13)
if err := requireAdminScope(ctx); err != nil {
return nil, err
}
startDate, endDate := normalizeMonitorDateRange(req.StartDate, req.EndDate)
list, err := GetModelChannels(ctx, startDate, endDate, req.ModelName)
if err != nil {
return nil, err
}
return &v1.MonitorModelChannelsRes{List: list}, nil
}
func (s *sMonitor) Latency(ctx context.Context, req *v1.MonitorLatencyReq) (*v1.MonitorLatencyRes, error) {
// 延迟直方图为跨租户聚合,限定平台域访问(P2-13)
if err := requireAdminScope(ctx); err != nil {
return nil, err
}
data, err := GetLatencyHistogram(ctx, req.Minutes)
if err != nil {
return nil, err
}
return &v1.MonitorLatencyRes{Data: data}, nil
}
// Dispatch 渠道调度引擎指标:最新累计快照 + 窗口增量(供监控页「调度引擎」面板)。
func (s *sMonitor) Dispatch(ctx context.Context, req *v1.MonitorDispatchReq) (*v1.MonitorDispatchRes, error) {
latest, window, collectedAt, err := GetDispatchMetricsRange(ctx, req.Minutes)
if err != nil {
return nil, err
}
res := &v1.MonitorDispatchRes{WindowMinutes: req.Minutes}
if latest == nil {
return res, nil
}
res.Available = true
res.CollectedAt = collectedAt
res.Latest = dispatchSnapshotToAPI(latest)
if window != nil {
res.Window = dispatchSnapshotToAPI(window)
}
return res, nil
}
// dispatchSnapshotToAPI 内部快照 → API 数据结构。
func dispatchSnapshotToAPI(s *DispatchMetricsSnapshot) *v1.DispatchMetricsData {
return &v1.DispatchMetricsData{
Selections: s.Selections,
Retries: s.Retries,
OverflowByTier: s.OverflowByTier,
SessionSources: s.SessionSources,
BreakerOpens: s.BreakerOpens,
NoCandidate: s.NoCandidate,
}
}
func (s *sMonitor) System(ctx context.Context, req *v1.MonitorSystemReq) (*v1.MonitorSystemRes, error) {
result := make(g.Map)
history, _ := GetSystemMetricsHistory(ctx, req.Minutes)
if len(history) > 0 {
result["history"] = history
result["current"] = history[len(history)-1]
}
if req.Minutes > 60 {
dbHistory, err := GetSystemMetricsFromDB(ctx, req.Minutes)
if err == nil {
result["db_history"] = dbHistory
}
}
return &v1.MonitorSystemRes{Data: result}, nil
}
func (s *sMonitor) DBPool(ctx context.Context, _ *v1.MonitorDBPoolReq) (*v1.MonitorDBPoolRes, error) {
data, err := GetDBPoolMetrics(ctx)
if err != nil {
return nil, err
}
return &v1.MonitorDBPoolRes{
ActiveConnections: data.ActiveConnections,
IdleConnections: data.IdleConnections,
TotalConnections: data.TotalConnections,
MaxConnections: data.MaxConnections,
WaitingQueries: data.WaitingQueries,
}, nil
}
func (s *sMonitor) RedisPool(ctx context.Context, _ *v1.MonitorRedisPoolReq) (*v1.MonitorRedisPoolRes, error) {
data, err := GetRedisPoolMetrics(ctx)
if err != nil {
return nil, err
}
return &v1.MonitorRedisPoolRes{
ConnectedClients: data.ConnectedClients,
UsedMemoryMB: data.UsedMemoryMB,
MaxMemoryMB: data.MaxMemoryMB,
UsedMemoryPercent: data.UsedMemoryPercent,
TotalCommands: data.TotalCommands,
InstantaneousOps: int64(data.InstantaneousOps),
KeyspaceHits: data.KeyspaceHits,
KeyspaceMisses: data.KeyspaceMisses,
HitRate: data.HitRate,
}, nil
}
func (s *sMonitor) Realtime(ctx context.Context, _ *v1.MonitorRealtimeReq) (*v1.MonitorRealtimeRes, error) {
data := GetRealtimeData()
// 实时 RPM/TPM(Redis 60 秒滑动窗口,全平台维度;Redis 不可用时为 0)
data.Rpm, data.Tpm = lcommon.GetRealtimeMetrics(ctx, 0)
return &v1.MonitorRealtimeRes{Data: data}, nil
}
// ===================== Alert Rules =====================
func (s *sMonitor) AlertRuleList(ctx context.Context, req *v1.AlertRuleListReq) (*v1.AlertRuleListRes, error) {
var enabled *bool
if req.Enabled == "true" {
val := true
enabled = &val
} else if req.Enabled == "false" {
val := false
enabled = &val
}
data, err := ListAlertRules(ctx, req.Page, req.PageSize, req.MetricType, req.Level, enabled)
if err != nil {
return nil, err
}
list, _ := data["list"].([]map[string]any)
total, _ := data["total"].(int)
return &v1.AlertRuleListRes{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
func (s *sMonitor) AlertOptions(ctx context.Context, _ *v1.AlertOptionsReq) (*v1.AlertOptionsRes, error) {
adminUsers, err := GetAdminUsers(ctx)
if err != nil {
adminUsers = []map[string]any{}
}
return &v1.AlertOptionsRes{Data: g.Map{
"metric_types": GetMetricTypeOptions(),
"conditions": GetConditionOptions(),
"levels": GetLevelOptions(),
"admin_users": adminUsers,
}}, nil
}
func (s *sMonitor) CreateAlertRule(ctx context.Context, req *v1.AlertRuleCreateReq) (*v1.AlertRuleCreateRes, error) {
id, err := CreateAlertRule(ctx, do.OpsAlertRules{
Name: req.Name,
MetricType: req.MetricType,
Condition: req.Condition,
Threshold: req.Threshold,
DurationSeconds: req.DurationSeconds,
NotificationMethods: req.NotificationMethods,
WebhookUrl: req.WebhookURL,
Level: req.Level,
CooldownSeconds: req.CooldownSeconds,
NotifyUserIds: req.NotifyUserIDs,
})
if err != nil {
return nil, err
}
return &v1.AlertRuleCreateRes{ID: id}, nil
}
func (s *sMonitor) UpdateAlertRule(ctx context.Context, req *v1.AlertRuleUpdateReq) (*v1.AlertRuleUpdateRes, error) {
updates := do.OpsAlertRules{}
if req.Name != "" {
updates.Name = req.Name
}
if req.MetricType != "" {
updates.MetricType = req.MetricType
}
if req.Condition != "" {
updates.Condition = req.Condition
}
if req.Threshold != nil {
updates.Threshold = *req.Threshold
}
if req.DurationSeconds != nil {
updates.DurationSeconds = *req.DurationSeconds
}
if len(req.NotificationMethods) > 0 {
updates.NotificationMethods = req.NotificationMethods
}
if req.WebhookURL != "" {
updates.WebhookUrl = req.WebhookURL
}
if req.Level != "" {
updates.Level = req.Level
}
if req.CooldownSeconds != nil {
updates.CooldownSeconds = *req.CooldownSeconds
}
if req.NotifyUserIDs != nil {
updates.NotifyUserIds = req.NotifyUserIDs
}
if err := UpdateAlertRule(ctx, req.Id, updates); err != nil {
return nil, err
}
return nil, nil
}
func (s *sMonitor) DeleteAlertRule(ctx context.Context, req *v1.AlertRuleDeleteReq) (*v1.AlertRuleDeleteRes, error) {
if err := DeleteAlertRule(ctx, req.Id); err != nil {
return nil, err
}
return nil, nil
}
func (s *sMonitor) ToggleAlertRule(ctx context.Context, req *v1.AlertRuleToggleReq) (*v1.AlertRuleToggleRes, error) {
if err := ToggleAlertRule(ctx, req.Id); err != nil {
return nil, err
}
return nil, nil
}
func (s *sMonitor) AlertTest(ctx context.Context, req *v1.AlertTestReq) (*v1.AlertTestRes, error) {
if err := SendTestAlert(ctx, req.Id); err != nil {
return nil, err
}
return &v1.AlertTestRes{Message: "测试通知已发送"}, nil
}
// ===================== Alert Events =====================
func (s *sMonitor) AlertEventList(ctx context.Context, req *v1.AlertEventListReq) (*v1.AlertEventListRes, error) {
data, err := ListAlertEvents(ctx, req.Page, req.PageSize, req.Status, req.Level, req.RuleID)
if err != nil {
return nil, err
}
list, _ := data["list"].([]map[string]any)
total, _ := data["total"].(int)
return &v1.AlertEventListRes{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
func (s *sMonitor) AcknowledgeAlert(ctx context.Context, req *v1.AlertEventAcknowledgeReq) (*v1.AlertEventAcknowledgeRes, error) {
adminID := middleware.GetUserID(ctx)
if err := AcknowledgeAlert(ctx, req.Id, adminID); err != nil {
return nil, err
}
return nil, nil
}
func (s *sMonitor) ResolveAlert(ctx context.Context, req *v1.AlertEventResolveReq) (*v1.AlertEventResolveRes, error) {
adminID := middleware.GetUserID(ctx)
if err := ResolveAlert(ctx, req.Id, adminID, req.Notes); err != nil {
return nil, err
}
return nil, nil
}
func (s *sMonitor) AlertEventClear(ctx context.Context, _ *v1.AlertEventClearReq) (*v1.AlertEventClearRes, error) {
deleted, err := ClearAlertEvents(ctx)
if err != nil {
return nil, err
}
return &v1.AlertEventClearRes{Deleted: deleted}, nil
}