Skip to content

Commit e48d1f4

Browse files
committed
feat(ops): add request error observability
1 parent 43339eb commit e48d1f4

17 files changed

Lines changed: 1331 additions & 121 deletions

admin/handler.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ func (h *Handler) RegisterRoutes(r *gin.Engine) {
139139
api.DELETE("/keys/:id", h.DeleteAPIKey)
140140
api.GET("/health", h.GetHealth)
141141
api.GET("/ops/overview", h.GetOpsOverview)
142+
api.GET("/ops/errors", h.GetOpsErrorLogs)
143+
api.GET("/ops/errors/summary", h.GetOpsErrorSummary)
142144
api.GET("/settings", h.GetSettings)
143145
api.PUT("/settings", h.UpdateSettings)
144146
api.POST("/settings/image-storage/test", h.TestImageStorageConnection)
@@ -1970,6 +1972,140 @@ func (h *Handler) GetChartData(c *gin.Context) {
19701972
c.JSON(http.StatusOK, result)
19711973
}
19721974

1975+
func parseOpsErrorPositiveInt64(c *gin.Context, name string) (*int64, bool) {
1976+
raw := strings.TrimSpace(c.Query(name))
1977+
if raw == "" {
1978+
return nil, true
1979+
}
1980+
parsed, err := strconv.ParseInt(raw, 10, 64)
1981+
if err != nil || parsed <= 0 {
1982+
writeError(c, http.StatusBadRequest, fmt.Sprintf("%s 参数无效,需要正整数", name))
1983+
return nil, false
1984+
}
1985+
return &parsed, true
1986+
}
1987+
1988+
func parseOpsErrorLogFilter(c *gin.Context, withPaging bool) (database.UsageLogFilter, bool) {
1989+
endTime := time.Now()
1990+
startTime := endTime.Add(-1 * time.Hour)
1991+
startStr := strings.TrimSpace(c.Query("start"))
1992+
endStr := strings.TrimSpace(c.Query("end"))
1993+
if startStr != "" || endStr != "" {
1994+
if startStr == "" || endStr == "" {
1995+
writeError(c, http.StatusBadRequest, "start/end 参数需要同时提供")
1996+
return database.UsageLogFilter{}, false
1997+
}
1998+
parsedStart, e1 := time.Parse(time.RFC3339, startStr)
1999+
parsedEnd, e2 := time.Parse(time.RFC3339, endStr)
2000+
if e1 != nil || e2 != nil {
2001+
writeError(c, http.StatusBadRequest, "start/end 参数格式错误,需要 RFC3339 格式")
2002+
return database.UsageLogFilter{}, false
2003+
}
2004+
startTime = parsedStart
2005+
endTime = parsedEnd
2006+
}
2007+
2008+
apiKeyID, ok := parseOpsErrorPositiveInt64(c, "api_key_id")
2009+
if !ok {
2010+
return database.UsageLogFilter{}, false
2011+
}
2012+
accountID, ok := parseOpsErrorPositiveInt64(c, "account_id")
2013+
if !ok {
2014+
return database.UsageLogFilter{}, false
2015+
}
2016+
2017+
filter := database.UsageLogFilter{
2018+
Start: startTime,
2019+
End: endTime,
2020+
Page: 1,
2021+
PageSize: 20,
2022+
Email: strings.TrimSpace(c.Query("email")),
2023+
Model: strings.TrimSpace(c.Query("model")),
2024+
Endpoint: strings.TrimSpace(c.Query("endpoint")),
2025+
APIKeyID: apiKeyID,
2026+
AccountID: accountID,
2027+
ErrorOnly: true,
2028+
IncludeCanceled: true,
2029+
ErrorKind: strings.TrimSpace(c.Query("error_kind")),
2030+
Query: strings.TrimSpace(c.Query("q")),
2031+
}
2032+
2033+
status := strings.TrimSpace(c.Query("status"))
2034+
if status == "" {
2035+
status = strings.TrimSpace(c.Query("status_code"))
2036+
}
2037+
switch strings.ToLower(status) {
2038+
case "", "all":
2039+
case "4xx", "5xx":
2040+
filter.StatusFamily = strings.ToLower(status)
2041+
default:
2042+
statusCode, err := strconv.Atoi(status)
2043+
if err != nil || statusCode < 100 || statusCode > 599 {
2044+
writeError(c, http.StatusBadRequest, "status/status_code 参数无效")
2045+
return database.UsageLogFilter{}, false
2046+
}
2047+
filter.StatusCode = statusCode
2048+
}
2049+
2050+
if fastStr := c.Query("fast"); fastStr != "" {
2051+
v := fastStr == "true"
2052+
filter.FastOnly = &v
2053+
}
2054+
if streamStr := c.Query("stream"); streamStr != "" {
2055+
v := streamStr == "true"
2056+
filter.StreamOnly = &v
2057+
}
2058+
2059+
if withPaging {
2060+
if pageStr := c.Query("page"); pageStr != "" {
2061+
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
2062+
filter.Page = page
2063+
}
2064+
}
2065+
if ps := c.Query("page_size"); ps != "" {
2066+
if n, err := strconv.Atoi(ps); err == nil && n > 0 && n <= 200 {
2067+
filter.PageSize = n
2068+
}
2069+
}
2070+
}
2071+
2072+
return filter, true
2073+
}
2074+
2075+
// GetOpsErrorLogs 获取运维错误日志
2076+
func (h *Handler) GetOpsErrorLogs(c *gin.Context) {
2077+
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
2078+
defer cancel()
2079+
2080+
filter, ok := parseOpsErrorLogFilter(c, true)
2081+
if !ok {
2082+
return
2083+
}
2084+
result, err := h.db.ListUsageLogsByTimeRangePaged(ctx, filter)
2085+
if err != nil {
2086+
writeInternalError(c, err)
2087+
return
2088+
}
2089+
c.JSON(http.StatusOK, result)
2090+
}
2091+
2092+
// GetOpsErrorSummary 获取运维错误日志概览
2093+
func (h *Handler) GetOpsErrorSummary(c *gin.Context) {
2094+
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
2095+
defer cancel()
2096+
2097+
filter, ok := parseOpsErrorLogFilter(c, false)
2098+
if !ok {
2099+
return
2100+
}
2101+
result, err := h.db.GetUsageErrorSummary(ctx, filter)
2102+
if err != nil {
2103+
writeInternalError(c, err)
2104+
return
2105+
}
2106+
c.JSON(http.StatusOK, result)
2107+
}
2108+
19732109
// GetUsageLogs 获取使用日志
19742110
func (h *Handler) GetUsageLogs(c *gin.Context) {
19752111
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)

0 commit comments

Comments
 (0)