package functions import ( "fmt" "strconv" "strings" "github.com/spf13/cobra" ) var ( logsLimit int logsWASMOnly bool ) // LogsCmd retrieves function invocation history. // // Default view: invocation history (always populated when the function has // been invoked) — request_id, status, duration, error_message, plus any // WASM-emitted log entries nested per record. // // --wasm-only switches to the legacy view that returns ONLY entries // emitted by the function via log_info / log_error (often empty). var LogsCmd = &cobra.Command{ Use: "logs ", Short: "Get invocation history for a function", Long: `Retrieves the most recent invocations for a deployed function. Each invocation record shows: timestamp, request_id, status, duration_ms, and (if any) the error message. WASM functions that emit log entries via log_info / log_error have those entries nested under each record. Pass --wasm-only to retrieve only the WASM-emitted log lines (legacy behavior; rarely useful on functions that don't call log_info).`, Args: cobra.ExactArgs(1), RunE: runLogs, } func init() { LogsCmd.Flags().IntVar(&logsLimit, "limit", 50, "Maximum number of records to retrieve") LogsCmd.Flags().BoolVar(&logsWASMOnly, "wasm-only", false, "Show only WASM-emitted log entries (legacy view)") } func runLogs(cmd *cobra.Command, args []string) error { name := args[0] endpoint := "/v1/functions/" + name + "/logs" q := []string{} if logsLimit > 0 { q = append(q, "limit="+strconv.Itoa(logsLimit)) } if logsWASMOnly { q = append(q, "wasm_only=1") } if len(q) > 0 { endpoint += "?" + strings.Join(q, "&") } result, err := apiGet(endpoint) if err != nil { return err } if logsWASMOnly { return printWASMLogs(name, result) } return printInvocations(name, result) } // printInvocations renders the default invocation-history view. func printInvocations(name string, result map[string]interface{}) error { invs, ok := result["invocations"].([]interface{}) if !ok || len(invs) == 0 { fmt.Printf("No invocations found for function %q.\n", name) return nil } for _, entry := range invs { inv, ok := entry.(map[string]interface{}) if !ok { continue } started := valStr(inv, "started_at") status := valStr(inv, "status") reqID := valStr(inv, "request_id") duration := valNumberAsString(inv, "duration_ms") errMsg := valStr(inv, "error_message") // Header line per invocation. fmt.Printf("[%s] %s request=%s duration=%sms\n", started, strings.ToUpper(status), reqID, duration) if errMsg != "" { fmt.Printf(" error: %s\n", errMsg) } // Nested WASM logs (if any). if wasmLogs, ok := inv["wasm_logs"].([]interface{}); ok { for _, l := range wasmLogs { le, ok := l.(map[string]interface{}) if !ok { continue } fmt.Printf(" %s [%s] %s\n", valStr(le, "timestamp"), strings.ToUpper(valStr(le, "level")), valStr(le, "message")) } } } fmt.Printf("\nShowing %d invocation(s). Use --wasm-only for the legacy log-line view.\n", len(invs)) return nil } // printWASMLogs renders the legacy WASM-only view. func printWASMLogs(name string, result map[string]interface{}) error { logs, ok := result["logs"].([]interface{}) if !ok || len(logs) == 0 { fmt.Printf("No WASM-emitted logs found for function %q. "+ "Tip: drop --wasm-only to see invocation history.\n", name) return nil } for _, entry := range logs { log, ok := entry.(map[string]interface{}) if !ok { continue } ts := valStr(log, "timestamp") level := valStr(log, "level") msg := valStr(log, "message") fmt.Printf("[%s] %s: %s\n", ts, level, msg) } fmt.Printf("\nShowing %d log(s)\n", len(logs)) return nil } // valNumberAsString formats a JSON number field as a clean integer string. func valNumberAsString(m map[string]interface{}, key string) string { v, ok := m[key] if !ok || v == nil { return "0" } switch n := v.(type) { case float64: return strconv.FormatInt(int64(n), 10) case int: return strconv.Itoa(n) case int64: return strconv.FormatInt(n, 10) case string: return n default: return fmt.Sprintf("%v", n) } }