feat: complete agent monitoring - hook, UI, and backend filter
- Add event_type and framework filters to events query endpoint - Add /agents SPA route to web-ui server - Add Agents nav link and route in frontend - Add agents page CSS (timeline, VM pills, stats panel) - Build VM status strip, activity timeline, and real-time stats - Add agentmon hook for OpenClaw (HOOK.md + handler.ts) - Add docker-compose, Dockerfile, and supporting infra files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+86
-1
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"agentmon/internal/httpx"
|
||||
@@ -13,11 +14,80 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
var (
|
||||
wsUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
wsClients = make(map[*websocket.Conn]bool)
|
||||
wsMu sync.RWMutex
|
||||
natsConn *nats.Conn
|
||||
)
|
||||
|
||||
func subscribeToNATS(nc *nats.Conn) {
|
||||
topic := envDefault("NATS_TOPIC", "agentmon.events.v1")
|
||||
sub, err := nc.Subscribe(topic, func(msg *nats.Msg) {
|
||||
wsMu.RLock()
|
||||
var stale []*websocket.Conn
|
||||
for conn := range wsClients {
|
||||
err := conn.WriteMessage(websocket.TextMessage, msg.Data)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
stale = append(stale, conn)
|
||||
}
|
||||
}
|
||||
wsMu.RUnlock()
|
||||
|
||||
if len(stale) > 0 {
|
||||
wsMu.Lock()
|
||||
for _, conn := range stale {
|
||||
delete(wsClients, conn)
|
||||
}
|
||||
wsMu.Unlock()
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("failed to subscribe to NATS: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("subscribed to NATS topic: %s", topic)
|
||||
_ = sub
|
||||
}
|
||||
|
||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
wsMu.Lock()
|
||||
wsClients[conn] = true
|
||||
wsMu.Unlock()
|
||||
|
||||
log.Printf("WebSocket client connected")
|
||||
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
wsMu.Lock()
|
||||
delete(wsClients, conn)
|
||||
wsMu.Unlock()
|
||||
log.Printf("WebSocket client disconnected")
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := envDefault("AGENTMON_QUERY_ADDR", ":8081")
|
||||
dsn := os.Getenv("DATABASE_URL")
|
||||
natsURL := envDefault("NATS_URL", "nats://localhost:4222")
|
||||
|
||||
if dsn == "" {
|
||||
log.Fatalf("DATABASE_URL is required")
|
||||
}
|
||||
@@ -28,6 +98,14 @@ func main() {
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
nc, err := nats.Connect(natsURL)
|
||||
if err != nil {
|
||||
log.Printf("warning: failed to connect to NATS: %v", err)
|
||||
} else {
|
||||
natsConn = nc
|
||||
go subscribeToNATS(nc)
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
@@ -39,9 +117,16 @@ func main() {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
r.Get("/v1/ws", wsHandler)
|
||||
|
||||
r.Get("/v1/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
events, err := db.ListRecentEvents(r.Context(), limit)
|
||||
f := postgres.EventsFilter{
|
||||
Limit: limit,
|
||||
EventType: r.URL.Query().Get("event_type"),
|
||||
Framework: r.URL.Query().Get("framework"),
|
||||
}
|
||||
events, err := db.ListRecentEvents(r.Context(), f)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(w, http.StatusInternalServerError, map[string]any{"error": "db_error"})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user