diff --git a/dag/dag.go b/dag/dag.go index ef508ef..6deb2ec 100644 --- a/dag/dag.go +++ b/dag/dag.go @@ -40,6 +40,7 @@ type Node struct { NodeType NodeType isReady bool Timeout time.Duration // ...new field for node-level timeout... + Debug bool // Individual node debug mode } // SetTimeout allows setting a maximum processing duration for the node. @@ -47,6 +48,16 @@ func (n *Node) SetTimeout(d time.Duration) { n.Timeout = d } +// SetDebug enables or disables debug mode for this specific node. +func (n *Node) SetDebug(enabled bool) { + n.Debug = enabled +} + +// IsDebugEnabled checks if debug is enabled for this node or globally. +func (n *Node) IsDebugEnabled(dagDebug bool) bool { + return n.Debug || dagDebug +} + type Edge struct { From *Node FromSource string @@ -100,6 +111,9 @@ type DAG struct { // Circuit breakers per node circuitBreakers map[string]*CircuitBreaker circuitBreakersMu sync.RWMutex + + // Debug configuration + debug bool // Global debug mode for the entire DAG } // SetPreProcessHook configures a function to be called before each node is processed. @@ -112,6 +126,63 @@ func (tm *DAG) SetPostProcessHook(hook func(ctx context.Context, node *Node, tas tm.PostProcessHook = hook } +// SetDebug enables or disables debug mode for the entire DAG. +func (tm *DAG) SetDebug(enabled bool) { + tm.debug = enabled +} + +// IsDebugEnabled returns whether debug mode is enabled for the DAG. +func (tm *DAG) IsDebugEnabled() bool { + return tm.debug +} + +// SetNodeDebug enables or disables debug mode for a specific node. +func (tm *DAG) SetNodeDebug(nodeID string, enabled bool) error { + node, exists := tm.nodes.Get(nodeID) + if !exists { + return fmt.Errorf("node with ID '%s' not found", nodeID) + } + node.SetDebug(enabled) + return nil +} + +// SetAllNodesDebug enables or disables debug mode for all nodes in the DAG. +func (tm *DAG) SetAllNodesDebug(enabled bool) { + tm.nodes.ForEach(func(nodeID string, node *Node) bool { + node.SetDebug(enabled) + return true + }) +} + +// GetDebugInfo returns debug information about the DAG and its nodes. +func (tm *DAG) GetDebugInfo() map[string]interface{} { + debugInfo := map[string]interface{}{ + "dag_name": tm.name, + "dag_key": tm.key, + "dag_debug_enabled": tm.debug, + "start_node": tm.startNode, + "has_page_node": tm.hasPageNode, + "is_paused": tm.paused, + "nodes": make(map[string]map[string]interface{}), + } + + nodesInfo := debugInfo["nodes"].(map[string]map[string]interface{}) + tm.nodes.ForEach(func(nodeID string, node *Node) bool { + nodesInfo[nodeID] = map[string]interface{}{ + "id": node.ID, + "label": node.Label, + "type": node.NodeType.String(), + "debug_enabled": node.Debug, + "has_timeout": node.Timeout > 0, + "timeout": node.Timeout.String(), + "edge_count": len(node.Edges), + } + return true + }) + + return debugInfo +} + func NewDAG(name, key string, finalResultCallback func(taskID string, result mq.Result), opts ...mq.Option) *DAG { callback := func(ctx context.Context, result mq.Result) error { return nil } d := &DAG{ @@ -284,6 +355,15 @@ func (tm *DAG) AddNode(nodeType NodeType, name, nodeID string, handler mq.Proces return tm } +// AddNodeWithDebug adds a node to the DAG with optional debug mode enabled +func (tm *DAG) AddNodeWithDebug(nodeType NodeType, name, nodeID string, handler mq.Processor, debug bool, startNode ...bool) *DAG { + dag := tm.AddNode(nodeType, name, nodeID, handler, startNode...) + if dag.Error == nil { + dag.SetNodeDebug(nodeID, debug) + } + return dag +} + func (tm *DAG) AddDeferredNode(nodeType NodeType, name, key string, firstNode ...bool) error { if tm.server.SyncMode() { return fmt.Errorf("DAG cannot have deferred node in Sync Mode") @@ -361,6 +441,11 @@ func (tm *DAG) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { // Enhanced processing with monitoring and rate limiting startTime := time.Now() + // Debug logging at DAG level + if tm.IsDebugEnabled() { + tm.debugDAGTaskStart(ctx, task, startTime) + } + // Record task start in monitoring if tm.monitor != nil { tm.monitor.metrics.RecordTaskStart(task.ID) @@ -402,6 +487,11 @@ func (tm *DAG) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { // Update internal metrics tm.updateTaskMetrics(task.ID, result, duration) + // Debug logging at DAG level for task completion + if tm.IsDebugEnabled() { + tm.debugDAGTaskComplete(ctx, task, result, duration, startTime) + } + // Trigger webhooks if configured if tm.webhookManager != nil { event := WebhookEvent{ @@ -1608,3 +1698,88 @@ func (h *ActivityAlertHandler) HandleAlert(alert Alert) error { } return nil } + +// debugDAGTaskStart logs debug information when a task starts at DAG level +func (tm *DAG) debugDAGTaskStart(ctx context.Context, task *mq.Task, startTime time.Time) { + var payload map[string]any + if err := json.Unmarshal(task.Payload, &payload); err != nil { + payload = map[string]any{"raw_payload": string(task.Payload)} + } + tm.Logger().Info("🚀 [DEBUG] DAG task processing started", + logger.Field{Key: "dag_name", Value: tm.name}, + logger.Field{Key: "dag_key", Value: tm.key}, + logger.Field{Key: "task_id", Value: task.ID}, + logger.Field{Key: "task_topic", Value: task.Topic}, + logger.Field{Key: "timestamp", Value: startTime.Format(time.RFC3339)}, + logger.Field{Key: "start_node", Value: tm.startNode}, + logger.Field{Key: "has_page_node", Value: tm.hasPageNode}, + logger.Field{Key: "is_paused", Value: tm.paused}, + logger.Field{Key: "payload_size", Value: len(task.Payload)}, + logger.Field{Key: "payload_preview", Value: tm.getDAGPayloadPreview(payload)}, + logger.Field{Key: "debug_enabled", Value: tm.debug}, + ) +} + +// debugDAGTaskComplete logs debug information when a task completes at DAG level +func (tm *DAG) debugDAGTaskComplete(ctx context.Context, task *mq.Task, result mq.Result, duration time.Duration, startTime time.Time) { + var resultPayload map[string]any + if len(result.Payload) > 0 { + if err := json.Unmarshal(result.Payload, &resultPayload); err != nil { + resultPayload = map[string]any{"raw_payload": string(result.Payload)} + } + } + + tm.Logger().Info("🏁 [DEBUG] DAG task processing completed", + logger.Field{Key: "dag_name", Value: tm.name}, + logger.Field{Key: "dag_key", Value: tm.key}, + logger.Field{Key: "task_id", Value: task.ID}, + logger.Field{Key: "task_topic", Value: task.Topic}, + logger.Field{Key: "result_topic", Value: result.Topic}, + logger.Field{Key: "timestamp", Value: time.Now().Format(time.RFC3339)}, + logger.Field{Key: "total_duration", Value: duration.String()}, + logger.Field{Key: "status", Value: string(result.Status)}, + logger.Field{Key: "has_error", Value: result.Error != nil}, + logger.Field{Key: "error_message", Value: tm.getDAGErrorMessage(result.Error)}, + logger.Field{Key: "result_size", Value: len(result.Payload)}, + logger.Field{Key: "result_preview", Value: tm.getDAGPayloadPreview(resultPayload)}, + logger.Field{Key: "is_last", Value: result.Last}, + logger.Field{Key: "metrics", Value: tm.GetTaskMetrics()}, + ) +} + +// getDAGPayloadPreview returns a truncated version of the payload for debug logging +func (tm *DAG) getDAGPayloadPreview(payload map[string]any) string { + if payload == nil { + return "null" + } + + preview := make(map[string]any) + count := 0 + maxFields := 3 // Limit to first 3 fields for DAG level logging + + for key, value := range payload { + if count >= maxFields { + preview["..."] = fmt.Sprintf("and %d more fields", len(payload)-maxFields) + break + } + + // Truncate string values if they're too long + if strVal, ok := value.(string); ok && len(strVal) > 50 { + preview[key] = strVal[:47] + "..." + } else { + preview[key] = value + } + count++ + } + + previewBytes, _ := json.Marshal(preview) + return string(previewBytes) +} + +// getDAGErrorMessage safely extracts error message +func (tm *DAG) getDAGErrorMessage(err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/dag/task_manager.go b/dag/task_manager.go index a478b03..88d9c44 100644 --- a/dag/task_manager.go +++ b/dag/task_manager.go @@ -209,6 +209,12 @@ func (tm *TaskManager) processNode(exec *task) { if tm.dag.PreProcessHook != nil { exec.ctx = tm.dag.PreProcessHook(exec.ctx, node, exec.taskID, exec.payload) } + + // Debug logging before processing + if node.IsDebugEnabled(tm.dag.IsDebugEnabled()) { + tm.debugNodeStart(exec, node) + } + state, _ := tm.taskStates.Get(exec.nodeID) if state == nil { tm.dag.Logger().Warn("State not found; creating new state", logger.Field{Key: "nodeID", Value: exec.nodeID}) @@ -260,6 +266,11 @@ func (tm *TaskManager) processNode(exec *task) { tm.dag.PostProcessHook(exec.ctx, node, exec.taskID, result) } + // Debug logging after processing + if node.IsDebugEnabled(tm.dag.IsDebugEnabled()) { + tm.debugNodeComplete(exec, node, result, nodeLatency, attempts) + } + if result.Error != nil { result.Status = mq.Failed state.Status = mq.Failed @@ -614,3 +625,94 @@ func (tm *TaskManager) Stop() { tm.currentNodePayload.Clear() tm.currentNodeResult.Clear() } + +// debugNodeStart logs debug information when a node starts processing +func (tm *TaskManager) debugNodeStart(exec *task, node *Node) { + var payload map[string]any + if err := json.Unmarshal(exec.payload, &payload); err != nil { + payload = map[string]any{"raw_payload": string(exec.payload)} + } + + tm.dag.Logger().Info("🐛 [DEBUG] Node processing started", + logger.Field{Key: "dag_name", Value: tm.dag.name}, + logger.Field{Key: "task_id", Value: exec.taskID}, + logger.Field{Key: "node_id", Value: node.ID}, + logger.Field{Key: "node_type", Value: node.NodeType.String()}, + logger.Field{Key: "node_label", Value: node.Label}, + logger.Field{Key: "timestamp", Value: time.Now().Format(time.RFC3339)}, + logger.Field{Key: "has_timeout", Value: node.Timeout > 0}, + logger.Field{Key: "timeout_duration", Value: node.Timeout.String()}, + logger.Field{Key: "payload_size", Value: len(exec.payload)}, + logger.Field{Key: "payload_preview", Value: tm.getPayloadPreview(payload)}, + logger.Field{Key: "debug_mode", Value: "individual_node:" + fmt.Sprintf("%t", node.Debug) + ", dag_global:" + fmt.Sprintf("%t", tm.dag.IsDebugEnabled())}, + ) + + // Log processor type if it implements the Debugger interface + if debugger, ok := node.processor.(Debugger); ok { + debugger.Debug(exec.ctx, mq.NewTask(exec.taskID, exec.payload, exec.nodeID, mq.WithDAG(tm.dag))) + } +} + +// debugNodeComplete logs debug information when a node completes processing +func (tm *TaskManager) debugNodeComplete(exec *task, node *Node, result mq.Result, latency time.Duration, attempts int) { + var resultPayload map[string]any + if len(result.Payload) > 0 { + if err := json.Unmarshal(result.Payload, &resultPayload); err != nil { + resultPayload = map[string]any{"raw_payload": string(result.Payload)} + } + } + + tm.dag.Logger().Info("🐛 [DEBUG] Node processing completed", + logger.Field{Key: "dag_name", Value: tm.dag.name}, + logger.Field{Key: "task_id", Value: exec.taskID}, + logger.Field{Key: "node_id", Value: node.ID}, + logger.Field{Key: "node_type", Value: node.NodeType.String()}, + logger.Field{Key: "node_label", Value: node.Label}, + logger.Field{Key: "timestamp", Value: time.Now().Format(time.RFC3339)}, + logger.Field{Key: "status", Value: string(result.Status)}, + logger.Field{Key: "latency", Value: latency.String()}, + logger.Field{Key: "attempts", Value: attempts + 1}, + logger.Field{Key: "has_error", Value: result.Error != nil}, + logger.Field{Key: "error_message", Value: tm.getErrorMessage(result.Error)}, + logger.Field{Key: "result_size", Value: len(result.Payload)}, + logger.Field{Key: "result_preview", Value: tm.getPayloadPreview(resultPayload)}, + logger.Field{Key: "is_last_node", Value: result.Last}, + ) +} + +// getPayloadPreview returns a truncated version of the payload for debug logging +func (tm *TaskManager) getPayloadPreview(payload map[string]any) string { + if payload == nil { + return "null" + } + + preview := make(map[string]any) + count := 0 + maxFields := 5 // Limit to first 5 fields to avoid log spam + + for key, value := range payload { + if count >= maxFields { + preview["..."] = fmt.Sprintf("and %d more fields", len(payload)-maxFields) + break + } + + // Truncate string values if they're too long + if strVal, ok := value.(string); ok && len(strVal) > 100 { + preview[key] = strVal[:97] + "..." + } else { + preview[key] = value + } + count++ + } + + previewBytes, _ := json.Marshal(preview) + return string(previewBytes) +} + +// getErrorMessage safely extracts error message +func (tm *TaskManager) getErrorMessage(err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/examples/dag.go b/examples/dag.go index fa8fb19..cee5e03 100644 --- a/examples/dag.go +++ b/examples/dag.go @@ -45,7 +45,6 @@ func main() { panic(flow.Error) } - fmt.Println(flow.ExportDOT()) rs := flow.Process(context.Background(), data) if rs.Error != nil { panic(rs.Error) diff --git a/services/examples/app/data/login_output.json b/services/examples/app/data/login_output.json new file mode 100644 index 0000000..d362bf9 --- /dev/null +++ b/services/examples/app/data/login_output.json @@ -0,0 +1,3 @@ +{"html_content":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\n\u003chead\u003e\n \u003ctitle\u003eBasic Template\u003c/title\u003e\n \u003cscript src=\"https://cdn.tailwindcss.com\"\u003e\u003c/script\u003e\n \u003clink rel=\"stylesheet\" href=\"form.css\"\u003e\n \u003cstyle\u003e\n .required {\n color: #dc3545;\n }\n\n .group-header {\n font-weight: bold;\n margin-top: 0.5rem;\n margin-bottom: 0.5rem;\n }\n\n .section-title {\n color: #0d6efd;\n border-bottom: 2px solid #0d6efd;\n padding-bottom: 0.5rem;\n }\n\n .form-group-fields\u003ediv {\n margin-bottom: 1rem;\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\n\u003cbody class=\"bg-gray-100\"\u003e\n \u003cform class=\"form-horizontal\" action=\"/process?task_id=345053712301170690\u0026next=true\" method=\"POST\" enctype=\"application/x-www-form-urlencoded\"\u003e\n \u003cdiv class=\"form-container p-4 bg-white shadow-md rounded\"\u003e\n \n\u003cdiv class=\"form-group-container\"\u003e\n\t\n\t\u003cdiv class=\"form-group-fields\"\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"username\"\u003eUsername or Email \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"username\" id=\"username\" type=\"text\" required=\"required\" title=\"Username or Email\" placeholder=\"Enter your username or email\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"username\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validateusername(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validateusername(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('username_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'username_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validateusername(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('username_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"password\"\u003ePassword \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"password\" id=\"password\" type=\"password\" required=\"required\" title=\"Password\" placeholder=\"Enter your password\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"password\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validatepassword(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validatepassword(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('password_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'password_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validatepassword(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('password_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-check\"\u003e\u003clabel for=\"remember_me\"\u003eRemember Me\u003c/label\u003e\u003cinput name=\"remember_me\" id=\"remember_me\" type=\"checkbox\" title=\"Remember Me\" placeholder=\"Enter remember me\" class=\"form-check\" /\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n \u003cdiv class=\"mt-4 flex gap-2\"\u003e\n \u003cbutton type=\"submit\" class=\"btn btn-primary\"\u003eLog In\u003c/button\u003e\u003cbutton type=\"reset\" class=\"btn btn-secondary\"\u003eClear\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/form\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n","login_message":"Login successful!","next":"true","password":"password","step":"form","task_id":"345053712301170690","username":"admin"} +{"html_content":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\n\u003chead\u003e\n \u003ctitle\u003eBasic Template\u003c/title\u003e\n \u003cscript src=\"https://cdn.tailwindcss.com\"\u003e\u003c/script\u003e\n \u003clink rel=\"stylesheet\" href=\"form.css\"\u003e\n \u003cstyle\u003e\n .required {\n color: #dc3545;\n }\n\n .group-header {\n font-weight: bold;\n margin-top: 0.5rem;\n margin-bottom: 0.5rem;\n }\n\n .section-title {\n color: #0d6efd;\n border-bottom: 2px solid #0d6efd;\n padding-bottom: 0.5rem;\n }\n\n .form-group-fields\u003ediv {\n margin-bottom: 1rem;\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\n\u003cbody class=\"bg-gray-100\"\u003e\n \u003cform class=\"form-horizontal\" action=\"/process?task_id=345054073104531457\u0026next=true\" method=\"POST\" enctype=\"application/x-www-form-urlencoded\"\u003e\n \u003cdiv class=\"form-container p-4 bg-white shadow-md rounded\"\u003e\n \n\u003cdiv class=\"form-group-container\"\u003e\n\t\n\t\u003cdiv class=\"form-group-fields\"\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"username\"\u003eUsername or Email \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"username\" id=\"username\" type=\"text\" required=\"required\" title=\"Username or Email\" placeholder=\"Enter your username or email\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"username\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validateusername(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validateusername(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('username_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'username_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validateusername(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('username_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"password\"\u003ePassword \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"password\" id=\"password\" type=\"password\" required=\"required\" title=\"Password\" placeholder=\"Enter your password\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"password\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validatepassword(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validatepassword(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('password_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'password_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validatepassword(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('password_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-check\"\u003e\u003clabel for=\"remember_me\"\u003eRemember Me\u003c/label\u003e\u003cinput name=\"remember_me\" id=\"remember_me\" type=\"checkbox\" title=\"Remember Me\" placeholder=\"Enter remember me\" class=\"form-check\" /\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n \u003cdiv class=\"mt-4 flex gap-2\"\u003e\n \u003cbutton type=\"submit\" class=\"btn btn-primary\"\u003eLog In\u003c/button\u003e\u003cbutton type=\"reset\" class=\"btn btn-secondary\"\u003eClear\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/form\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n","login_message":"Login successful!","next":"true","password":"password","step":"form","task_id":"345054073104531457","username":"admin"} +{"html_content":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\n\u003chead\u003e\n \u003ctitle\u003eBasic Template\u003c/title\u003e\n \u003cscript src=\"https://cdn.tailwindcss.com\"\u003e\u003c/script\u003e\n \u003clink rel=\"stylesheet\" href=\"form.css\"\u003e\n \u003cstyle\u003e\n .required {\n color: #dc3545;\n }\n\n .group-header {\n font-weight: bold;\n margin-top: 0.5rem;\n margin-bottom: 0.5rem;\n }\n\n .section-title {\n color: #0d6efd;\n border-bottom: 2px solid #0d6efd;\n padding-bottom: 0.5rem;\n }\n\n .form-group-fields\u003ediv {\n margin-bottom: 1rem;\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\n\u003cbody class=\"bg-gray-100\"\u003e\n \u003cform class=\"form-horizontal\" action=\"/process?task_id=345055067016990721\u0026next=true\" method=\"POST\" enctype=\"application/x-www-form-urlencoded\"\u003e\n \u003cdiv class=\"form-container p-4 bg-white shadow-md rounded\"\u003e\n \n\u003cdiv class=\"form-group-container\"\u003e\n\t\n\t\u003cdiv class=\"form-group-fields\"\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"username\"\u003eUsername or Email \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"username\" id=\"username\" type=\"text\" required=\"required\" title=\"Username or Email\" placeholder=\"Enter your username or email\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"username\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validateusername(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validateusername(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('username_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'username_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validateusername(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('username_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"password\"\u003ePassword \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"password\" id=\"password\" type=\"password\" required=\"required\" title=\"Password\" placeholder=\"Enter your password\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"password\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validatepassword(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validatepassword(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('password_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'password_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validatepassword(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('password_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-check\"\u003e\u003clabel for=\"remember_me\"\u003eRemember Me\u003c/label\u003e\u003cinput name=\"remember_me\" id=\"remember_me\" type=\"checkbox\" title=\"Remember Me\" placeholder=\"Enter remember me\" class=\"form-check\" /\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n \u003cdiv class=\"mt-4 flex gap-2\"\u003e\n \u003cbutton type=\"submit\" class=\"btn btn-primary\"\u003eLog In\u003c/button\u003e\u003cbutton type=\"reset\" class=\"btn btn-secondary\"\u003eClear\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/form\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n","login_message":"Login successful!","next":"true","password":"password","step":"form","task_id":"345055067016990721","username":"admin"} diff --git a/services/examples/json/login.json b/services/examples/json/login.json index 522e2f1..2b4ea35 100644 --- a/services/examples/json/login.json +++ b/services/examples/json/login.json @@ -1,7 +1,6 @@ { "name": "Login Flow", "key": "login:flow", - "disable_log": true, "nodes": [ { "id": "LoginForm", diff --git a/services/examples/json/send-email.json b/services/examples/json/send-email.json index c2bed7c..272ae01 100644 --- a/services/examples/json/send-email.json +++ b/services/examples/json/send-email.json @@ -1,7 +1,6 @@ { "name": "Email Notification System", "key": "email:notification", - "disable_log": true, "nodes": [ { "id": "Login", diff --git a/services/setup.go b/services/setup.go index deb5f50..1fc8ca7 100644 --- a/services/setup.go +++ b/services/setup.go @@ -55,6 +55,7 @@ func SetupHandler(handler Handler, brokerAddr string, async ...bool) *dag.DAG { opts = append(opts, mq.WithLogger(nil)) } flow := dag.NewDAG(handler.Name, handler.Key, nil, opts...) + flow.SetDebug(handler.Debug) for _, node := range handler.Nodes { if node.Node == "" && node.NodeKey == "" { flow.Error = errors.New("Node not defined " + node.ID) @@ -182,7 +183,7 @@ func prepareNode(flow *dag.DAG, node Node) error { if node.Name == "" { node.Name = node.ID } - flow.AddNode(nodeType, node.Name, node.ID, nodeHandler, node.FirstNode) + flow.AddNodeWithDebug(nodeType, node.Name, node.ID, nodeHandler, node.Debug, node.FirstNode) return nil } diff --git a/services/user_config.go b/services/user_config.go index 9548dd1..0445990 100644 --- a/services/user_config.go +++ b/services/user_config.go @@ -139,6 +139,7 @@ type Node struct { NodeKey string `json:"node_key" yaml:"node_key"` Node string `json:"node" yaml:"node"` Data Data `json:"data" yaml:"data"` + Debug bool `json:"debug" yaml:"debug"` FirstNode bool `json:"first_node" yaml:"first_node"` } @@ -163,6 +164,7 @@ type Handler struct { Name string `json:"name" yaml:"name"` Key string `json:"key" yaml:"key"` DisableLog bool `json:"disable_log" yaml:"disable_log"` + Debug bool `json:"debug" yaml:"debug"` Nodes []Node `json:"nodes,omitempty" yaml:"nodes,omitempty"` Edges []Edge `json:"edges,omitempty" yaml:"edges,omitempty"` Branches []Branch `json:"branches,omitempty" yaml:"branches,omitempty"`