Files
Cyrene_v2/cmd/cyrene/main.go
T
AskaEth 025dd1796b
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(observability): 日志聚合后端 — logger环形缓冲 + GET /admin/observability/logs
- internal/logger/ring.go: Entry{time,level,source,msg} + RingBuffer(2000,满覆盖最旧,不阻塞)+ringHandler(slog旁路,Enabled委托内层,sourceOf提取source=再回落Scoped); Options.Ring非nil才包
- gateway/observability_logs.go: GET /admin/observability/logs?level=&limit= (writeAuth owner/admin) → {data:[{time(RFC3339 UTC),level,source,msg}]} 最新在前; level阈值过滤(warn=warn+error,已标注取舍); limit缺省200上限1000; 未知level400/未注入501
- routes.go注册 + observability.go SetLogBuffer(atomic.Pointer) + cmd/cyrene main.go/wireGatewayLogBuffer接线(logger先于gateway)
- 控制台/文件输出零变化; 记录只源自已产出日志,不额外打印敏感
- Docs/api-client v0.35; 测试(ring 10 + observability_logs 8 + cmd端到端2); build/vet/test绿(56)+win/darwin交叉编译
2026-10-03 15:22:03 +08:00

120 lines
3.8 KiB
Go

// Command cyrene is the single Cyrene v2 binary. It assembles every subsystem
// (config, logging, database, bus, personas, model adapter, tool dispatcher,
// memory stack, agent loop, orchestration, execution, IoT, channel sessions and
// adapters) into one process and serves the gateway + embedded frontend on a
// single port (:8080), shutting down gracefully on a termination signal.
//
// Build with `go build -ldflags="-s -w" -trimpath -o cyrene ./cmd/cyrene/`.
// The frontend must be built first so the SPA can be embedded:
// `cd frontend/web && npm run build`.
package main
import (
"context"
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"time"
capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"git.yeij.top/AskaEth/Cyrene_v2/internal/platform"
"git.yeij.top/AskaEth/Cyrene_v2/internal/webui"
)
// envPath is the .env file merged at boot (development only; production uses
// systemd/env_file). A missing .env is not an error.
const envPath = ".env"
// shutdownTimeout bounds how long the graceful teardown may take.
const shutdownTimeout = 15 * time.Second
// externalDistCandidates are the on-disk directories checked (in order) for a
// frontend override before falling back to the embedded dist. The first that
// exists wins. They are relative to the process working directory.
func externalDistCandidates() []string {
return []string{
filepath.Join("frontend", "web", "dist"),
filepath.Join("frontend", "dist"),
}
}
// resolveFrontendFS returns the filesystem serving the SPA: an external dist
// directory when present (deploy-time hot override), otherwise the embedded dist.
func resolveFrontendFS(log *slog.Logger) fs.FS {
for _, dir := range externalDistCandidates() {
if info, err := os.Stat(dir); err == nil && info.IsDir() {
log.Info("serving external frontend dist", "dir", dir)
return os.DirFS(dir)
}
}
log.Info("serving embedded frontend dist")
return webui.Assets()
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "cyrene:", err)
os.Exit(1)
}
}
// run boots the application, starts every service and the gateway, then blocks
// until a termination signal arrives or the gateway fails.
func run() error {
cfg := config.MustLoad(envPath)
format := logger.FormatText
if cfg.Env == "production" {
format = logger.FormatJSON
}
logRing := logger.NewRingBuffer(logger.DefaultRingCapacity)
log := logger.New(logger.Options{Level: cfg.LogLevel, Format: string(format), Ring: logRing})
log = logger.WithSource(log, "main")
ctx, stop := platform.SignalContext()
defer stop()
a, err := newApp(ctx, cfg, log)
if err != nil {
return err
}
// Observability log tail: the logger's ring buffer is built above (before the
// gateway), so hand it to the gateway now that the gateway exists.
wireGatewayLogBuffer(a, logRing)
if err := a.automation.Start(ctx); err != nil {
return fmt.Errorf("start automations: %w", err)
}
a.backthink.Start(ctx)
a.log.Info("cyrene subsystems started",
"personas", a.personas.CountPersonas(),
"tools", len(a.caps.List(capreg.KindTool)),
"channels", len(a.caps.List(capreg.KindChannel)))
a.log.Info("cyrene ready", "data_dir", resolveDataDir(cfg))
errCh, err := a.gw.Start(ctx)
if err != nil {
return fmt.Errorf("start gateway: %w", err)
}
select {
case <-ctx.Done():
a.log.Info("cyrene signal received, shutting down")
case gwErr := <-errCh:
if gwErr != nil {
shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
a.close(shutCtx)
return fmt.Errorf("gateway failed: %w", gwErr)
}
}
shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
a.close(shutCtx)
return nil
}