Files
synapbus/internal/trace/tracer.go
T
Algis DumbrisandClaude Opus 4.6 2f55ce87c3 feat: implement core foundation (storage, messaging, agents, MCP server)
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>
2026-03-13 11:45:22 +02:00

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,
)
}
}