Port the standalone lab dashboard (lab.lthn.io) into the core CLI as pkg/lab/ with collectors, handlers, and HTML templates. The dashboard monitors machines, Docker containers, Forgejo, HuggingFace models, training runs, and InfluxDB metrics with SSE live updates. New command: core lab serve --bind :8080 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
82 lines
1.5 KiB
Go
82 lines
1.5 KiB
Go
package collector
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Collector interface {
|
|
Name() string
|
|
Collect(ctx context.Context) error
|
|
}
|
|
|
|
type Registry struct {
|
|
mu sync.Mutex
|
|
entries []entry
|
|
logger *slog.Logger
|
|
}
|
|
|
|
type entry struct {
|
|
c Collector
|
|
interval time.Duration
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func NewRegistry(logger *slog.Logger) *Registry {
|
|
return &Registry{logger: logger}
|
|
}
|
|
|
|
func (r *Registry) Register(c Collector, interval time.Duration) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.entries = append(r.entries, entry{c: c, interval: interval})
|
|
}
|
|
|
|
func (r *Registry) Start(ctx context.Context) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
for i := range r.entries {
|
|
e := &r.entries[i]
|
|
cctx, cancel := context.WithCancel(ctx)
|
|
e.cancel = cancel
|
|
go r.run(cctx, e.c, e.interval)
|
|
}
|
|
}
|
|
|
|
func (r *Registry) run(ctx context.Context, c Collector, interval time.Duration) {
|
|
r.logger.Info("collector started", "name", c.Name(), "interval", interval)
|
|
|
|
// Run immediately on start.
|
|
if err := c.Collect(ctx); err != nil {
|
|
r.logger.Warn("collector error", "name", c.Name(), "err", err)
|
|
}
|
|
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
r.logger.Info("collector stopped", "name", c.Name())
|
|
return
|
|
case <-ticker.C:
|
|
if err := c.Collect(ctx); err != nil {
|
|
r.logger.Warn("collector error", "name", c.Name(), "err", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Registry) Stop() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
for _, e := range r.entries {
|
|
if e.cancel != nil {
|
|
e.cancel()
|
|
}
|
|
}
|
|
}
|