fix: robustify analytics handlers and fix auth middleware scope
Moved AuthMiddleware to /v1 group only. Added COALESCE and empty result handling to analytics SQL queries to prevent 500 errors on empty databases.
This commit is contained in:
@@ -248,25 +248,33 @@ func (s *Server) handleUsageSummary(c *gin.Context) {
|
||||
TodayCost float64 `db:"today_cost"`
|
||||
}
|
||||
today := time.Now().UTC().Format("2006-01-02")
|
||||
_ = s.database.Get(&todayStats, `
|
||||
err = s.database.Get(&todayStats, `
|
||||
SELECT
|
||||
COUNT(*) as today_requests,
|
||||
COALESCE(SUM(cost), 0.0) as today_cost
|
||||
FROM llm_requests
|
||||
WHERE strftime('%Y-%m-%d', timestamp) = ?
|
||||
`, today)
|
||||
if err != nil {
|
||||
todayStats.TodayRequests = 0
|
||||
todayStats.TodayCost = 0.0
|
||||
}
|
||||
|
||||
// Error rate & Avg response time
|
||||
var miscStats struct {
|
||||
ErrorRate float64 `db:"error_rate"`
|
||||
AvgResponseTime float64 `db:"avg_response_time"`
|
||||
}
|
||||
_ = s.database.Get(&miscStats, `
|
||||
err = s.database.Get(&miscStats, `
|
||||
SELECT
|
||||
(CAST(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*)) * 100.0 as error_rate,
|
||||
CASE WHEN COUNT(*) = 0 THEN 0.0 ELSE (CAST(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*)) * 100.0 END as error_rate,
|
||||
COALESCE(AVG(duration_ms), 0.0) as avg_response_time
|
||||
FROM llm_requests
|
||||
`)
|
||||
if err != nil {
|
||||
miscStats.ErrorRate = 0.0
|
||||
miscStats.AvgResponseTime = 0.0
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||
"total_requests": totalStats.TotalRequests,
|
||||
@@ -329,12 +337,7 @@ func (s *Server) handleTimeSeries(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add granularity for the UI
|
||||
granularity := "day"
|
||||
if filter.Period == "24h" || filter.Period == "today" {
|
||||
// If we had hourly bucketing we'd use 'hour'
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||
"series": series,
|
||||
"granularity": granularity,
|
||||
@@ -360,7 +363,7 @@ func (s *Server) handleProvidersUsage(c *gin.Context) {
|
||||
GROUP BY provider
|
||||
`, clause), binds...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -379,20 +382,24 @@ func (s *Server) handleAnalyticsBreakdown(c *gin.Context) {
|
||||
Label string `db:"label"`
|
||||
Value int `db:"value"`
|
||||
}
|
||||
err := s.database.Select(&models, fmt.Sprintf("SELECT model as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY model ORDER BY value DESC", clause), binds...)
|
||||
err := s.database.Select(&models, fmt.Sprintf("SELECT COALESCE(model, 'unknown') as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY model ORDER BY value DESC", clause), binds...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
models = []struct {
|
||||
Label string `db:"label"`
|
||||
Value int `db:"value"`
|
||||
}{}
|
||||
}
|
||||
|
||||
var clients []struct {
|
||||
Label string `db:"label"`
|
||||
Value int `db:"value"`
|
||||
}
|
||||
err = s.database.Select(&clients, fmt.Sprintf("SELECT client_id as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY client_id ORDER BY value DESC", clause), binds...)
|
||||
err = s.database.Select(&clients, fmt.Sprintf("SELECT COALESCE(client_id, 'unknown') as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY client_id ORDER BY value DESC", clause), binds...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
clients = []struct {
|
||||
Label string `db:"label"`
|
||||
Value int `db:"value"`
|
||||
}{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||
@@ -412,14 +419,14 @@ func (s *Server) handleDetailedUsage(c *gin.Context) {
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
strftime('%%Y-%%m-%%d', timestamp) as date,
|
||||
client_id as client,
|
||||
provider,
|
||||
model,
|
||||
COALESCE(client_id, 'unknown') as client,
|
||||
COALESCE(provider, 'unknown') as provider,
|
||||
COALESCE(model, 'unknown') as model,
|
||||
COUNT(*) as requests,
|
||||
SUM(total_tokens) as tokens,
|
||||
SUM(cache_read_tokens) as cache_read_tokens,
|
||||
SUM(cache_write_tokens) as cache_write_tokens,
|
||||
SUM(cost) as cost
|
||||
COALESCE(SUM(total_tokens), 0) as tokens,
|
||||
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
||||
COALESCE(SUM(cache_write_tokens), 0) as cache_write_tokens,
|
||||
COALESCE(SUM(cost), 0.0) as cost
|
||||
FROM llm_requests
|
||||
WHERE 1=1 %s
|
||||
GROUP BY date, client, provider, model
|
||||
@@ -440,7 +447,7 @@ func (s *Server) handleDetailedUsage(c *gin.Context) {
|
||||
|
||||
err := s.database.Select(&rows, query, binds...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -483,7 +490,6 @@ func (s *Server) handleGetClients(c *gin.Context) {
|
||||
desc = *cl.Description
|
||||
}
|
||||
|
||||
// Get last used from tokens
|
||||
var lastUsed *time.Time
|
||||
_ = s.database.Get(&lastUsed, "SELECT MAX(last_used_at) FROM client_tokens WHERE client_id = ?", cl.ClientID)
|
||||
|
||||
|
||||
@@ -74,7 +74,8 @@ func NewServer(cfg *config.Config, database *db.DB) *Server {
|
||||
}
|
||||
|
||||
func (s *Server) setupRoutes() {
|
||||
s.router.Use(middleware.AuthMiddleware(s.database))
|
||||
// Global middleware should only be for logging/recovery
|
||||
// Auth is specific to groups
|
||||
|
||||
// Static files
|
||||
s.router.StaticFile("/", "./static/index.html")
|
||||
@@ -86,7 +87,9 @@ func (s *Server) setupRoutes() {
|
||||
// WebSocket
|
||||
s.router.GET("/ws", s.handleWebSocket)
|
||||
|
||||
// API V1 (External LLM Access)
|
||||
v1 := s.router.Group("/v1")
|
||||
v1.Use(middleware.AuthMiddleware(s.database))
|
||||
{
|
||||
v1.POST("/chat/completions", s.handleChatCompletions)
|
||||
v1.GET("/models", s.handleListModels)
|
||||
|
||||
Reference in New Issue
Block a user