From a4df450d411ae695001d407cb6171db7a9f8186c Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 29 Mar 2026 17:29:47 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20findOpsDB=20falla=20con=20error=20en=20v?= =?UTF-8?q?ez=20de=20crear=20operations.db=20en=20la=20ra=C3=ADz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Antes, si no encontraba operations.db subiendo directorios, hacía fallback silencioso a ./operations.db — lo que creaba la BD en la raíz violando la regla de db_locations. Ahora retorna error explícito indicando que se debe ejecutar fn ops init en el directorio correcto. También elimina operations.db espuria de la raíz (2 executions de metabase_registry creadas por el fallback). Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/fn/ops.go | 208 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 202 insertions(+), 6 deletions(-) diff --git a/cmd/fn/ops.go b/cmd/fn/ops.go index 82761aee..45d6a18b 100644 --- a/cmd/fn/ops.go +++ b/cmd/fn/ops.go @@ -37,6 +37,8 @@ func cmdOps(args []string) { cmdOpsExecution(args[1:]) case "assertion": cmdOpsAssertion(args[1:]) + case "log": + cmdOpsLog(args[1:]) case "help", "-h", "--help": printOpsUsage() default: @@ -87,7 +89,16 @@ Assertion commands: fn ops assertion delete Elimina assertion fn ops assertion eval --entity-id Evalua assertions activas fn ops assertion result add Registra resultado manual - fn ops assertion result list [--assertion-id ]`) + fn ops assertion result list [--assertion-id ] + +Log commands: + fn ops log add Registra log entry + fn ops log list [--level ] [--source ] [--limit N] + fn ops log show Muestra log entry + +Log flags: + --id --level --source + --entity-id --execution-id --message --metadata `) } // --- ops init --- @@ -714,12 +725,12 @@ func requireRegistryDB() *registry.DB { // --- helpers --- -func findOpsDB() string { +func findOpsDB() (string, error) { dir, _ := os.Getwd() for { path := filepath.Join(dir, opsDBName) if _, err := os.Stat(path); err == nil { - return path + return path, nil } parent := filepath.Dir(dir) if parent == dir { @@ -727,15 +738,18 @@ func findOpsDB() string { } dir = parent } - return filepath.Join(".", opsDBName) + return "", fmt.Errorf("operations.db not found (searched from %s up to /)\nRun 'fn ops init ' to create one inside an app directory", dir) } func openOpsDB() *ops.DB { - path := findOpsDB() + path, err := findOpsDB() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } db, err := ops.Open(path) if err != nil { fmt.Fprintf(os.Stderr, "error opening operations.db: %v\n", err) - fmt.Fprintln(os.Stderr, "Run 'fn ops init' first to create one.") os.Exit(1) } return db @@ -1398,6 +1412,188 @@ func formatResultValue(v map[string]any) string { return s } +// --- Log subcommands --- + +func cmdOpsLog(args []string) { + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "usage: fn ops log ") + os.Exit(1) + } + switch args[0] { + case "add": + cmdOpsLogAdd(args[1:]) + case "list": + cmdOpsLogList(args[1:]) + case "show": + cmdOpsLogShow(args[1:]) + default: + fmt.Fprintf(os.Stderr, "unknown log command: %s\n", args[0]) + os.Exit(1) + } +} + +func cmdOpsLogAdd(args []string) { + var id, level, source, entityID, executionID, message, metadataStr string + i := 0 + for i < len(args) { + switch args[i] { + case "--id": + i++ + id = args[i] + case "--level": + i++ + level = args[i] + case "--source": + i++ + source = args[i] + case "--entity-id": + i++ + entityID = args[i] + case "--execution-id": + i++ + executionID = args[i] + case "--message", "-m": + i++ + message = args[i] + case "--metadata": + i++ + metadataStr = args[i] + } + i++ + } + + if message == "" { + // Read from stdin if no message flag + b, err := io.ReadAll(os.Stdin) + if err == nil && len(b) > 0 { + message = strings.TrimSpace(string(b)) + } + } + + if message == "" { + fmt.Fprintln(os.Stderr, "error: --message is required (or pipe to stdin)") + os.Exit(1) + } + + if level == "" { + level = "info" + } + if id == "" { + id = fmt.Sprintf("log_%d", timeNow().UnixNano()) + } + + var metadata map[string]any + if metadataStr != "" { + if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil { + fmt.Fprintf(os.Stderr, "error: invalid metadata JSON: %v\n", err) + os.Exit(1) + } + } + + l := &ops.Log{ + ID: id, + Level: ops.LogLevel(level), + Source: source, + EntityID: entityID, + ExecutionID: executionID, + Message: message, + Metadata: metadata, + } + + db := openOpsDB() + defer db.Close() + + if err := db.InsertLog(l); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Logged: [%s] %s\n", l.Level, truncate(l.Message, 60)) +} + +func cmdOpsLogList(args []string) { + var level, source, entityID, executionID string + limit := 50 + i := 0 + for i < len(args) { + switch args[i] { + case "--level": + i++ + level = args[i] + case "--source": + i++ + source = args[i] + case "--entity-id": + i++ + entityID = args[i] + case "--execution-id": + i++ + executionID = args[i] + case "--limit", "-n": + i++ + limit = int(parseInt64(args[i])) + } + i++ + } + + db := openOpsDB() + defer db.Close() + + logs, err := db.ListLogs(ops.LogLevel(level), source, entityID, executionID, limit) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if len(logs) == 0 { + fmt.Println("No logs.") + return + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "CREATED_AT\tLEVEL\tSOURCE\tMESSAGE") + for _, l := range logs { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", l.CreatedAt.Format(time.RFC3339), l.Level, l.Source, truncate(l.Message, 60)) + } + w.Flush() +} + +func cmdOpsLogShow(args []string) { + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "usage: fn ops log show ") + os.Exit(1) + } + + db := openOpsDB() + defer db.Close() + + l, err := db.GetLog(args[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + if l == nil { + fmt.Fprintf(os.Stderr, "log %q not found\n", args[0]) + os.Exit(1) + } + + fmt.Printf("ID: %s\n", l.ID) + fmt.Printf("Level: %s\n", l.Level) + fmt.Printf("Source: %s\n", l.Source) + if l.EntityID != "" { + fmt.Printf("Entity: %s\n", l.EntityID) + } + if l.ExecutionID != "" { + fmt.Printf("Execution: %s\n", l.ExecutionID) + } + fmt.Printf("Message: %s\n", l.Message) + if len(l.Metadata) > 0 { + m, _ := json.MarshalIndent(l.Metadata, " ", " ") + fmt.Printf("Metadata: %s\n", string(m)) + } + fmt.Printf("Created: %s\n", l.CreatedAt.Format(time.RFC3339)) +} + func tryOpenRegistryDB() *registry.DB { // Try FN_REGISTRY_ROOT env var first if envRoot := os.Getenv("FN_REGISTRY_ROOT"); envRoot != "" {