Implements the foundational layer that all SynapBus features depend on: - internal/storage: SQLite connection manager (WAL mode, busy_timeout, foreign_keys) and embedded migration runner using modernc.org/sqlite - internal/messaging: MessagingService with send, read inbox, claim, mark done/failed, and FTS5 search. SQLite-backed MessageStore with conversation auto-creation and read/unread tracking via inbox_state. - internal/agents: AgentService with register, authenticate (bcrypt), update, deregister, discover by capability. HTTP auth middleware. - internal/mcp: MCP server using mark3labs/mcp-go with 9 registered tools (send_message, read_inbox, claim_messages, mark_done, search_messages, register_agent, discover_agents, update_agent, deregister_agent). SSE transport, health endpoint, connection manager. - internal/trace: Async trace recorder with buffered channel for recording agent actions to SQLite traces table. - cmd/synapbus: Updated main.go wiring storage, migrations, services, MCP server, chi router, and graceful shutdown. All code compiles with CGO_ENABLED=0. Full test suite passes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
2.6 KiB
Go
126 lines
2.6 KiB
Go
// Package trace provides agent activity trace recording for SynapBus.
|
|
package trace
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"log/slog"
|
|
)
|
|
|
|
// TraceEntry represents a single trace record.
|
|
type TraceEntry struct {
|
|
AgentName string
|
|
Action string
|
|
Details any
|
|
Error string
|
|
}
|
|
|
|
// Tracer records agent actions to the traces table.
|
|
// It uses a buffered channel for async recording.
|
|
type Tracer struct {
|
|
db *sql.DB
|
|
logger *slog.Logger
|
|
ch chan TraceEntry
|
|
done chan struct{}
|
|
}
|
|
|
|
// NewTracer creates a new Tracer with a buffered channel.
|
|
func NewTracer(db *sql.DB) *Tracer {
|
|
t := &Tracer{
|
|
db: db,
|
|
logger: slog.Default().With("component", "tracer"),
|
|
ch: make(chan TraceEntry, 256),
|
|
done: make(chan struct{}),
|
|
}
|
|
go t.processLoop()
|
|
return t
|
|
}
|
|
|
|
// Record enqueues a trace entry for async storage.
|
|
func (t *Tracer) Record(ctx context.Context, agentName, action string, details any) {
|
|
entry := TraceEntry{
|
|
AgentName: agentName,
|
|
Action: action,
|
|
Details: details,
|
|
}
|
|
|
|
select {
|
|
case t.ch <- entry:
|
|
default:
|
|
t.logger.Warn("trace channel full, dropping entry",
|
|
"agent", agentName,
|
|
"action", action,
|
|
)
|
|
}
|
|
|
|
t.logger.Info("trace recorded",
|
|
"agent", agentName,
|
|
"action", action,
|
|
)
|
|
}
|
|
|
|
// RecordError enqueues a trace entry with an error.
|
|
func (t *Tracer) RecordError(ctx context.Context, agentName, action string, details any, traceErr error) {
|
|
entry := TraceEntry{
|
|
AgentName: agentName,
|
|
Action: action,
|
|
Details: details,
|
|
}
|
|
if traceErr != nil {
|
|
entry.Error = traceErr.Error()
|
|
}
|
|
|
|
select {
|
|
case t.ch <- entry:
|
|
default:
|
|
t.logger.Warn("trace channel full, dropping entry",
|
|
"agent", agentName,
|
|
"action", action,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Close stops the tracer and flushes remaining entries.
|
|
func (t *Tracer) Close() {
|
|
close(t.ch)
|
|
<-t.done
|
|
}
|
|
|
|
func (t *Tracer) processLoop() {
|
|
defer close(t.done)
|
|
for entry := range t.ch {
|
|
t.writeEntry(entry)
|
|
}
|
|
}
|
|
|
|
func (t *Tracer) writeEntry(entry TraceEntry) {
|
|
detailsJSON, err := json.Marshal(entry.Details)
|
|
if err != nil {
|
|
t.logger.Error("failed to marshal trace details",
|
|
"error", err,
|
|
"agent", entry.AgentName,
|
|
"action", entry.Action,
|
|
)
|
|
detailsJSON = []byte("{}")
|
|
}
|
|
|
|
var traceErr sql.NullString
|
|
if entry.Error != "" {
|
|
traceErr = sql.NullString{String: entry.Error, Valid: true}
|
|
}
|
|
|
|
_, err = t.db.Exec(
|
|
`INSERT INTO traces (agent_name, action, details, error, created_at)
|
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
entry.AgentName, entry.Action, string(detailsJSON), traceErr,
|
|
)
|
|
if err != nil {
|
|
t.logger.Error("failed to write trace entry",
|
|
"error", err,
|
|
"agent", entry.AgentName,
|
|
"action", entry.Action,
|
|
)
|
|
}
|
|
}
|