mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(api): add admin traffic chart statistics endpoint
Add new endpoint GET /admin/logs/stats/traffic-chart to provide aggregated traffic metrics grouped by time and model. Features include: - Time granularity selection (hour/minute) - Top-N model breakdown with "other" aggregation - Metrics for request counts and token usage Includes generated Swagger documentation.
This commit is contained in:
@@ -368,6 +368,7 @@ func main() {
|
|||||||
adminGroup.GET("/logs", handler.ListLogs)
|
adminGroup.GET("/logs", handler.ListLogs)
|
||||||
adminGroup.DELETE("/logs", handler.DeleteLogs)
|
adminGroup.DELETE("/logs", handler.DeleteLogs)
|
||||||
adminGroup.GET("/logs/stats", handler.LogStats)
|
adminGroup.GET("/logs/stats", handler.LogStats)
|
||||||
|
adminGroup.GET("/logs/stats/traffic-chart", handler.GetTrafficChart)
|
||||||
adminGroup.GET("/logs/webhook", handler.GetLogWebhookConfig)
|
adminGroup.GET("/logs/webhook", handler.GetLogWebhookConfig)
|
||||||
adminGroup.PUT("/logs/webhook", handler.UpdateLogWebhookConfig)
|
adminGroup.PUT("/logs/webhook", handler.UpdateLogWebhookConfig)
|
||||||
adminGroup.GET("/stats", adminHandler.GetAdminStats)
|
adminGroup.GET("/stats", adminHandler.GetAdminStats)
|
||||||
|
|||||||
@@ -1610,6 +1610,73 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/logs/stats/traffic-chart": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get time × model aggregated data for stacked traffic charts. Returns time buckets with per-model breakdown.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"summary": "Traffic chart data (admin)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"hour",
|
||||||
|
"minute"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Time granularity: hour (default) or minute",
|
||||||
|
"name": "granularity",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Start time (unix seconds), defaults to 24h ago",
|
||||||
|
"name": "since",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "End time (unix seconds), defaults to now",
|
||||||
|
"name": "until",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of top models to return (1-20), defaults to 5",
|
||||||
|
"name": "top_n",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/internal_api.TrafficChartResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/gin.H"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/gin.H"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/logs/webhook": {
|
"/admin/logs/webhook": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -5840,6 +5907,66 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"internal_api.TrafficBucket": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"breakdown": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/internal_api.TrafficMetrics"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"$ref": "#/definitions/internal_api.TrafficMetrics"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal_api.TrafficChartResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"buckets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/internal_api.TrafficBucket"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"granularity": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"since": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"until": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal_api.TrafficMetrics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"tokens_in": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"tokens_out": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"internal_api.UpdateAccessRequest": {
|
"internal_api.UpdateAccessRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1604,6 +1604,73 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/logs/stats/traffic-chart": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get time × model aggregated data for stacked traffic charts. Returns time buckets with per-model breakdown.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"summary": "Traffic chart data (admin)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"hour",
|
||||||
|
"minute"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Time granularity: hour (default) or minute",
|
||||||
|
"name": "granularity",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Start time (unix seconds), defaults to 24h ago",
|
||||||
|
"name": "since",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "End time (unix seconds), defaults to now",
|
||||||
|
"name": "until",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of top models to return (1-20), defaults to 5",
|
||||||
|
"name": "top_n",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/internal_api.TrafficChartResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/gin.H"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/gin.H"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/logs/webhook": {
|
"/admin/logs/webhook": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -5834,6 +5901,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"internal_api.TrafficBucket": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"breakdown": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/internal_api.TrafficMetrics"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"$ref": "#/definitions/internal_api.TrafficMetrics"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal_api.TrafficChartResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"buckets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/internal_api.TrafficBucket"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"granularity": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"since": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"until": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal_api.TrafficMetrics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"tokens_in": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"tokens_out": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"internal_api.UpdateAccessRequest": {
|
"internal_api.UpdateAccessRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1055,6 +1055,45 @@ definitions:
|
|||||||
tokens:
|
tokens:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
internal_api.TrafficBucket:
|
||||||
|
properties:
|
||||||
|
breakdown:
|
||||||
|
additionalProperties:
|
||||||
|
$ref: '#/definitions/internal_api.TrafficMetrics'
|
||||||
|
type: object
|
||||||
|
time:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
total:
|
||||||
|
$ref: '#/definitions/internal_api.TrafficMetrics'
|
||||||
|
type: object
|
||||||
|
internal_api.TrafficChartResponse:
|
||||||
|
properties:
|
||||||
|
buckets:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/internal_api.TrafficBucket'
|
||||||
|
type: array
|
||||||
|
granularity:
|
||||||
|
type: string
|
||||||
|
models:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
since:
|
||||||
|
type: integer
|
||||||
|
until:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
internal_api.TrafficMetrics:
|
||||||
|
properties:
|
||||||
|
count:
|
||||||
|
type: integer
|
||||||
|
tokens_in:
|
||||||
|
type: integer
|
||||||
|
tokens_out:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
internal_api.UpdateAccessRequest:
|
internal_api.UpdateAccessRequest:
|
||||||
properties:
|
properties:
|
||||||
default_namespace:
|
default_namespace:
|
||||||
@@ -2333,6 +2372,50 @@ paths:
|
|||||||
summary: Log stats (admin)
|
summary: Log stats (admin)
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
|
/admin/logs/stats/traffic-chart:
|
||||||
|
get:
|
||||||
|
description: Get time × model aggregated data for stacked traffic charts. Returns
|
||||||
|
time buckets with per-model breakdown.
|
||||||
|
parameters:
|
||||||
|
- description: 'Time granularity: hour (default) or minute'
|
||||||
|
enum:
|
||||||
|
- hour
|
||||||
|
- minute
|
||||||
|
in: query
|
||||||
|
name: granularity
|
||||||
|
type: string
|
||||||
|
- description: Start time (unix seconds), defaults to 24h ago
|
||||||
|
in: query
|
||||||
|
name: since
|
||||||
|
type: integer
|
||||||
|
- description: End time (unix seconds), defaults to now
|
||||||
|
in: query
|
||||||
|
name: until
|
||||||
|
type: integer
|
||||||
|
- description: Number of top models to return (1-20), defaults to 5
|
||||||
|
in: query
|
||||||
|
name: top_n
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/internal_api.TrafficChartResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/gin.H'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/gin.H'
|
||||||
|
security:
|
||||||
|
- AdminAuth: []
|
||||||
|
summary: Traffic chart data (admin)
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
/admin/logs/webhook:
|
/admin/logs/webhook:
|
||||||
get:
|
get:
|
||||||
description: Returns current webhook notification config
|
description: Returns current webhook notification config
|
||||||
|
|||||||
@@ -609,6 +609,223 @@ func (h *Handler) logStatsByMinute(c *gin.Context, q *gorm.DB, sinceTime, untilT
|
|||||||
c.JSON(http.StatusOK, GroupedStatsResponse{Items: items})
|
c.JSON(http.StatusOK, GroupedStatsResponse{Items: items})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TrafficMetrics contains the metrics for a model in a bucket
|
||||||
|
type TrafficMetrics struct {
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
TokensIn int64 `json:"tokens_in"`
|
||||||
|
TokensOut int64 `json:"tokens_out"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrafficBucket represents one time bucket with model breakdown
|
||||||
|
type TrafficBucket struct {
|
||||||
|
Time string `json:"time"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Breakdown map[string]TrafficMetrics `json:"breakdown"`
|
||||||
|
Total TrafficMetrics `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrafficChartResponse is the response for traffic chart API
|
||||||
|
type TrafficChartResponse struct {
|
||||||
|
Granularity string `json:"granularity"`
|
||||||
|
Since int64 `json:"since"`
|
||||||
|
Until int64 `json:"until"`
|
||||||
|
Models []string `json:"models"`
|
||||||
|
Buckets []TrafficBucket `json:"buckets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTrafficTopN = 5
|
||||||
|
maxTrafficTopN = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTrafficChart godoc
|
||||||
|
// @Summary Traffic chart data (admin)
|
||||||
|
// @Description Get time × model aggregated data for stacked traffic charts. Returns time buckets with per-model breakdown.
|
||||||
|
// @Tags admin
|
||||||
|
// @Produce json
|
||||||
|
// @Security AdminAuth
|
||||||
|
// @Param granularity query string false "Time granularity: hour (default) or minute" Enums(hour, minute)
|
||||||
|
// @Param since query int false "Start time (unix seconds), defaults to 24h ago"
|
||||||
|
// @Param until query int false "End time (unix seconds), defaults to now"
|
||||||
|
// @Param top_n query int false "Number of top models to return (1-20), defaults to 5"
|
||||||
|
// @Success 200 {object} TrafficChartResponse
|
||||||
|
// @Failure 400 {object} gin.H
|
||||||
|
// @Failure 500 {object} gin.H
|
||||||
|
// @Router /admin/logs/stats/traffic-chart [get]
|
||||||
|
func (h *Handler) GetTrafficChart(c *gin.Context) {
|
||||||
|
// Parse granularity
|
||||||
|
granularity := strings.TrimSpace(c.Query("granularity"))
|
||||||
|
if granularity == "" {
|
||||||
|
granularity = "hour"
|
||||||
|
}
|
||||||
|
if granularity != "hour" && granularity != "minute" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "granularity must be 'hour' or 'minute'"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse time range
|
||||||
|
now := time.Now().UTC()
|
||||||
|
var sinceTime, untilTime time.Time
|
||||||
|
|
||||||
|
if t, ok := parseUnixSeconds(c.Query("since")); ok {
|
||||||
|
sinceTime = t
|
||||||
|
} else {
|
||||||
|
sinceTime = now.Add(-24 * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, ok := parseUnixSeconds(c.Query("until")); ok {
|
||||||
|
untilTime = t
|
||||||
|
} else {
|
||||||
|
untilTime = now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate time range for minute granularity
|
||||||
|
if granularity == "minute" {
|
||||||
|
if c.Query("since") == "" || c.Query("until") == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "minute-level aggregation requires both 'since' and 'until' parameters"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
duration := untilTime.Sub(sinceTime)
|
||||||
|
if duration > maxMinuteRangeDuration {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "time range too large for minute granularity",
|
||||||
|
"max_hours": 6,
|
||||||
|
"actual_hours": duration.Hours(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if duration < 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "'until' must be after 'since'"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse top_n
|
||||||
|
topN := defaultTrafficTopN
|
||||||
|
if raw := strings.TrimSpace(c.Query("top_n")); raw != "" {
|
||||||
|
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
|
||||||
|
topN = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if topN > maxTrafficTopN {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "top_n cannot exceed 20"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
q := h.logBaseQuery().
|
||||||
|
Where("created_at >= ?", sinceTime).
|
||||||
|
Where("created_at <= ?", untilTime)
|
||||||
|
|
||||||
|
// Select with time truncation based on granularity
|
||||||
|
var truncFunc string
|
||||||
|
if granularity == "minute" {
|
||||||
|
truncFunc = "DATE_TRUNC('minute', created_at)"
|
||||||
|
} else {
|
||||||
|
truncFunc = "DATE_TRUNC('hour', created_at)"
|
||||||
|
}
|
||||||
|
|
||||||
|
type bucketModelStats struct {
|
||||||
|
Bucket time.Time
|
||||||
|
ModelName string
|
||||||
|
Cnt int64
|
||||||
|
TokensIn int64
|
||||||
|
TokensOut int64
|
||||||
|
}
|
||||||
|
var rows []bucketModelStats
|
||||||
|
|
||||||
|
if err := q.Select(truncFunc+" as bucket, model_name, COUNT(*) as cnt, COALESCE(SUM(tokens_in),0) as tokens_in, COALESCE(SUM(tokens_out),0) as tokens_out").
|
||||||
|
Group("bucket, model_name").
|
||||||
|
Order("bucket ASC, cnt DESC").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to aggregate traffic data", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate global model counts for top_n selection
|
||||||
|
modelCounts := make(map[string]int64)
|
||||||
|
for _, r := range rows {
|
||||||
|
modelCounts[r.ModelName] += r.Cnt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get top N models
|
||||||
|
type modelCount struct {
|
||||||
|
name string
|
||||||
|
count int64
|
||||||
|
}
|
||||||
|
var modelList []modelCount
|
||||||
|
for name, cnt := range modelCounts {
|
||||||
|
modelList = append(modelList, modelCount{name, cnt})
|
||||||
|
}
|
||||||
|
// Sort by count descending
|
||||||
|
for i := 0; i < len(modelList)-1; i++ {
|
||||||
|
for j := i + 1; j < len(modelList); j++ {
|
||||||
|
if modelList[j].count > modelList[i].count {
|
||||||
|
modelList[i], modelList[j] = modelList[j], modelList[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topModels := make(map[string]bool)
|
||||||
|
var modelNames []string
|
||||||
|
for i := 0; i < len(modelList) && i < topN; i++ {
|
||||||
|
topModels[modelList[i].name] = true
|
||||||
|
modelNames = append(modelNames, modelList[i].name)
|
||||||
|
}
|
||||||
|
hasOther := len(modelList) > topN
|
||||||
|
if hasOther {
|
||||||
|
modelNames = append(modelNames, "other")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build buckets with breakdown
|
||||||
|
bucketMap := make(map[int64]*TrafficBucket)
|
||||||
|
var bucketOrder []int64
|
||||||
|
|
||||||
|
for _, r := range rows {
|
||||||
|
ts := r.Bucket.Unix()
|
||||||
|
bucket, exists := bucketMap[ts]
|
||||||
|
if !exists {
|
||||||
|
bucket = &TrafficBucket{
|
||||||
|
Time: r.Bucket.UTC().Format(time.RFC3339),
|
||||||
|
Timestamp: ts,
|
||||||
|
Breakdown: make(map[string]TrafficMetrics),
|
||||||
|
}
|
||||||
|
bucketMap[ts] = bucket
|
||||||
|
bucketOrder = append(bucketOrder, ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
modelKey := r.ModelName
|
||||||
|
if !topModels[modelKey] {
|
||||||
|
modelKey = "other"
|
||||||
|
}
|
||||||
|
|
||||||
|
existing := bucket.Breakdown[modelKey]
|
||||||
|
bucket.Breakdown[modelKey] = TrafficMetrics{
|
||||||
|
Count: existing.Count + r.Cnt,
|
||||||
|
TokensIn: existing.TokensIn + r.TokensIn,
|
||||||
|
TokensOut: existing.TokensOut + r.TokensOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.Total.Count += r.Cnt
|
||||||
|
bucket.Total.TokensIn += r.TokensIn
|
||||||
|
bucket.Total.TokensOut += r.TokensOut
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response buckets in order
|
||||||
|
buckets := make([]TrafficBucket, 0, len(bucketOrder))
|
||||||
|
for _, ts := range bucketOrder {
|
||||||
|
buckets = append(buckets, *bucketMap[ts])
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, TrafficChartResponse{
|
||||||
|
Granularity: granularity,
|
||||||
|
Since: sinceTime.Unix(),
|
||||||
|
Until: untilTime.Unix(),
|
||||||
|
Models: modelNames,
|
||||||
|
Buckets: buckets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ListSelfLogs godoc
|
// ListSelfLogs godoc
|
||||||
// @Summary List logs (master)
|
// @Summary List logs (master)
|
||||||
// @Description List request logs for the authenticated master
|
// @Description List request logs for the authenticated master
|
||||||
|
|||||||
Reference in New Issue
Block a user