All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s
180 lines
8.3 KiB
Go
180 lines
8.3 KiB
Go
package integration
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
retentionquery "github.com/break/junhong_cmp_fiber/internal/query/retention"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
)
|
||
|
||
// OverviewFilter 定义外部交互总览的受控筛选和时间粒度。
|
||
type OverviewFilter struct {
|
||
ListFilter
|
||
Bucket string
|
||
}
|
||
|
||
// Overview 是外部交互固定维度聚合结果。
|
||
type Overview struct {
|
||
Total int64 `json:"total" description:"外部交互总数"`
|
||
AnomalyCount int64 `json:"anomaly_count" description:"failed或indeterminate类别的异常交互数"`
|
||
UnknownCount int64 `json:"unknown_count" description:"原始结果为unknown的交互数"`
|
||
StalePendingCount int64 `json:"stale_pending_count" description:"超过既定阈值仍为pending的交互数"`
|
||
StateChangedCount int64 `json:"state_changed_count" description:"改变本地业务状态的交互数"`
|
||
AverageDurationMS float64 `json:"average_duration_ms" description:"平均交互耗时,单位毫秒"`
|
||
P95DurationMS float64 `json:"p95_duration_ms" description:"P95交互耗时,单位毫秒"`
|
||
Results []ResultCount `json:"results" description:"原始结果分布,code/name/category分别为稳定编码、中文名和派生类别"`
|
||
Providers []NamedCount `json:"providers" description:"提供方分布,code为provider枚举,name为中文展示名"`
|
||
Directions []NamedCount `json:"directions" description:"方向分布,code为inbound或outbound,name为中文展示名"`
|
||
Trend []TrendPoint `json:"trend" description:"五类派生结果的时间趋势"`
|
||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||
}
|
||
|
||
// ResultCount 是原始结果及其派生类别计数。
|
||
type ResultCount struct {
|
||
Code string `json:"code" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"外部交互原始结果稳定编码"`
|
||
Name string `json:"name" description:"code对应的中文展示名称"`
|
||
Category string `json:"category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"由code派生的固定结果类别"`
|
||
Count int64 `json:"count" description:"该原始结果的交互数量"`
|
||
}
|
||
|
||
// NamedCount 是稳定编码、中文名称和数量。
|
||
type NamedCount struct {
|
||
Code string `json:"code" description:"当前聚合维度的稳定编码,枚举域由所属数组字段说明"`
|
||
Name string `json:"name" description:"code对应的中文展示名称"`
|
||
Count int64 `json:"count" description:"该编码的交互数量"`
|
||
}
|
||
|
||
// TrendPoint 是固定时间桶内的结果类别趋势。
|
||
type TrendPoint struct {
|
||
BucketAt time.Time `json:"bucket_at" description:"时间桶起点"`
|
||
Total int64 `json:"total" description:"桶内外部交互总数"`
|
||
Succeeded int64 `json:"succeeded" description:"桶内succeeded类别数量"`
|
||
Processing int64 `json:"processing" description:"桶内processing类别数量"`
|
||
Indeterminate int64 `json:"indeterminate" description:"桶内indeterminate类别数量"`
|
||
Failed int64 `json:"failed" description:"桶内failed类别数量"`
|
||
NotSent int64 `json:"not_sent" description:"桶内not_sent类别数量"`
|
||
}
|
||
|
||
// Overview 查询指定时间范围的固定维度外部交互总览。
|
||
func (q *Query) Overview(ctx context.Context, filter OverviewFilter) (*Overview, error) {
|
||
if err := q.authorize(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if filter.Bucket == "" {
|
||
filter.Bucket = "hour"
|
||
}
|
||
var retention retentionquery.Info
|
||
var err error
|
||
filter.ListFilter, retention, err = q.normalizeOnlineFilter(ctx, filter.ListFilter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !validFilter(filter.ListFilter) || (filter.Bucket != "hour" && filter.Bucket != "day") {
|
||
return nil, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
base := applyFilters(q.db.WithContext(ctx).Model(&model.IntegrationLog{}), filter.ListFilter)
|
||
result := &Overview{Results: []ResultCount{}, Providers: []NamedCount{}, Directions: []NamedCount{}, Trend: []TrendPoint{}, Retention: retention}
|
||
if err := loadOverviewMetrics(base, result); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := loadResultCounts(base, result); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := loadNamedCounts(base, "provider", result); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := loadNamedCounts(base, "direction", result); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := loadTrend(base, filter.Bucket, result); err != nil {
|
||
return nil, err
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func loadOverviewMetrics(query *gorm.DB, result *Overview) error {
|
||
failed := categoryResults(constants.IntegrationResultCategoryFailed)
|
||
var row struct {
|
||
Total, AnomalyCount, UnknownCount, StalePendingCount, StateChangedCount int64
|
||
AverageDurationMS, P95DurationMS float64
|
||
}
|
||
err := query.Select(`COUNT(*) AS total,
|
||
COUNT(*) FILTER (WHERE result IN ? OR result = ?) AS anomaly_count,
|
||
COUNT(*) FILTER (WHERE result = ?) AS unknown_count,
|
||
COUNT(*) FILTER (WHERE result = ? AND created_at < ?) AS stale_pending_count,
|
||
COUNT(*) FILTER (WHERE state_changed) AS state_changed_count,
|
||
COALESCE(AVG(duration_ms), 0)::float8 AS average_duration_ms,
|
||
COALESCE(percentile_cont(0.95) WITHIN GROUP (ORDER BY duration_ms), 0)::float8 AS p95_duration_ms`,
|
||
failed, constants.IntegrationResultUnknown, constants.IntegrationResultUnknown,
|
||
constants.IntegrationResultPending, time.Now().UTC().Add(-constants.IntegrationPendingStaleAfter)).Scan(&row).Error
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "聚合外部交互总览失败")
|
||
}
|
||
result.Total, result.AnomalyCount, result.UnknownCount = row.Total, row.AnomalyCount, row.UnknownCount
|
||
result.StalePendingCount, result.StateChangedCount = row.StalePendingCount, row.StateChangedCount
|
||
result.AverageDurationMS, result.P95DurationMS = row.AverageDurationMS, row.P95DurationMS
|
||
return nil
|
||
}
|
||
|
||
func loadResultCounts(query *gorm.DB, result *Overview) error {
|
||
var rows []struct {
|
||
Code string
|
||
Count int64
|
||
}
|
||
if err := query.Select("result AS code, COUNT(*) AS count").Group("result").Order("result ASC").Scan(&rows).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "聚合外部交互结果分布失败")
|
||
}
|
||
for _, row := range rows {
|
||
result.Results = append(result.Results, ResultCount{Code: row.Code, Name: constants.IntegrationResultName(row.Code), Category: constants.IntegrationResultCategory(row.Code), Count: row.Count})
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func loadNamedCounts(query *gorm.DB, column string, result *Overview) error {
|
||
var rows []struct {
|
||
Code string
|
||
Count int64
|
||
}
|
||
if err := query.Select(column + " AS code, COUNT(*) AS count").Group(column).Order(column + " ASC").Scan(&rows).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "聚合外部交互维度分布失败")
|
||
}
|
||
items := make([]NamedCount, 0, len(rows))
|
||
for _, row := range rows {
|
||
name := constants.IntegrationProviderName(row.Code)
|
||
if column == "direction" {
|
||
name = constants.IntegrationDirectionName(row.Code)
|
||
}
|
||
items = append(items, NamedCount{Code: row.Code, Name: name, Count: row.Count})
|
||
}
|
||
if column == "provider" {
|
||
result.Providers = items
|
||
} else {
|
||
result.Directions = items
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func loadTrend(query *gorm.DB, bucket string, result *Overview) error {
|
||
failed, notSent := categoryResults(constants.IntegrationResultCategoryFailed), categoryResults(constants.IntegrationResultCategoryNotSent)
|
||
return wrapTrendError(query.Select(`date_trunc(?, created_at) AS bucket_at, COUNT(*) AS total,
|
||
COUNT(*) FILTER (WHERE result = ?) AS succeeded,
|
||
COUNT(*) FILTER (WHERE result = ?) AS processing,
|
||
COUNT(*) FILTER (WHERE result = ?) AS indeterminate,
|
||
COUNT(*) FILTER (WHERE result IN ?) AS failed,
|
||
COUNT(*) FILTER (WHERE result IN ?) AS not_sent`, bucket, constants.IntegrationResultSuccess,
|
||
constants.IntegrationResultPending, constants.IntegrationResultUnknown, failed, notSent).
|
||
Group("bucket_at").Order("bucket_at ASC").Scan(&result.Trend).Error)
|
||
}
|
||
|
||
func wrapTrendError(err error) error {
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "聚合外部交互趋势失败")
|
||
}
|