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"`
|
TodayCost float64 `db:"today_cost"`
|
||||||
}
|
}
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := time.Now().UTC().Format("2006-01-02")
|
||||||
_ = s.database.Get(&todayStats, `
|
err = s.database.Get(&todayStats, `
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as today_requests,
|
COUNT(*) as today_requests,
|
||||||
COALESCE(SUM(cost), 0.0) as today_cost
|
COALESCE(SUM(cost), 0.0) as today_cost
|
||||||
FROM llm_requests
|
FROM llm_requests
|
||||||
WHERE strftime('%Y-%m-%d', timestamp) = ?
|
WHERE strftime('%Y-%m-%d', timestamp) = ?
|
||||||
`, today)
|
`, today)
|
||||||
|
if err != nil {
|
||||||
|
todayStats.TodayRequests = 0
|
||||||
|
todayStats.TodayCost = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
// Error rate & Avg response time
|
// Error rate & Avg response time
|
||||||
var miscStats struct {
|
var miscStats struct {
|
||||||
ErrorRate float64 `db:"error_rate"`
|
ErrorRate float64 `db:"error_rate"`
|
||||||
AvgResponseTime float64 `db:"avg_response_time"`
|
AvgResponseTime float64 `db:"avg_response_time"`
|
||||||
}
|
}
|
||||||
_ = s.database.Get(&miscStats, `
|
err = s.database.Get(&miscStats, `
|
||||||
SELECT
|
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
|
COALESCE(AVG(duration_ms), 0.0) as avg_response_time
|
||||||
FROM llm_requests
|
FROM llm_requests
|
||||||
`)
|
`)
|
||||||
|
if err != nil {
|
||||||
|
miscStats.ErrorRate = 0.0
|
||||||
|
miscStats.AvgResponseTime = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||||
"total_requests": totalStats.TotalRequests,
|
"total_requests": totalStats.TotalRequests,
|
||||||
@@ -329,12 +337,7 @@ func (s *Server) handleTimeSeries(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add granularity for the UI
|
|
||||||
granularity := "day"
|
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{
|
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||||
"series": series,
|
"series": series,
|
||||||
"granularity": granularity,
|
"granularity": granularity,
|
||||||
@@ -360,7 +363,7 @@ func (s *Server) handleProvidersUsage(c *gin.Context) {
|
|||||||
GROUP BY provider
|
GROUP BY provider
|
||||||
`, clause), binds...)
|
`, clause), binds...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,20 +382,24 @@ func (s *Server) handleAnalyticsBreakdown(c *gin.Context) {
|
|||||||
Label string `db:"label"`
|
Label string `db:"label"`
|
||||||
Value int `db:"value"`
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
models = []struct {
|
||||||
return
|
Label string `db:"label"`
|
||||||
|
Value int `db:"value"`
|
||||||
|
}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var clients []struct {
|
var clients []struct {
|
||||||
Label string `db:"label"`
|
Label string `db:"label"`
|
||||||
Value int `db:"value"`
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
clients = []struct {
|
||||||
return
|
Label string `db:"label"`
|
||||||
|
Value int `db:"value"`
|
||||||
|
}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||||
@@ -412,14 +419,14 @@ func (s *Server) handleDetailedUsage(c *gin.Context) {
|
|||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
strftime('%%Y-%%m-%%d', timestamp) as date,
|
strftime('%%Y-%%m-%%d', timestamp) as date,
|
||||||
client_id as client,
|
COALESCE(client_id, 'unknown') as client,
|
||||||
provider,
|
COALESCE(provider, 'unknown') as provider,
|
||||||
model,
|
COALESCE(model, 'unknown') as model,
|
||||||
COUNT(*) as requests,
|
COUNT(*) as requests,
|
||||||
SUM(total_tokens) as tokens,
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
||||||
SUM(cache_read_tokens) as cache_read_tokens,
|
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
||||||
SUM(cache_write_tokens) as cache_write_tokens,
|
COALESCE(SUM(cache_write_tokens), 0) as cache_write_tokens,
|
||||||
SUM(cost) as cost
|
COALESCE(SUM(cost), 0.0) as cost
|
||||||
FROM llm_requests
|
FROM llm_requests
|
||||||
WHERE 1=1 %s
|
WHERE 1=1 %s
|
||||||
GROUP BY date, client, provider, model
|
GROUP BY date, client, provider, model
|
||||||
@@ -440,7 +447,7 @@ func (s *Server) handleDetailedUsage(c *gin.Context) {
|
|||||||
|
|
||||||
err := s.database.Select(&rows, query, binds...)
|
err := s.database.Select(&rows, query, binds...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,7 +490,6 @@ func (s *Server) handleGetClients(c *gin.Context) {
|
|||||||
desc = *cl.Description
|
desc = *cl.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get last used from tokens
|
|
||||||
var lastUsed *time.Time
|
var lastUsed *time.Time
|
||||||
_ = s.database.Get(&lastUsed, "SELECT MAX(last_used_at) FROM client_tokens WHERE client_id = ?", cl.ClientID)
|
_ = 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() {
|
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
|
// Static files
|
||||||
s.router.StaticFile("/", "./static/index.html")
|
s.router.StaticFile("/", "./static/index.html")
|
||||||
@@ -86,7 +87,9 @@ func (s *Server) setupRoutes() {
|
|||||||
// WebSocket
|
// WebSocket
|
||||||
s.router.GET("/ws", s.handleWebSocket)
|
s.router.GET("/ws", s.handleWebSocket)
|
||||||
|
|
||||||
|
// API V1 (External LLM Access)
|
||||||
v1 := s.router.Group("/v1")
|
v1 := s.router.Group("/v1")
|
||||||
|
v1.Use(middleware.AuthMiddleware(s.database))
|
||||||
{
|
{
|
||||||
v1.POST("/chat/completions", s.handleChatCompletions)
|
v1.POST("/chat/completions", s.handleChatCompletions)
|
||||||
v1.GET("/models", s.handleListModels)
|
v1.GET("/models", s.handleListModels)
|
||||||
|
|||||||
Reference in New Issue
Block a user