This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
@@ -35,10 +36,11 @@ type ListFilter struct {
|
||||
|
||||
// ListPage 是按创建时间和主键稳定倒序的分页结果。
|
||||
type ListPage struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []ListItem `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []ListItem `json:"items"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
}
|
||||
|
||||
// ListItem 是 Integration Log 列表投影。
|
||||
@@ -81,6 +83,12 @@ type Detail struct {
|
||||
Fidelity FidelityView `json:"fidelity"`
|
||||
}
|
||||
|
||||
// DetailResponse 是外部交互详情及在线留存边界。
|
||||
type DetailResponse struct {
|
||||
Detail
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
}
|
||||
|
||||
// AttemptView 是显式 trigger_series 下的单次技术尝试。
|
||||
type AttemptView struct {
|
||||
IntegrationID string `json:"integration_id"`
|
||||
@@ -169,6 +177,10 @@ func (q *Query) List(ctx context.Context, filter ListFilter) (*ListPage, error)
|
||||
if err := q.authorize(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter, retention, err := q.normalizeOnlineFilter(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !validFilter(filter) {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
@@ -178,27 +190,52 @@ func (q *Query) List(ctx context.Context, filter ListFilter) (*ListPage, error)
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计外部交互日志失败")
|
||||
}
|
||||
rows := make([]model.IntegrationLog, 0, filter.PageSize)
|
||||
if err := query.Order("created_at DESC, id DESC").Offset((filter.Page - 1) * filter.PageSize).Limit(filter.PageSize).Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互日志失败")
|
||||
rows, err := q.loadListPage(ctx, query, filter.Page, filter.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]ListItem, len(rows))
|
||||
for i, row := range rows {
|
||||
items[i] = projectListItem(row)
|
||||
}
|
||||
return &ListPage{Total: total, Page: filter.Page, PageSize: filter.PageSize, Items: items}, nil
|
||||
return &ListPage{Total: total, Page: filter.Page, PageSize: filter.PageSize, Items: items, Retention: retention}, nil
|
||||
}
|
||||
|
||||
// loadListPage 先分页主键,再批量读取列表字段,避免加载正文摘要 JSON。
|
||||
func (q *Query) loadListPage(ctx context.Context, query *gorm.DB, page, pageSize int) ([]model.IntegrationLog, error) {
|
||||
ids := make([]uint, 0, pageSize)
|
||||
if err := query.Select("id").Order("created_at DESC, id DESC").
|
||||
Offset((page-1)*pageSize).Limit(pageSize).Pluck("id", &ids).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互日志分页ID失败")
|
||||
}
|
||||
rows := make([]model.IntegrationLog, 0, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return rows, nil
|
||||
}
|
||||
if err := q.db.WithContext(ctx).Select(
|
||||
"id", "integration_id", "provider", "direction", "operation",
|
||||
"resource_type", "resource_id", "resource_key", "result", "duration_ms",
|
||||
"state_changed", "request_id", "correlation_id", "created_at",
|
||||
).Where("id IN ?", ids).Order("created_at DESC, id DESC").Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量投影外部交互日志失败")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// Get 使用稳定 integration_id 查询结构化详情。
|
||||
func (q *Query) Get(ctx context.Context, integrationID string) (*Detail, error) {
|
||||
func (q *Query) Get(ctx context.Context, integrationID string) (*DetailResponse, error) {
|
||||
if err := q.authorize(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if integrationID == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceIntegration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var row model.IntegrationLog
|
||||
if err := q.db.WithContext(ctx).Where("integration_id = ?", integrationID).First(&row).Error; err != nil {
|
||||
if err := q.db.WithContext(ctx).Where("integration_id = ? AND created_at >= ?", integrationID, retention.OnlineFrom.UTC()).First(&row).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "外部交互日志不存在")
|
||||
}
|
||||
@@ -216,12 +253,12 @@ func (q *Query) Get(ctx context.Context, integrationID string) (*Detail, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attempts, err := q.loadAttempts(ctx, row)
|
||||
attempts, err := q.loadAttempts(ctx, row, retention.OnlineFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providerMessage, providerMessageFidelity := safeProviderMessage(row.ProviderMessage)
|
||||
return &Detail{
|
||||
return &DetailResponse{Detail: Detail{
|
||||
Identity: IdentityView{IntegrationID: row.IntegrationID, Provider: row.Provider, ProviderName: constants.IntegrationProviderName(row.Provider), Direction: row.Direction, DirectionName: constants.IntegrationDirectionName(row.Direction), Operation: row.Operation, OperationName: constants.IntegrationOperationName(row.Operation), ExternalID: row.ExternalID},
|
||||
Resource: resourceView(row), Trigger: TriggerView{Source: row.TriggerSource, Scene: row.TriggerScene, Series: row.TriggerSeries, Attempt: row.Attempt},
|
||||
Result: ResultView{Code: row.Result, Name: constants.IntegrationResultName(row.Result), Category: constants.IntegrationResultCategory(row.Result), HTTPStatus: row.HTTPStatus, ProviderCode: row.ProviderCode, ProviderMessage: providerMessage, DurationMS: row.DurationMS, StateChanged: row.StateChanged, RecoveryStrategy: row.RecoveryStrategy},
|
||||
@@ -235,13 +272,13 @@ func (q *Query) Get(ctx context.Context, integrationID string) (*Detail, error)
|
||||
ResourceIDAvailable: row.ResourceID != nil && *row.ResourceID != "",
|
||||
ProviderMessageFidelity: providerMessageFidelity,
|
||||
},
|
||||
}, nil
|
||||
}, Retention: retention}, nil
|
||||
}
|
||||
|
||||
func (q *Query) loadAttempts(ctx context.Context, current model.IntegrationLog) ([]AttemptView, error) {
|
||||
func (q *Query) loadAttempts(ctx context.Context, current model.IntegrationLog, onlineFrom time.Time) ([]AttemptView, error) {
|
||||
rows := []model.IntegrationLog{current}
|
||||
if current.TriggerSeries != nil && *current.TriggerSeries != "" {
|
||||
if err := q.db.WithContext(ctx).Where("trigger_series = ?", *current.TriggerSeries).
|
||||
if err := q.db.WithContext(ctx).Where("trigger_series = ? AND created_at >= ?", *current.TriggerSeries, onlineFrom.UTC()).
|
||||
Order("attempt ASC, created_at ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互尝试序列失败")
|
||||
}
|
||||
@@ -270,6 +307,15 @@ func (q *Query) authorize(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Query) normalizeOnlineFilter(ctx context.Context, filter ListFilter) (ListFilter, retentionquery.Info, error) {
|
||||
retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceIntegration)
|
||||
if err != nil {
|
||||
return filter, retention, err
|
||||
}
|
||||
filter.CreatedFrom, filter.CreatedTo, err = retentionquery.NormalizeRange(retention, filter.CreatedFrom, filter.CreatedTo, constants.IntegrationQueryMaxRange)
|
||||
return filter, retention, err
|
||||
}
|
||||
|
||||
func validFilter(filter ListFilter) bool {
|
||||
if filter.CreatedFrom == nil || filter.CreatedTo == nil || !filter.CreatedFrom.Before(*filter.CreatedTo) || filter.CreatedTo.Sub(*filter.CreatedFrom) > constants.IntegrationQueryMaxRange {
|
||||
return false
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -19,17 +20,18 @@ type OverviewFilter struct {
|
||||
|
||||
// Overview 是外部交互固定维度聚合结果。
|
||||
type Overview struct {
|
||||
Total int64 `json:"total"`
|
||||
AnomalyCount int64 `json:"anomaly_count"`
|
||||
UnknownCount int64 `json:"unknown_count"`
|
||||
StalePendingCount int64 `json:"stale_pending_count"`
|
||||
StateChangedCount int64 `json:"state_changed_count"`
|
||||
AverageDurationMS float64 `json:"average_duration_ms"`
|
||||
P95DurationMS float64 `json:"p95_duration_ms"`
|
||||
Results []ResultCount `json:"results"`
|
||||
Providers []NamedCount `json:"providers"`
|
||||
Directions []NamedCount `json:"directions"`
|
||||
Trend []TrendPoint `json:"trend"`
|
||||
Total int64 `json:"total"`
|
||||
AnomalyCount int64 `json:"anomaly_count"`
|
||||
UnknownCount int64 `json:"unknown_count"`
|
||||
StalePendingCount int64 `json:"stale_pending_count"`
|
||||
StateChangedCount int64 `json:"state_changed_count"`
|
||||
AverageDurationMS float64 `json:"average_duration_ms"`
|
||||
P95DurationMS float64 `json:"p95_duration_ms"`
|
||||
Results []ResultCount `json:"results"`
|
||||
Providers []NamedCount `json:"providers"`
|
||||
Directions []NamedCount `json:"directions"`
|
||||
Trend []TrendPoint `json:"trend"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
}
|
||||
|
||||
// ResultCount 是原始结果及其派生类别计数。
|
||||
@@ -66,11 +68,17 @@ func (q *Query) Overview(ctx context.Context, filter OverviewFilter) (*Overview,
|
||||
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{}}
|
||||
result := &Overview{Results: []ResultCount{}, Providers: []NamedCount{}, Directions: []NamedCount{}, Trend: []TrendPoint{}, Retention: retention}
|
||||
if err := loadOverviewMetrics(base, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user