feat(ai): add recent metrics events
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
97b1854857
commit
3c0d9f853c
3 changed files with 84 additions and 4 deletions
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -152,11 +153,21 @@ func Summary(events []Event) map[string]any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recent := make([]Event, len(events))
|
||||||
|
copy(recent, events)
|
||||||
|
sort.SliceStable(recent, func(i, j int) bool {
|
||||||
|
return recent[i].Timestamp.After(recent[j].Timestamp)
|
||||||
|
})
|
||||||
|
if len(recent) > 10 {
|
||||||
|
recent = recent[:10]
|
||||||
|
}
|
||||||
|
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"total": len(events),
|
"total": len(events),
|
||||||
"by_type": sortedMap(byType),
|
"by_type": sortedMap(byType),
|
||||||
"by_repo": sortedMap(byRepo),
|
"by_repo": sortedMap(byRepo),
|
||||||
"by_agent": sortedMap(byAgent),
|
"by_agent": sortedMap(byAgent),
|
||||||
|
"events": briefEvents(recent),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,3 +192,22 @@ func sortedMap(m map[string]int) []map[string]any {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// briefEvents converts events into the compact shape used by metrics_query.
|
||||||
|
func briefEvents(events []Event) []map[string]any {
|
||||||
|
result := make([]map[string]any, len(events))
|
||||||
|
for i, ev := range events {
|
||||||
|
item := map[string]any{
|
||||||
|
"type": ev.Type,
|
||||||
|
"timestamp": ev.Timestamp,
|
||||||
|
}
|
||||||
|
if ev.AgentID != "" {
|
||||||
|
item["agent_id"] = ev.AgentID
|
||||||
|
}
|
||||||
|
if ev.Repo != "" {
|
||||||
|
item["repo"] = ev.Repo
|
||||||
|
}
|
||||||
|
result[i] = item
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -228,9 +228,9 @@ func TestSummary_Good_Empty(t *testing.T) {
|
||||||
|
|
||||||
func TestSummary_Good(t *testing.T) {
|
func TestSummary_Good(t *testing.T) {
|
||||||
events := []Event{
|
events := []Event{
|
||||||
{Type: "build", Repo: "core-php", AgentID: "agent-1"},
|
{Type: "build", Repo: "core-php", AgentID: "agent-1", Timestamp: time.Date(2026, 3, 15, 9, 0, 0, 0, time.UTC)},
|
||||||
{Type: "build", Repo: "core-php", AgentID: "agent-2"},
|
{Type: "build", Repo: "core-php", AgentID: "agent-2", Timestamp: time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)},
|
||||||
{Type: "test", Repo: "core-api", AgentID: "agent-1"},
|
{Type: "test", Repo: "core-api", AgentID: "agent-1", Timestamp: time.Date(2026, 3, 15, 11, 0, 0, 0, time.UTC)},
|
||||||
}
|
}
|
||||||
|
|
||||||
s := Summary(events)
|
s := Summary(events)
|
||||||
|
|
@ -248,6 +248,39 @@ func TestSummary_Good(t *testing.T) {
|
||||||
if byType[0]["key"] != "build" || byType[0]["count"] != 2 {
|
if byType[0]["key"] != "build" || byType[0]["count"] != 2 {
|
||||||
t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"])
|
t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recent, _ := s["events"].([]map[string]any)
|
||||||
|
if len(recent) != 3 {
|
||||||
|
t.Fatalf("expected 3 recent events, got %d", len(recent))
|
||||||
|
}
|
||||||
|
if recent[0]["type"] != "test" {
|
||||||
|
t.Errorf("expected newest event first, got %v", recent[0]["type"])
|
||||||
|
}
|
||||||
|
if _, ok := recent[0]["timestamp"].(time.Time); !ok {
|
||||||
|
t.Errorf("expected timestamp to be time.Time, got %T", recent[0]["timestamp"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSummary_Good_RecentEventsLimit(t *testing.T) {
|
||||||
|
events := make([]Event, 0, 12)
|
||||||
|
for i := 0; i < 12; i++ {
|
||||||
|
events = append(events, Event{
|
||||||
|
Type: "type",
|
||||||
|
Timestamp: time.Date(2026, 3, 15, 12, i, 0, 0, time.UTC),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
s := Summary(events)
|
||||||
|
recent, _ := s["events"].([]map[string]any)
|
||||||
|
if len(recent) != 10 {
|
||||||
|
t.Fatalf("expected 10 recent events, got %d", len(recent))
|
||||||
|
}
|
||||||
|
if recent[0]["timestamp"].(time.Time).Minute() != 11 {
|
||||||
|
t.Errorf("expected newest event first, got minute %d", recent[0]["timestamp"].(time.Time).Minute())
|
||||||
|
}
|
||||||
|
if recent[9]["timestamp"].(time.Time).Minute() != 2 {
|
||||||
|
t.Errorf("expected tenth newest event last, got minute %d", recent[9]["timestamp"].(time.Time).Minute())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- sortedMap ---
|
// --- sortedMap ---
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
|
||||||
"dappco.re/go/core/ai/ai"
|
"dappco.re/go/core/ai/ai"
|
||||||
"dappco.re/go/core/i18n"
|
"dappco.re/go/core/i18n"
|
||||||
coreerr "dappco.re/go/core/log"
|
coreerr "dappco.re/go/core/log"
|
||||||
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -94,6 +94,23 @@ func runMetrics() error {
|
||||||
cli.Blank()
|
cli.Blank()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recent events
|
||||||
|
if recent, ok := summary["events"].([]map[string]any); ok && len(recent) > 0 {
|
||||||
|
cli.Print("%s\n", cli.DimStyle.Render("Recent events:"))
|
||||||
|
for _, entry := range recent {
|
||||||
|
ts, _ := entry["timestamp"].(time.Time)
|
||||||
|
agent, _ := entry["agent_id"].(string)
|
||||||
|
repo, _ := entry["repo"].(string)
|
||||||
|
cli.Print(" %-20s %-24s %-20s %-20s\n",
|
||||||
|
ts.Format(time.RFC3339),
|
||||||
|
entry["type"],
|
||||||
|
agent,
|
||||||
|
repo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli.Blank()
|
||||||
|
}
|
||||||
|
|
||||||
if len(events) == 0 {
|
if len(events) == 0 {
|
||||||
cli.Text(i18n.T("cmd.ai.metrics.none_found"))
|
cli.Text(i18n.T("cmd.ai.metrics.none_found"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue