Files
sub2api-cn-relay-manager/internal/store/sqlite/route_decision_logs_repo.go
2026-05-28 21:24:05 +08:00

209 lines
5.9 KiB
Go

package sqlite
import (
"context"
"fmt"
"strings"
)
type RouteDecisionLog struct {
ID int64
RequestID string
LogicalGroupID string
PublicModel string
UserKey string
ConversationKey string
StickyKey string
StickyKeyType string
StickyHit bool
SelectedRouteID string
SelectedShadowGroupID string
FallbackUsed bool
ErrorClass string
UpstreamStatus int
LatencyMS int
CreatedAt string
}
type RouteDecisionLogFilter struct {
RequestID string
LogicalGroupID string
PublicModel string
SelectedRouteID string
StickyKey string
Limit int
}
type RouteDecisionLogsRepo struct {
db execQuerier
}
func newRouteDecisionLogsRepo(db execQuerier) *RouteDecisionLogsRepo {
return &RouteDecisionLogsRepo{db: db}
}
func (r *RouteDecisionLogsRepo) Create(ctx context.Context, row RouteDecisionLog) (int64, error) {
row, err := normalizeRouteDecisionLog(row)
if err != nil {
return 0, err
}
result, err := r.db.ExecContext(
ctx,
`INSERT INTO route_decision_logs (
request_id,
logical_group_id,
public_model,
user_key,
conversation_key,
sticky_key,
sticky_key_type,
sticky_hit,
selected_route_id,
selected_shadow_group_id,
fallback_used,
error_class,
upstream_status,
latency_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
row.RequestID,
row.LogicalGroupID,
row.PublicModel,
row.UserKey,
row.ConversationKey,
row.StickyKey,
row.StickyKeyType,
boolToSQLiteInt(row.StickyHit),
row.SelectedRouteID,
row.SelectedShadowGroupID,
boolToSQLiteInt(row.FallbackUsed),
row.ErrorClass,
row.UpstreamStatus,
row.LatencyMS,
)
if err != nil {
return 0, fmt.Errorf("insert route decision log %q: %w", row.RequestID, err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("read inserted route decision log id for %q: %w", row.RequestID, err)
}
return id, nil
}
func (r *RouteDecisionLogsRepo) ListRecent(ctx context.Context, filter RouteDecisionLogFilter) ([]RouteDecisionLog, error) {
clauses := make([]string, 0, 5)
args := make([]any, 0, 6)
if requestID := strings.TrimSpace(filter.RequestID); requestID != "" {
clauses = append(clauses, "request_id = ?")
args = append(args, requestID)
}
if logicalGroupID := strings.TrimSpace(filter.LogicalGroupID); logicalGroupID != "" {
clauses = append(clauses, "logical_group_id = ?")
args = append(args, logicalGroupID)
}
if publicModel := strings.TrimSpace(filter.PublicModel); publicModel != "" {
clauses = append(clauses, "public_model = ?")
args = append(args, publicModel)
}
if selectedRouteID := strings.TrimSpace(filter.SelectedRouteID); selectedRouteID != "" {
clauses = append(clauses, "selected_route_id = ?")
args = append(args, selectedRouteID)
}
if stickyKey := strings.TrimSpace(filter.StickyKey); stickyKey != "" {
clauses = append(clauses, "sticky_key = ?")
args = append(args, stickyKey)
}
query := `SELECT id, request_id, logical_group_id, public_model, user_key, conversation_key, sticky_key, sticky_key_type, sticky_hit, selected_route_id, selected_shadow_group_id, fallback_used, error_class, upstream_status, latency_ms, created_at
FROM route_decision_logs`
if len(clauses) > 0 {
query += " WHERE " + strings.Join(clauses, " AND ")
}
query += " ORDER BY id DESC LIMIT ?"
args = append(args, normalizeRouteLogListLimit(filter.Limit))
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("list route decision logs: %w", err)
}
defer rows.Close()
items := make([]RouteDecisionLog, 0)
for rows.Next() {
var (
item RouteDecisionLog
stickyHit int
fallbackUsed int
)
if err := rows.Scan(
&item.ID,
&item.RequestID,
&item.LogicalGroupID,
&item.PublicModel,
&item.UserKey,
&item.ConversationKey,
&item.StickyKey,
&item.StickyKeyType,
&stickyHit,
&item.SelectedRouteID,
&item.SelectedShadowGroupID,
&fallbackUsed,
&item.ErrorClass,
&item.UpstreamStatus,
&item.LatencyMS,
&item.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan route decision log: %w", err)
}
item.StickyHit = stickyHit != 0
item.FallbackUsed = fallbackUsed != 0
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate route decision logs: %w", err)
}
return items, nil
}
func normalizeRouteDecisionLog(row RouteDecisionLog) (RouteDecisionLog, error) {
row.RequestID = strings.TrimSpace(row.RequestID)
row.LogicalGroupID = strings.TrimSpace(row.LogicalGroupID)
row.PublicModel = strings.TrimSpace(row.PublicModel)
row.UserKey = strings.TrimSpace(row.UserKey)
row.ConversationKey = strings.TrimSpace(row.ConversationKey)
row.StickyKey = strings.TrimSpace(row.StickyKey)
row.StickyKeyType = strings.TrimSpace(row.StickyKeyType)
row.SelectedRouteID = strings.TrimSpace(row.SelectedRouteID)
row.SelectedShadowGroupID = strings.TrimSpace(row.SelectedShadowGroupID)
row.ErrorClass = strings.TrimSpace(row.ErrorClass)
switch {
case row.RequestID == "":
return RouteDecisionLog{}, fmt.Errorf("request_id is required")
case row.LogicalGroupID == "":
return RouteDecisionLog{}, fmt.Errorf("logical_group_id is required")
case row.PublicModel == "":
return RouteDecisionLog{}, fmt.Errorf("public_model is required")
case row.SelectedRouteID == "":
return RouteDecisionLog{}, fmt.Errorf("selected_route_id is required")
case row.SelectedShadowGroupID == "":
return RouteDecisionLog{}, fmt.Errorf("selected_shadow_group_id is required")
case row.UpstreamStatus < 0:
return RouteDecisionLog{}, fmt.Errorf("upstream_status must be >= 0")
case row.LatencyMS < 0:
return RouteDecisionLog{}, fmt.Errorf("latency_ms must be >= 0")
}
return row, nil
}
func boolToSQLiteInt(value bool) int {
if value {
return 1
}
return 0
}