feat: modelo Log y CRUD en fn_operations
Tipo Log con niveles debug/info/warn/error, source, entity_id y execution_id opcionales. Migración 003_logs.sql y funciones InsertLog, GetLog, ListLogs con filtros combinables. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE logs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
level TEXT NOT NULL DEFAULT 'info' CHECK(level IN ('debug','info','warn','error')),
|
||||||
|
source TEXT NOT NULL DEFAULT '',
|
||||||
|
entity_id TEXT NOT NULL DEFAULT '',
|
||||||
|
execution_id TEXT NOT NULL DEFAULT '',
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
metadata TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_logs_level ON logs(level);
|
||||||
|
CREATE INDEX idx_logs_source ON logs(source);
|
||||||
|
CREATE INDEX idx_logs_created_at ON logs(created_at);
|
||||||
@@ -145,6 +145,28 @@ type AssertionResult struct {
|
|||||||
EvaluatedAt time.Time `json:"evaluated_at"`
|
EvaluatedAt time.Time `json:"evaluated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogLevel represents the severity of a log entry.
|
||||||
|
type LogLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogDebug LogLevel = "debug"
|
||||||
|
LogInfo LogLevel = "info"
|
||||||
|
LogWarn LogLevel = "warn"
|
||||||
|
LogError LogLevel = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Log is a free-form operational event within a project context.
|
||||||
|
type Log struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Level LogLevel `json:"level"`
|
||||||
|
Source string `json:"source"` // who: agent, pipeline, reactive_loop, function name...
|
||||||
|
EntityID string `json:"entity_id"` // optional context
|
||||||
|
ExecutionID string `json:"execution_id"` // optional context
|
||||||
|
Message string `json:"message"`
|
||||||
|
Metadata map[string]any `json:"metadata"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// TypeSnapshot is an immutable copy of a registry type at point of use.
|
// TypeSnapshot is an immutable copy of a registry type at point of use.
|
||||||
type TypeSnapshot struct {
|
type TypeSnapshot struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|||||||
@@ -814,3 +814,92 @@ func scanAssertionResults(rows *sql.Rows) ([]AssertionResult, error) {
|
|||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Log CRUD ---
|
||||||
|
|
||||||
|
// InsertLog inserts a log entry.
|
||||||
|
func (db *DB) InsertLog(l *Log) error {
|
||||||
|
if l.CreatedAt.IsZero() {
|
||||||
|
l.CreatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.conn.Exec(`
|
||||||
|
INSERT INTO logs (id, level, source, entity_id, execution_id, message, metadata, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
l.ID, string(l.Level), l.Source, l.EntityID, l.ExecutionID,
|
||||||
|
l.Message, marshalJSON(l.Metadata), l.CreatedAt.Format(time.RFC3339),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLog returns a log entry by ID.
|
||||||
|
func (db *DB) GetLog(id string) (*Log, error) {
|
||||||
|
row := db.conn.QueryRow(`
|
||||||
|
SELECT id, level, source, entity_id, execution_id, message, metadata, created_at
|
||||||
|
FROM logs WHERE id = ?`, id)
|
||||||
|
|
||||||
|
var l Log
|
||||||
|
var metadataJSON, createdAt string
|
||||||
|
err := row.Scan(&l.ID, &l.Level, &l.Source, &l.EntityID, &l.ExecutionID,
|
||||||
|
&l.Message, &metadataJSON, &createdAt)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning log: %w", err)
|
||||||
|
}
|
||||||
|
l.Metadata = unmarshalJSON(metadataJSON)
|
||||||
|
l.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||||
|
return &l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListLogs returns logs filtered by level, source, entity, and/or execution.
|
||||||
|
func (db *DB) ListLogs(level LogLevel, source, entityID, executionID string, limit int) ([]Log, error) {
|
||||||
|
where := []string{}
|
||||||
|
args := []any{}
|
||||||
|
if level != "" {
|
||||||
|
where = append(where, "level = ?")
|
||||||
|
args = append(args, string(level))
|
||||||
|
}
|
||||||
|
if source != "" {
|
||||||
|
where = append(where, "source = ?")
|
||||||
|
args = append(args, source)
|
||||||
|
}
|
||||||
|
if entityID != "" {
|
||||||
|
where = append(where, "entity_id = ?")
|
||||||
|
args = append(args, entityID)
|
||||||
|
}
|
||||||
|
if executionID != "" {
|
||||||
|
where = append(where, "execution_id = ?")
|
||||||
|
args = append(args, executionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `SELECT id, level, source, entity_id, execution_id, message, metadata, created_at FROM logs`
|
||||||
|
if len(where) > 0 {
|
||||||
|
q += " WHERE " + strings.Join(where, " AND ")
|
||||||
|
}
|
||||||
|
q += " ORDER BY created_at DESC"
|
||||||
|
if limit > 0 {
|
||||||
|
q += fmt.Sprintf(" LIMIT %d", limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.conn.Query(q, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var result []Log
|
||||||
|
for rows.Next() {
|
||||||
|
var l Log
|
||||||
|
var metadataJSON, createdAt string
|
||||||
|
if err := rows.Scan(&l.ID, &l.Level, &l.Source, &l.EntityID, &l.ExecutionID,
|
||||||
|
&l.Message, &metadataJSON, &createdAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning log: %w", err)
|
||||||
|
}
|
||||||
|
l.Metadata = unmarshalJSON(metadataJSON)
|
||||||
|
l.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||||
|
result = append(result, l)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user