feat(query-api): add richer stats and retention
This commit is contained in:
@@ -16,14 +16,22 @@ type Summary struct {
|
||||
RunsToday int `json:"runs_today"`
|
||||
ToolCallsToday int `json:"tool_calls_today"`
|
||||
ErrorsToday int `json:"errors_today"`
|
||||
TokensToday int64 `json:"tokens_today"`
|
||||
CostToday float64 `json:"cost_today"`
|
||||
AvgDurationMS float64 `json:"avg_duration_ms"`
|
||||
ByFramework map[string]FrameworkStats `json:"by_framework"`
|
||||
}
|
||||
|
||||
type TimeseriesBucket struct {
|
||||
TS time.Time `json:"ts"`
|
||||
Runs int `json:"runs"`
|
||||
Tools int `json:"tools"`
|
||||
Errors int `json:"errors"`
|
||||
TS time.Time `json:"ts"`
|
||||
Runs int `json:"runs"`
|
||||
Tools int `json:"tools"`
|
||||
Errors int `json:"errors"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
Cost float64 `json:"cost"`
|
||||
AvgDurationMS float64 `json:"avg_duration_ms"`
|
||||
}
|
||||
|
||||
type TimeseriesResult struct {
|
||||
@@ -36,21 +44,23 @@ func (d *DB) GetSummary(ctx context.Context) (*Summary, error) {
|
||||
now := time.Now()
|
||||
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
// Active sessions: sessions with a session.start but no session.end (ever)
|
||||
// Active sessions: sessions with a session.start but no session.end (last 7 days)
|
||||
activeQ := `
|
||||
SELECT COUNT(DISTINCT session_id)
|
||||
FROM events
|
||||
WHERE type = 'session.start'
|
||||
AND session_id IS NOT NULL
|
||||
AND session_id NOT IN (
|
||||
SELECT DISTINCT session_id
|
||||
FROM events
|
||||
WHERE type = 'session.end'
|
||||
AND session_id IS NOT NULL
|
||||
SELECT COUNT(DISTINCT e.session_id)
|
||||
FROM events e
|
||||
WHERE e.type = 'session.start'
|
||||
AND e.session_id IS NOT NULL
|
||||
AND e.ts >= $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM events e2
|
||||
WHERE e2.type = 'session.end'
|
||||
AND e2.session_id = e.session_id
|
||||
)
|
||||
`
|
||||
var activeSessions int
|
||||
if err := d.sql.QueryRowContext(ctx, activeQ).Scan(&activeSessions); err != nil {
|
||||
activeSessionsSince := time.Now().Add(-7 * 24 * time.Hour)
|
||||
if err := d.sql.QueryRowContext(ctx, activeQ, activeSessionsSince).Scan(&activeSessions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -64,6 +74,7 @@ func (d *DB) GetSummary(ctx context.Context) (*Summary, error) {
|
||||
COUNT(*) FILTER (WHERE type = 'error') AS errors
|
||||
FROM events
|
||||
WHERE ts >= $1
|
||||
AND type IN ('run.start', 'span.end', 'error')
|
||||
GROUP BY source_framework
|
||||
`
|
||||
rows, err := d.sql.QueryContext(ctx, fwQ, midnight)
|
||||
@@ -90,11 +101,30 @@ func (d *DB) GetSummary(ctx context.Context) (*Summary, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Usage stats for today (tokens, cost, avg latency)
|
||||
usageQ := `
|
||||
SELECT
|
||||
COALESCE(SUM((payload->'payload'->'usage'->>'total_tokens')::bigint), 0),
|
||||
COALESCE(SUM((payload->'payload'->'usage'->>'total_cost')::float8), 0),
|
||||
COALESCE(AVG((payload->'payload'->>'duration_ms')::float8), 0)
|
||||
FROM events
|
||||
WHERE type = 'run.end'
|
||||
AND ts >= $1
|
||||
`
|
||||
var tokensToday int64
|
||||
var costToday, avgDurationMS float64
|
||||
if err := d.sql.QueryRowContext(ctx, usageQ, midnight).Scan(&tokensToday, &costToday, &avgDurationMS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Summary{
|
||||
ActiveSessions: activeSessions,
|
||||
RunsToday: totalRuns,
|
||||
ToolCallsToday: totalTools,
|
||||
ErrorsToday: totalErrors,
|
||||
TokensToday: tokensToday,
|
||||
CostToday: costToday,
|
||||
AvgDurationMS: avgDurationMS,
|
||||
ByFramework: byFramework,
|
||||
}, nil
|
||||
}
|
||||
@@ -104,6 +134,11 @@ type TopTool struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type TopModel struct {
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func (d *DB) GetTopTools(ctx context.Context, limit int) ([]TopTool, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
@@ -141,6 +176,43 @@ func (d *DB) GetTopTools(ctx context.Context, limit int) ([]TopTool, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (d *DB) GetTopModels(ctx context.Context, limit int) ([]TopModel, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
now := time.Now()
|
||||
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
q := `
|
||||
SELECT
|
||||
payload->'payload'->>'model' AS model_name,
|
||||
COUNT(*) AS cnt
|
||||
FROM events
|
||||
WHERE type = 'run.end'
|
||||
AND payload->'payload'->>'model' IS NOT NULL
|
||||
AND payload->'payload'->>'model' <> ''
|
||||
AND ts >= $1
|
||||
GROUP BY model_name
|
||||
ORDER BY cnt DESC
|
||||
LIMIT $2
|
||||
`
|
||||
rows, err := d.sql.QueryContext(ctx, q, midnight, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []TopModel
|
||||
for rows.Next() {
|
||||
var m TopModel
|
||||
if err := rows.Scan(&m.Name, &m.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func bucketForWindow(window string) string {
|
||||
switch window {
|
||||
case "1h":
|
||||
@@ -191,9 +263,20 @@ func (d *DB) GetTimeseries(ctx context.Context, window string) (*TimeseriesResul
|
||||
COUNT(*) FILTER (WHERE type = 'run.start') AS runs,
|
||||
COUNT(*) FILTER (WHERE type = 'span.end'
|
||||
AND payload->'attributes'->>'span_kind' = 'tool') AS tools,
|
||||
COUNT(*) FILTER (WHERE type = 'error') AS errors
|
||||
COUNT(*) FILTER (WHERE type = 'error') AS errors,
|
||||
COALESCE(SUM((payload->'payload'->'usage'->>'total_tokens')::bigint)
|
||||
FILTER (WHERE type = 'run.end'), 0) AS tokens,
|
||||
COALESCE(SUM((payload->'payload'->'usage'->>'input_tokens')::bigint)
|
||||
FILTER (WHERE type = 'run.end'), 0) AS input_tokens,
|
||||
COALESCE(SUM((payload->'payload'->'usage'->>'output_tokens')::bigint)
|
||||
FILTER (WHERE type = 'run.end'), 0) AS output_tokens,
|
||||
COALESCE(SUM((payload->'payload'->'usage'->>'total_cost')::float8)
|
||||
FILTER (WHERE type = 'run.end'), 0) AS cost,
|
||||
COALESCE(AVG((payload->'payload'->>'duration_ms')::float8)
|
||||
FILTER (WHERE type = 'run.end'), 0) AS avg_duration_ms
|
||||
FROM events
|
||||
WHERE ts >= $2
|
||||
AND type IN ('run.start', 'run.end', 'span.end', 'error')
|
||||
GROUP BY bucket_ts
|
||||
ORDER BY bucket_ts ASC
|
||||
`
|
||||
@@ -207,7 +290,8 @@ func (d *DB) GetTimeseries(ctx context.Context, window string) (*TimeseriesResul
|
||||
var series []TimeseriesBucket
|
||||
for rows.Next() {
|
||||
var b TimeseriesBucket
|
||||
if err := rows.Scan(&b.TS, &b.Runs, &b.Tools, &b.Errors); err != nil {
|
||||
if err := rows.Scan(&b.TS, &b.Runs, &b.Tools, &b.Errors,
|
||||
&b.Tokens, &b.InputTokens, &b.OutputTokens, &b.Cost, &b.AvgDurationMS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
series = append(series, b)
|
||||
|
||||
Reference in New Issue
Block a user