Enhance trace logging and error handling across components

- Integrated trace management into the Assistant's Stream method, allowing for detailed logging of processing steps and errors.
- Improved error handling in the OpenAI stream and post methods, adding trace logs for retries and failures.
- Enhanced CUI and OpenAI writers to log message adaptation and sending errors, improving debugging capabilities.
- Updated trace manager to use milliseconds for timestamps, ensuring consistency across the trace system.
- Refactored log methods in the trace node to streamline logging and broadcasting of events.
This commit is contained in:
Max 2025-11-20 10:45:26 +08:00
parent eff70ab07d
commit 465e45828b
15 changed files with 387 additions and 133 deletions

View file

@ -7,6 +7,7 @@ import (
"github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm" "github.com/yaoapp/yao/agent/llm"
"github.com/yaoapp/yao/agent/output" "github.com/yaoapp/yao/agent/output"
"github.com/yaoapp/yao/trace/types"
"github.com/yaoapp/yao/utils/jsonschema" "github.com/yaoapp/yao/utils/jsonschema"
) )
@ -29,20 +30,51 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
_ = traceID // traceID is available for trace logging _ = traceID // traceID is available for trace logging
// Trace Add
trace, _ := ctx.Trace()
var agentNode types.Node = nil
if trace != nil {
agentNode, _ = trace.Add(inputMessages, types.TraceNodeOption{
Label: fmt.Sprintf("Assistant %s", ast.Name),
Icon: "assistant",
Description: fmt.Sprintf("Assistant %s is processing the request", ast.Name),
})
if agentNode != nil {
// Mark the node as complete when the function returns
defer agentNode.Complete()
}
}
// Full input messages with chat history // Full input messages with chat history
fullMessages, err := ast.WithHistory(ctx, inputMessages) fullMessages, err := ast.WithHistory(ctx, inputMessages)
if err != nil { if err != nil {
if agentNode != nil {
agentNode.Fail(err)
}
return nil, err return nil, err
} }
// Log the chat history
if agentNode != nil {
agentNode.Info("Get Chat History", map[string]any{"messages": fullMessages})
}
// Request Create hook ( Optional ) // Request Create hook ( Optional )
var createResponse *context.HookCreateResponse var createResponse *context.HookCreateResponse
if ast.Script != nil { if ast.Script != nil {
var err error var err error
createResponse, err = ast.Script.Create(ctx, fullMessages) createResponse, err = ast.Script.Create(ctx, fullMessages)
if err != nil { if err != nil {
if agentNode != nil {
agentNode.Fail(err)
}
return nil, err return nil, err
} }
// Log the create response
if agentNode != nil {
agentNode.Debug("Call Create Hook", map[string]any{"response": createResponse})
}
} }
var completionOptions *context.CompletionOptions // default is nil var completionOptions *context.CompletionOptions // default is nil
@ -54,12 +86,18 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
// Build the LLM request first // Build the LLM request first
completionMessages, completionOptions, err = ast.BuildRequest(ctx, inputMessages, createResponse) completionMessages, completionOptions, err = ast.BuildRequest(ctx, inputMessages, createResponse)
if err != nil { if err != nil {
if agentNode != nil {
agentNode.Fail(err)
}
return nil, err return nil, err
} }
// Get connector object and capabilities // Get connector object and capabilities
conn, capabilities, err := ast.GetConnector(ctx) conn, capabilities, err := ast.GetConnector(ctx)
if err != nil { if err != nil {
if agentNode != nil {
agentNode.Fail(err)
}
return nil, err return nil, err
} }
@ -68,6 +106,22 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
completionOptions.Capabilities = capabilities completionOptions.Capabilities = capabilities
} }
// Log the capabilities
if agentNode != nil {
agentNode.Debug("Get Connector Capabilities", map[string]any{"capabilities": capabilities})
}
// Trace Add
if trace != nil {
trace.Add(
map[string]any{"messages": completionMessages, "options": completionOptions},
types.TraceNodeOption{
Label: fmt.Sprintf("LLM %s", conn.ID()), Icon: "llm", Description: fmt.Sprintf("LLM %s is processing the request", conn.ID()),
},
)
}
// Create LLM instance with connector and options // Create LLM instance with connector and options
llmInstance, err := llm.New(conn, completionOptions) llmInstance, err := llm.New(conn, completionOptions)
if err != nil { if err != nil {
@ -86,6 +140,10 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
return nil, err return nil, err
} }
// Mark LLM Request Complete
if trace != nil {
trace.Complete(completionResponse)
}
} }
// Request MCP hook ( Optional ) // Request MCP hook ( Optional )

View file

@ -1,8 +1,6 @@
package handlers package handlers
import ( import (
"fmt"
"github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/output" "github.com/yaoapp/yao/agent/output"
"github.com/yaoapp/yao/agent/output/message" "github.com/yaoapp/yao/agent/output/message"
@ -20,10 +18,11 @@ func DefaultStreamHandler(ctx *context.Context) context.StreamFunc {
} }
return func(chunkType context.StreamChunkType, data []byte) int { return func(chunkType context.StreamChunkType, data []byte) int {
fmt.Println("-----------------------------------------------") trace, _ := ctx.Trace()
fmt.Println("Chunk Type: ", string(chunkType)) if trace != nil {
fmt.Println("Data: ", string(data)) trace.Info("LLM Stream", map[string]any{"data": string(data)})
fmt.Println("-----------------------------------------------") }
// Handle different chunk types // Handle different chunk types
switch chunkType { switch chunkType {
case context.ChunkStreamStart: case context.ChunkStreamStart:

View file

@ -9,7 +9,6 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/connector" "github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/http" "github.com/yaoapp/gou/http"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm/adapters" "github.com/yaoapp/yao/agent/llm/adapters"
"github.com/yaoapp/yao/agent/llm/providers/base" "github.com/yaoapp/yao/agent/llm/providers/base"
@ -186,6 +185,14 @@ func detectReasoningFormat(cap *context.ModelCapabilities) adapters.ReasoningFor
// Stream stream completion from OpenAI API // Stream stream completion from OpenAI API
func (p *Provider) Stream(ctx *context.Context, messages []context.Message, options *context.CompletionOptions, handler context.StreamFunc) (*context.CompletionResponse, error) { func (p *Provider) Stream(ctx *context.Context, messages []context.Message, options *context.CompletionOptions, handler context.StreamFunc) (*context.CompletionResponse, error) {
// Add debug log
trace, _ := ctx.Trace()
if trace != nil {
trace.Debug("OpenAI Stream: Starting stream request", map[string]any{
"message_count": len(messages),
})
}
maxRetries := 3 maxRetries := 3
maxValidationRetries := 3 maxValidationRetries := 3
var lastErr error var lastErr error
@ -219,7 +226,16 @@ func (p *Provider) Stream(ctx *context.Context, messages []context.Message, opti
if attempt > 0 { if attempt > 0 {
// Exponential backoff: 1s, 2s, 4s // Exponential backoff: 1s, 2s, 4s
backoff := time.Duration(1<<uint(attempt-1)) * time.Second backoff := time.Duration(1<<uint(attempt-1)) * time.Second
log.Warn("OpenAI stream request failed, retrying in %v (attempt %d/%d): %v", backoff, attempt+1, maxRetries, lastErr)
// Add debug log to trace
if trace != nil {
trace.Warn("OpenAI stream request failed, retrying", map[string]any{
"backoff": backoff.String(),
"attempt": attempt + 1,
"max_retries": maxRetries,
"error": lastErr.Error(),
})
}
// Sleep with context cancellation support // Sleep with context cancellation support
timer := time.NewTimer(backoff) timer := time.NewTimer(backoff)
@ -249,16 +265,31 @@ func (p *Provider) Stream(ctx *context.Context, messages []context.Message, opti
response, err := p.streamWithRetry(ctx, currentMessages, options, handler) response, err := p.streamWithRetry(ctx, currentMessages, options, handler)
if err == nil { if err == nil {
if trace != nil {
trace.Debug("OpenAI Stream: Request completed successfully")
}
return response, nil return response, nil
} }
lastErr = err lastErr = err
if trace != nil {
trace.Debug("OpenAI Stream: Request failed", map[string]any{
"error": err.Error(),
})
}
// Check if error is tool call validation failure // Check if error is tool call validation failure
if isToolCallValidationError(err) { if isToolCallValidationError(err) {
// Handle tool call validation retry with feedback to LLM // Handle tool call validation retry with feedback to LLM
validationRetryMessages := currentMessages validationRetryMessages := currentMessages
for validationAttempt := 0; validationAttempt < maxValidationRetries; validationAttempt++ { for validationAttempt := 0; validationAttempt < maxValidationRetries; validationAttempt++ {
log.Warn("Tool call validation failed (attempt %d/%d): %v", validationAttempt+1, maxValidationRetries, err) if trace != nil {
trace.Warn("Tool call validation failed", map[string]any{
"attempt": validationAttempt + 1,
"max_retries": maxValidationRetries,
"error": err.Error(),
})
}
// Add error feedback to conversation history // Add error feedback to conversation history
validationRetryMessages = append(validationRetryMessages, context.Message{ validationRetryMessages = append(validationRetryMessages, context.Message{
@ -301,6 +332,14 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
streamStartTime := time.Now() streamStartTime := time.Now()
requestID := fmt.Sprintf("req_%d", streamStartTime.UnixNano()) requestID := fmt.Sprintf("req_%d", streamStartTime.UnixNano())
// Add debug log
trace, _ := ctx.Trace()
if trace != nil {
trace.Debug("OpenAI Stream: streamWithRetry starting", map[string]any{
"request_id": requestID,
})
}
// Get Go context for cancellation support // Get Go context for cancellation support
goCtx := ctx.Context goCtx := ctx.Context
if goCtx == nil { if goCtx == nil {
@ -394,6 +433,12 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
// Build URL // Build URL
url := buildAPIURL(host, "/chat/completions") url := buildAPIURL(host, "/chat/completions")
if trace != nil {
trace.Debug("OpenAI Stream: Sending request", map[string]any{
"url": url,
})
}
// Create HTTP request with proxy support // Create HTTP request with proxy support
req := http.New(url). req := http.New(url).
SetHeader("Content-Type", "application/json"). SetHeader("Content-Type", "application/json").
@ -413,7 +458,9 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
// Check for context cancellation // Check for context cancellation
select { select {
case <-goCtx.Done(): case <-goCtx.Done():
log.Warn("Stream cancelled by context") if trace != nil {
trace.Warn("Stream cancelled by context")
}
return http.HandlerReturnBreak return http.HandlerReturnBreak
default: default:
} }
@ -421,7 +468,9 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
// Check for force interrupt signal // Check for force interrupt signal
if ctx.Interrupt != nil { if ctx.Interrupt != nil {
if signal := ctx.Interrupt.Peek(); signal != nil && signal.Type == context.InterruptForce { if signal := ctx.Interrupt.Peek(); signal != nil && signal.Type == context.InterruptForce {
log.Warn("Stream cancelled by force interrupt") if trace != nil {
trace.Warn("Stream cancelled by force interrupt")
}
return http.HandlerReturnBreak return http.HandlerReturnBreak
} }
} }
@ -430,13 +479,16 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
return http.HandlerReturnOk return http.HandlerReturnOk
} }
// Log raw stream data for debugging // Record LLM raw output to trace
log.Trace("OpenAI Stream Raw Data: %s", string(data)) if trace != nil {
trace.Debug("LLM Raw Output", map[string]any{
"data": string(data),
})
}
// Parse SSE data // Parse SSE data
dataStr := string(data) dataStr := string(data)
if !strings.HasPrefix(dataStr, "data: ") { if !strings.HasPrefix(dataStr, "data: ") {
log.Trace("Skipping non-SSE line: %s", dataStr)
return http.HandlerReturnOk return http.HandlerReturnOk
} }
@ -451,7 +503,11 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
// Parse JSON chunk // Parse JSON chunk
var chunk StreamChunk var chunk StreamChunk
if err := jsoniter.UnmarshalFromString(dataStr, &chunk); err != nil { if err := jsoniter.UnmarshalFromString(dataStr, &chunk); err != nil {
log.Warn("Failed to parse stream chunk: %v", err) if trace != nil {
trace.Warn("Failed to parse stream chunk", map[string]any{
"error": err.Error(),
})
}
return http.HandlerReturnOk return http.HandlerReturnOk
} }
@ -590,8 +646,13 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
} }
// Log request for debugging // Log request for debugging
if requestBodyJSON, marshalErr := jsoniter.Marshal(requestBody); marshalErr == nil { if trace != nil {
log.Debug("OpenAI Stream Request - URL: %s, Body: %s", url, string(requestBodyJSON)) if requestBodyJSON, marshalErr := jsoniter.Marshal(requestBody); marshalErr == nil {
trace.Debug("OpenAI Stream Request", map[string]any{
"url": url,
"body": string(requestBodyJSON),
})
}
} }
// Buffer to capture non-SSE error responses // Buffer to capture non-SSE error responses
@ -624,7 +685,11 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
// Check if we captured an error response // Check if we captured an error response
if errorDetected && errorBuffer.Len() > 0 { if errorDetected && errorBuffer.Len() > 0 {
errorJSON := errorBuffer.String() errorJSON := errorBuffer.String()
log.Error("OpenAI API returned error response: %s", errorJSON) if trace != nil {
trace.Error("OpenAI API returned error response", map[string]any{
"response": errorJSON,
})
}
// Try to parse error // Try to parse error
var apiError struct { var apiError struct {
@ -645,8 +710,10 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
} }
// Log any error from streaming // Log any error from streaming
if err != nil { if err != nil && trace != nil {
log.Error("OpenAI Stream Error: %v", err) trace.Error("OpenAI Stream Error", map[string]any{
"error": err.Error(),
})
} }
// Check if error is due to context cancellation // Check if error is due to context cancellation
@ -696,14 +763,21 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
// Check if we received any data // Check if we received any data
if accumulator.id == "" { if accumulator.id == "" {
log.Warn("OpenAI stream completed but no data was received (accumulator.id is empty)") if trace != nil {
trace.Warn("OpenAI stream completed but no data was received")
// Log request details for debugging // Log request details for debugging
if requestBodyJSON, err := jsoniter.Marshal(requestBody); err == nil { if requestBodyJSON, err := jsoniter.Marshal(requestBody); err == nil {
log.Error("Request body that caused empty response: %s", string(requestBodyJSON)) trace.Error("Request body that caused empty response", map[string]any{
"body": string(requestBodyJSON),
})
}
trace.Error("Request details", map[string]any{
"url": url,
"model": accumulator.model,
"created": accumulator.created,
})
} }
log.Error("Request URL: %s", url)
log.Error("Model in accumulator: %s, Created: %d", accumulator.model, accumulator.created)
err := fmt.Errorf("no data received from OpenAI API") err := fmt.Errorf("no data received from OpenAI API")
@ -807,6 +881,14 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
// Post post completion request to OpenAI API // Post post completion request to OpenAI API
func (p *Provider) Post(ctx *context.Context, messages []context.Message, options *context.CompletionOptions) (*context.CompletionResponse, error) { func (p *Provider) Post(ctx *context.Context, messages []context.Message, options *context.CompletionOptions) (*context.CompletionResponse, error) {
// Add debug log
trace, _ := ctx.Trace()
if trace != nil {
trace.Debug("OpenAI Post: Starting non-stream request", map[string]any{
"message_count": len(messages),
})
}
maxRetries := 3 maxRetries := 3
maxValidationRetries := 3 maxValidationRetries := 3
var lastErr error var lastErr error
@ -833,7 +915,14 @@ func (p *Provider) Post(ctx *context.Context, messages []context.Message, option
if attempt > 0 { if attempt > 0 {
// Exponential backoff // Exponential backoff
backoff := time.Duration(1<<uint(attempt-1)) * time.Second backoff := time.Duration(1<<uint(attempt-1)) * time.Second
log.Warn("OpenAI post request failed, retrying in %v (attempt %d/%d): %v", backoff, attempt+1, maxRetries, lastErr) if trace != nil {
trace.Warn("OpenAI post request failed, retrying", map[string]any{
"backoff": backoff.String(),
"attempt": attempt + 1,
"max_retries": maxRetries,
"error": lastErr.Error(),
})
}
// Sleep with context cancellation support // Sleep with context cancellation support
timer := time.NewTimer(backoff) timer := time.NewTimer(backoff)
@ -857,7 +946,13 @@ func (p *Provider) Post(ctx *context.Context, messages []context.Message, option
// Handle tool call validation retry with feedback to LLM // Handle tool call validation retry with feedback to LLM
validationRetryMessages := currentMessages validationRetryMessages := currentMessages
for validationAttempt := 0; validationAttempt < maxValidationRetries; validationAttempt++ { for validationAttempt := 0; validationAttempt < maxValidationRetries; validationAttempt++ {
log.Warn("Tool call validation failed (attempt %d/%d): %v", validationAttempt+1, maxValidationRetries, err) if trace != nil {
trace.Warn("Tool call validation failed", map[string]any{
"attempt": validationAttempt + 1,
"max_retries": maxValidationRetries,
"error": err.Error(),
})
}
// Add error feedback to conversation history // Add error feedback to conversation history
validationRetryMessages = append(validationRetryMessages, context.Message{ validationRetryMessages = append(validationRetryMessages, context.Message{
@ -897,6 +992,9 @@ func (p *Provider) Post(ctx *context.Context, messages []context.Message, option
// postWithRetry performs a single POST request attempt // postWithRetry performs a single POST request attempt
func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Message, options *context.CompletionOptions) (*context.CompletionResponse, error) { func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Message, options *context.CompletionOptions) (*context.CompletionResponse, error) {
// Get trace from context
trace, _ := ctx.Trace()
// Preprocess messages and options through adapters // Preprocess messages and options through adapters
processedMessages := messages processedMessages := messages
processedOptions := options processedOptions := options
@ -958,8 +1056,12 @@ func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Messag
} }
} }
// Log full response data for debugging // Log full response data for debugging
if respJSON, err := jsoniter.Marshal(resp.Data); err == nil { if trace != nil {
log.Error("OpenAI API error response: %s", string(respJSON)) if respJSON, err := jsoniter.Marshal(resp.Data); err == nil {
trace.Error("OpenAI API error response", map[string]any{
"response": string(respJSON),
})
}
} }
} }
return nil, fmt.Errorf("HTTP %d: %s", resp.Code, errorMsg) return nil, fmt.Errorf("HTTP %d: %s", resp.Code, errorMsg)

View file

@ -26,12 +26,23 @@ func (w *Writer) Write(msg *message.Message) error {
// CUI adapter passes messages through as-is // CUI adapter passes messages through as-is
chunks, err := w.adapter.Adapt(msg) chunks, err := w.adapter.Adapt(msg)
if err != nil { if err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("CUI Writer: Failed to adapt message", map[string]any{
"error": err.Error(),
"message_type": msg.Type,
})
}
return err return err
} }
// Send each chunk // Send each chunk
for _, chunk := range chunks { for _, chunk := range chunks {
if err := w.sendChunk(chunk); err != nil { if err := w.sendChunk(chunk); err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("CUI Writer: Failed to send chunk", map[string]any{
"error": err.Error(),
})
}
return err return err
} }
} }
@ -46,6 +57,12 @@ func (w *Writer) WriteGroup(group *message.MessageGroup) error {
// Send the group // Send the group
if err := w.sendChunk(group); err != nil { if err := w.sendChunk(group); err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("CUI Writer: Failed to send message group", map[string]any{
"error": err.Error(),
"group_id": group.ID,
})
}
return err return err
} }
@ -70,10 +87,31 @@ func (w *Writer) sendChunk(chunk interface{}) error {
// Convert chunk to JSON // Convert chunk to JSON
data, err := json.Marshal(chunk) data, err := json.Marshal(chunk)
if err != nil { if err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("CUI Writer: Failed to marshal chunk", map[string]any{
"error": err.Error(),
})
}
return err return err
} }
// Log outgoing data to trace for debugging
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Debug("CUI Writer: Sending chunk to client", map[string]any{
"data": string(data),
})
}
// Send via context's writer // Send via context's writer
// The context knows how to send data based on the connection type (SSE, WebSocket, etc.) // The context knows how to send data based on the connection type (SSE, WebSocket, etc.)
return w.ctx.Send(data) if err := w.ctx.Send(data); err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("CUI Writer: Failed to send data to client", map[string]any{
"error": err.Error(),
})
}
return err
}
return nil
} }

View file

@ -2,7 +2,6 @@ package openai
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/output/message" "github.com/yaoapp/yao/agent/output/message"
@ -32,6 +31,12 @@ func (w *Writer) Write(msg *message.Message) error {
// Convert message to OpenAI format using adapter // Convert message to OpenAI format using adapter
chunks, err := w.adapter.Adapt(msg) chunks, err := w.adapter.Adapt(msg)
if err != nil { if err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("OpenAI Writer: Failed to adapt message", map[string]any{
"error": err.Error(),
"message_type": msg.Type,
})
}
return err return err
} }
@ -50,6 +55,11 @@ func (w *Writer) Write(msg *message.Message) error {
} }
if err := w.sendChunk(chunk); err != nil { if err := w.sendChunk(chunk); err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("OpenAI Writer: Failed to send chunk", map[string]any{
"error": err.Error(),
})
}
return err return err
} }
} }
@ -63,6 +73,13 @@ func (w *Writer) WriteGroup(group *message.MessageGroup) error {
// Just send each message individually // Just send each message individually
for _, msg := range group.Messages { for _, msg := range group.Messages {
if err := w.Write(msg); err != nil { if err := w.Write(msg); err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("OpenAI Writer: Failed to write message in group", map[string]any{
"error": err.Error(),
"group_id": group.ID,
"message_type": msg.Type,
})
}
return err return err
} }
} }
@ -88,25 +105,55 @@ func (w *Writer) sendChunk(chunk interface{}) error {
// Convert chunk to JSON // Convert chunk to JSON
data, err := json.Marshal(chunk) data, err := json.Marshal(chunk)
if err != nil { if err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("OpenAI Writer: Failed to marshal chunk", map[string]any{
"error": err.Error(),
})
}
return err return err
} }
// Debug: print the chunk being sent
fmt.Println("-----------------------------------------------")
fmt.Println("Sending SSE chunk: ", string(data))
fmt.Println("-----------------------------------------------")
// Format as SSE: "data: {json}\n\n" // Format as SSE: "data: {json}\n\n"
sseData := append([]byte("data: "), data...) sseData := append([]byte("data: "), data...)
sseData = append(sseData, []byte("\n\n")...) sseData = append(sseData, []byte("\n\n")...)
// Log outgoing data to trace for debugging
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Debug("OpenAI Writer: Sending chunk to client", map[string]any{
"data": string(data),
})
}
// Send via context's writer // Send via context's writer
return w.ctx.Send(sseData) if err := w.ctx.Send(sseData); err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("OpenAI Writer: Failed to send data to client", map[string]any{
"error": err.Error(),
})
}
return err
}
return nil
} }
// sendDone sends the final [DONE] message // sendDone sends the final [DONE] message
func (w *Writer) sendDone() error { func (w *Writer) sendDone() error {
// Log completion to trace
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Debug("OpenAI Writer: Sending [DONE] to client")
}
// OpenAI SSE format uses "data: [DONE]\n\n" to signal completion // OpenAI SSE format uses "data: [DONE]\n\n" to signal completion
doneData := []byte("data: [DONE]\n\n") doneData := []byte("data: [DONE]\n\n")
return w.ctx.Send(doneData) if err := w.ctx.Send(doneData); err != nil {
if trace, _ := w.ctx.Trace(); trace != nil {
trace.Error("OpenAI Writer: Failed to send [DONE] to client", map[string]any{
"error": err.Error(),
})
}
return err
}
return nil
} }

View file

@ -99,7 +99,7 @@ func main() {
manager.SetSpaceValue(space.ID, fmt.Sprintf("worker_%d", idx+1), map[string]any{ manager.SetSpaceValue(space.ID, fmt.Sprintf("worker_%d", idx+1), map[string]any{
"id": idx + 1, "id": idx + 1,
"status": "done", "status": "done",
"time": time.Now().Unix(), "time": time.Now().UnixMilli(),
}) })
// Each worker completes itself // Each worker completes itself
@ -124,7 +124,7 @@ func main() {
Icon: "database", Icon: "database",
}) })
manager.SetSpaceValue(sessionSpace.ID, "total_processed", 3) manager.SetSpaceValue(sessionSpace.ID, "total_processed", 3)
manager.SetSpaceValue(sessionSpace.ID, "timestamp", time.Now().Unix()) manager.SetSpaceValue(sessionSpace.ID, "timestamp", time.Now().UnixMilli())
manager.Complete(map[string]any{"total": 3, "success": true}) manager.Complete(map[string]any{"total": 3, "success": true})

View file

@ -49,7 +49,7 @@ func NewManager(ctx context.Context, traceID string, driver types.Driver) (types
} }
} else { } else {
// New trace - create and broadcast init event // New trace - create and broadcast init event
now := time.Now().Unix() now := time.Now().UnixMilli()
m.addUpdateAndBroadcast(&types.TraceUpdate{ m.addUpdateAndBroadcast(&types.TraceUpdate{
Type: types.UpdateTypeInit, Type: types.UpdateTypeInit,
TraceID: traceID, TraceID: traceID,
@ -100,7 +100,7 @@ func (m *manager) handleCancellation() {
return // Already completed return // Already completed
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Get current nodes (state worker will process this before exiting) // Get current nodes (state worker will process this before exiting)
nodes := m.stateGetCurrentNodes() nodes := m.stateGetCurrentNodes()
@ -125,7 +125,7 @@ func (m *manager) handleCancellation() {
NodeID: node.ID, NodeID: node.ID,
Status: types.CompleteStatusCancelled, Status: types.CompleteStatusCancelled,
EndTime: now, EndTime: now,
Duration: (now - node.StartTime) * 1000, Duration: now - node.StartTime, // Already in milliseconds
Error: "context cancelled", Error: "context cancelled",
}, },
}) })
@ -139,7 +139,7 @@ func (m *manager) handleCancellation() {
totalDuration := int64(0) totalDuration := int64(0)
rootNode := m.stateGetRoot() rootNode := m.stateGetRoot()
if rootNode != nil && rootNode.CreatedAt > 0 { if rootNode != nil && rootNode.CreatedAt > 0 {
totalDuration = (now - rootNode.CreatedAt) * 1000 totalDuration = now - rootNode.CreatedAt // Already in milliseconds
} }
// Broadcast trace cancelled event (this will be processed even after state worker starts draining) // Broadcast trace cancelled event (this will be processed even after state worker starts draining)
@ -169,7 +169,7 @@ func (m *manager) Add(input types.TraceInput, option types.TraceNodeOption) (typ
return nil, err return nil, err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Check if root exists // Check if root exists
rootNode := m.stateGetRoot() rootNode := m.stateGetRoot()
@ -291,7 +291,7 @@ func (m *manager) Parallel(parallelInputs []types.TraceParallelInput) ([]types.N
return nil, fmt.Errorf("root node does not exist, please call Add first before using Parallel") return nil, fmt.Errorf("root node does not exist, please call Add first before using Parallel")
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Get current nodes // Get current nodes
currentNodes := m.stateGetCurrentNodes() currentNodes := m.stateGetCurrentNodes()
@ -356,33 +356,32 @@ func (m *manager) Parallel(parallelInputs []types.TraceParallelInput) ([]types.N
} }
// Info logs info message to current node(s) // Info logs info message to current node(s)
func (m *manager) Info(format string, args ...any) types.Manager { func (m *manager) Info(message string, args ...any) types.Manager {
m.log("info", format, args...) m.log("info", message, args...)
return m return m
} }
// Debug logs debug message to current node(s) // Debug logs debug message to current node(s)
func (m *manager) Debug(format string, args ...any) types.Manager { func (m *manager) Debug(message string, args ...any) types.Manager {
m.log("debug", format, args...) m.log("debug", message, args...)
return m return m
} }
// Error logs error message to current node(s) // Error logs error message to current node(s)
func (m *manager) Error(format string, args ...any) types.Manager { func (m *manager) Error(message string, args ...any) types.Manager {
m.log("error", format, args...) m.log("error", message, args...)
return m return m
} }
// Warn logs warning message to current node(s) // Warn logs warning message to current node(s)
func (m *manager) Warn(format string, args ...any) types.Manager { func (m *manager) Warn(message string, args ...any) types.Manager {
m.log("warn", format, args...) m.log("warn", message, args...)
return m return m
} }
// log helper method to log messages // log helper method to log messages
func (m *manager) log(level string, format string, args ...any) { func (m *manager) log(level string, message string, args ...any) {
message := fmt.Sprintf(format, args...) now := time.Now().UnixMilli()
now := time.Now().Unix()
// Get current nodes // Get current nodes
nodes := m.stateGetCurrentNodes() nodes := m.stateGetCurrentNodes()
@ -393,6 +392,7 @@ func (m *manager) log(level string, format string, args ...any) {
Timestamp: now, Timestamp: now,
Level: level, Level: level,
Message: message, Message: message,
Data: args,
NodeID: node.ID, NodeID: node.ID,
} }
// Save log (ignore errors for non-critical logging) // Save log (ignore errors for non-critical logging)
@ -415,7 +415,7 @@ func (m *manager) SetOutput(output types.TraceOutput) error {
return err return err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
nodes := m.stateGetCurrentNodes() nodes := m.stateGetCurrentNodes()
for _, node := range nodes { for _, node := range nodes {
node.Output = output node.Output = output
@ -442,7 +442,7 @@ func (m *manager) SetMetadata(key string, value any) error {
return err return err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
nodes := m.stateGetCurrentNodes() nodes := m.stateGetCurrentNodes()
for _, node := range nodes { for _, node := range nodes {
if node.Metadata == nil { if node.Metadata == nil {
@ -473,7 +473,7 @@ func (m *manager) Complete(output ...types.TraceOutput) error {
return err return err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
nodes := m.stateGetCurrentNodes() nodes := m.stateGetCurrentNodes()
// Determine output value once // Determine output value once
@ -493,7 +493,7 @@ func (m *manager) Complete(output ...types.TraceOutput) error {
NodeID: node.ID, NodeID: node.ID,
Status: types.CompleteStatusSuccess, Status: types.CompleteStatusSuccess,
EndTime: now, EndTime: now,
Duration: (now - node.StartTime) * 1000, Duration: now - node.StartTime, // Already in milliseconds
Output: node.Output, Output: node.Output,
} }
@ -523,7 +523,7 @@ func (m *manager) Fail(err error) error {
return ctxErr return ctxErr
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Log error first // Log error first
m.Error("Node failed: %v", err) m.Error("Node failed: %v", err)
@ -546,7 +546,7 @@ func (m *manager) Fail(err error) error {
NodeID: node.ID, NodeID: node.ID,
Status: types.CompleteStatusFailed, Status: types.CompleteStatusFailed,
EndTime: now, EndTime: now,
Duration: (node.EndTime - node.StartTime) * 1000, // Convert to milliseconds Duration: node.EndTime - node.StartTime, // Already in milliseconds
Error: err.Error(), Error: err.Error(),
}, },
}) })
@ -580,11 +580,11 @@ func (m *manager) MarkComplete() error {
m.stateSetTraceStatus(types.TraceStatusCompleted) m.stateSetTraceStatus(types.TraceStatusCompleted)
// Calculate total duration from root node // Calculate total duration from root node
now := time.Now().Unix() now := time.Now().UnixMilli()
totalDuration := int64(0) totalDuration := int64(0)
rootNode := m.stateGetRoot() rootNode := m.stateGetRoot()
if rootNode != nil && rootNode.CreatedAt > 0 { if rootNode != nil && rootNode.CreatedAt > 0 {
totalDuration = (now - rootNode.CreatedAt) * 1000 // Convert to milliseconds totalDuration = now - rootNode.CreatedAt // Already in milliseconds
} }
// Broadcast completion event // Broadcast completion event
@ -604,7 +604,7 @@ func (m *manager) CreateSpace(option types.TraceSpaceOption) (*types.TraceSpace,
return nil, err return nil, err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Create space instance // Create space instance
space := &types.TraceSpace{ space := &types.TraceSpace{
@ -673,7 +673,7 @@ func (m *manager) DeleteSpace(id string) error {
return err return err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Remove from cache // Remove from cache
m.stateDeleteSpace(id) m.stateDeleteSpace(id)
@ -722,7 +722,7 @@ func (m *manager) SetSpaceValue(spaceID, key string, value any) error {
return err return err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Get space // Get space
space, err := m.GetSpace(spaceID) space, err := m.GetSpace(spaceID)
@ -788,7 +788,7 @@ func (m *manager) DeleteSpaceValue(spaceID, key string) error {
return err return err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Delete value from driver (through state worker for concurrent safety) // Delete value from driver (through state worker for concurrent safety)
err := m.stateExecuteSpaceOp(spaceID, func() error { err := m.stateExecuteSpaceOp(spaceID, func() error {
@ -817,7 +817,7 @@ func (m *manager) ClearSpaceValues(spaceID string) error {
return err return err
} }
now := time.Now().Unix() now := time.Now().UnixMilli()
// Clear values from driver (through state worker for concurrent safety) // Clear values from driver (through state worker for concurrent safety)
err := m.stateExecuteSpaceOp(spaceID, func() error { err := m.stateExecuteSpaceOp(spaceID, func() error {

View file

@ -1,7 +1,6 @@
package trace package trace
import ( import (
"fmt"
"time" "time"
"github.com/yaoapp/yao/trace/types" "github.com/yaoapp/yao/trace/types"
@ -14,32 +13,32 @@ type node struct {
} }
// Info logs info message (public method, broadcasts event) // Info logs info message (public method, broadcasts event)
func (n *node) Info(format string, args ...any) types.Node { func (n *node) Info(message string, args ...any) types.Node {
n.logWithBroadcast("info", format, args...) n.logWithBroadcast("info", message, args...)
return n return n
} }
// Debug logs debug message (public method, broadcasts event) // Debug logs debug message (public method, broadcasts event)
func (n *node) Debug(format string, args ...any) types.Node { func (n *node) Debug(message string, args ...any) types.Node {
n.logWithBroadcast("debug", format, args...) n.logWithBroadcast("debug", message, args...)
return n return n
} }
// Error logs error message (public method, broadcasts event) // Error logs error message (public method, broadcasts event)
func (n *node) Error(format string, args ...any) types.Node { func (n *node) Error(message string, args ...any) types.Node {
n.logWithBroadcast("error", format, args...) n.logWithBroadcast("error", message, args...)
return n return n
} }
// Warn logs warning message (public method, broadcasts event) // Warn logs warning message (public method, broadcasts event)
func (n *node) Warn(format string, args ...any) types.Node { func (n *node) Warn(message string, args ...any) types.Node {
n.logWithBroadcast("warn", format, args...) n.logWithBroadcast("warn", message, args...)
return n return n
} }
// logWithBroadcast logs and broadcasts event (for external calls) // logWithBroadcast logs and broadcasts event (for external calls)
func (n *node) logWithBroadcast(level string, format string, args ...any) { func (n *node) logWithBroadcast(level string, message string, args ...any) {
log := n.log(level, format, args...) log := n.log(level, message, args...)
// Broadcast event // Broadcast event
n.manager.addUpdateAndBroadcast(&types.TraceUpdate{ n.manager.addUpdateAndBroadcast(&types.TraceUpdate{
@ -52,12 +51,12 @@ func (n *node) logWithBroadcast(level string, format string, args ...any) {
} }
// log logs without broadcasting (for internal Manager calls) // log logs without broadcasting (for internal Manager calls)
func (n *node) log(level string, format string, args ...any) *types.TraceLog { func (n *node) log(level string, message string, args ...any) *types.TraceLog {
message := fmt.Sprintf(format, args...)
log := &types.TraceLog{ log := &types.TraceLog{
Timestamp: time.Now().Unix(), Timestamp: time.Now().UnixMilli(),
Level: level, Level: level,
Message: message, Message: message,
Data: args,
NodeID: n.data.ID, NodeID: n.data.ID,
} }
// Save log (ignore errors for non-critical logging) // Save log (ignore errors for non-critical logging)
@ -67,7 +66,7 @@ func (n *node) log(level string, format string, args ...any) *types.TraceLog {
// Add creates next sequential node // Add creates next sequential node
func (n *node) Add(input types.TraceInput, option types.TraceNodeOption) (types.Node, error) { func (n *node) Add(input types.TraceInput, option types.TraceNodeOption) (types.Node, error) {
now := time.Now().Unix() now := time.Now().UnixMilli()
// Create child node data // Create child node data
childNodeData := &types.TraceNode{ childNodeData := &types.TraceNode{
@ -102,7 +101,7 @@ func (n *node) Add(input types.TraceInput, option types.TraceNodeOption) (types.
// Parallel creates multiple concurrent child nodes // Parallel creates multiple concurrent child nodes
func (n *node) Parallel(parallelInputs []types.TraceParallelInput) ([]types.Node, error) { func (n *node) Parallel(parallelInputs []types.TraceParallelInput) ([]types.Node, error) {
now := time.Now().Unix() now := time.Now().UnixMilli()
nodeInterfaces := make([]types.Node, 0, len(parallelInputs)) nodeInterfaces := make([]types.Node, 0, len(parallelInputs))
// Create multiple child nodes // Create multiple child nodes
@ -142,7 +141,7 @@ func (n *node) Parallel(parallelInputs []types.TraceParallelInput) ([]types.Node
// Join joins multiple nodes into one // Join joins multiple nodes into one
func (n *node) Join(nodes []*types.TraceNode, input types.TraceInput, option types.TraceNodeOption) (types.Node, error) { func (n *node) Join(nodes []*types.TraceNode, input types.TraceInput, option types.TraceNodeOption) (types.Node, error) {
now := time.Now().Unix() now := time.Now().UnixMilli()
// Create join node data // Create join node data
joinNodeData := &types.TraceNode{ joinNodeData := &types.TraceNode{
@ -177,7 +176,7 @@ func (n *node) ID() string {
// SetOutput sets the node output // SetOutput sets the node output
func (n *node) SetOutput(output types.TraceOutput) error { func (n *node) SetOutput(output types.TraceOutput) error {
n.data.Output = output n.data.Output = output
n.data.UpdatedAt = time.Now().Unix() n.data.UpdatedAt = time.Now().UnixMilli()
return n.manager.driver.SaveNode(n.manager.ctx, n.manager.traceID, n.data) return n.manager.driver.SaveNode(n.manager.ctx, n.manager.traceID, n.data)
} }
@ -187,14 +186,14 @@ func (n *node) SetMetadata(key string, value any) error {
n.data.Metadata = make(map[string]any) n.data.Metadata = make(map[string]any)
} }
n.data.Metadata[key] = value n.data.Metadata[key] = value
n.data.UpdatedAt = time.Now().Unix() n.data.UpdatedAt = time.Now().UnixMilli()
return n.manager.driver.SaveNode(n.manager.ctx, n.manager.traceID, n.data) return n.manager.driver.SaveNode(n.manager.ctx, n.manager.traceID, n.data)
} }
// SetStatus sets the node status // SetStatus sets the node status
func (n *node) SetStatus(status string) error { func (n *node) SetStatus(status string) error {
n.data.Status = types.NodeStatus(status) n.data.Status = types.NodeStatus(status)
n.data.UpdatedAt = time.Now().Unix() n.data.UpdatedAt = time.Now().UnixMilli()
return n.manager.driver.SaveNode(n.manager.ctx, n.manager.traceID, n.data) return n.manager.driver.SaveNode(n.manager.ctx, n.manager.traceID, n.data)
} }
@ -219,7 +218,7 @@ func (n *node) Complete(output ...types.TraceOutput) error {
// complete marks as completed without broadcasting (for Manager calls) // complete marks as completed without broadcasting (for Manager calls)
func (n *node) complete(output ...types.TraceOutput) error { func (n *node) complete(output ...types.TraceOutput) error {
now := time.Now().Unix() now := time.Now().UnixMilli()
// Set output if provided // Set output if provided
if len(output) > 0 { if len(output) > 0 {
@ -255,7 +254,7 @@ func (n *node) Fail(err error) error {
// fail marks as failed without broadcasting (for Manager calls) // fail marks as failed without broadcasting (for Manager calls)
func (n *node) fail(err error) error { func (n *node) fail(err error) error {
now := time.Now().Unix() now := time.Now().UnixMilli()
// Update status // Update status
n.data.Status = types.StatusFailed n.data.Status = types.StatusFailed

View file

@ -7,9 +7,9 @@ import (
"github.com/yaoapp/yao/trace/types" "github.com/yaoapp/yao/trace/types"
) )
// Subscribe creates a new subscription for trace updates (real-time from now) // Subscribe creates a new subscription for trace updates (replays all historical events from the beginning)
func (m *manager) Subscribe() (<-chan *types.TraceUpdate, error) { func (m *manager) Subscribe() (<-chan *types.TraceUpdate, error) {
return m.subscribe(time.Now().Unix()) return m.subscribe(0) // Subscribe from beginning to get all historical events
} }
// SubscribeFrom creates a subscription starting from a specific timestamp // SubscribeFrom creates a subscription starting from a specific timestamp
@ -47,15 +47,25 @@ func (m *manager) replayAndStream(subID string, ch chan *types.TraceUpdate, sinc
// Get historical updates // Get historical updates
updates := m.stateGetUpdates(since) updates := m.stateGetUpdates(since)
// Replay historical updates // Replay historical updates and check if trace was already completed
traceWasCompleted := false
for _, update := range updates { for _, update := range updates {
select { select {
case ch <- update: case ch <- update:
// Check if this is a trace complete event
if update.Type == types.UpdateTypeComplete {
traceWasCompleted = true
}
case <-m.ctx.Done(): case <-m.ctx.Done():
return return
} }
} }
// If trace was already completed in historical events, exit immediately
if traceWasCompleted {
return
}
// Continue streaming new updates // Continue streaming new updates
// The channel will receive updates via broadcast from addUpdate // The channel will receive updates via broadcast from addUpdate
// Monitor completion to know when to exit // Monitor completion to know when to exit

View file

@ -103,7 +103,7 @@ func GenTraceID(safe ...bool) string {
// option: trace options (optional) // option: trace options (optional)
// driverOptions: driver-specific options (e.g., base path for local, store name for store) // driverOptions: driver-specific options (e.g., base path for local, store name for store)
func New(ctx context.Context, driver string, option *types.TraceOption, driverOptions ...any) (string, types.Manager, error) { func New(ctx context.Context, driver string, option *types.TraceOption, driverOptions ...any) (string, types.Manager, error) {
now := time.Now().Unix() now := time.Now().UnixMilli()
// Handle nil option // Handle nil option
if option == nil { if option == nil {
@ -235,7 +235,7 @@ func LoadFromStorage(ctx context.Context, driver string, traceID string, driverO
// Update stored info with new manager // Update stored info with new manager
storedInfo.Manager = manager storedInfo.Manager = manager
storedInfo.UpdatedAt = time.Now().Unix() storedInfo.UpdatedAt = time.Now().UnixMilli()
// Register in global registry // Register in global registry
registryMu.Lock() registryMu.Lock()

View file

@ -124,7 +124,7 @@ func TestSubscribeFrom(t *testing.T) {
time.Sleep(1100 * time.Millisecond) time.Sleep(1100 * time.Millisecond)
// Record timestamp (simulate user noting current time before refresh) // Record timestamp (simulate user noting current time before refresh)
resumeTimestamp := time.Now().Unix() resumeTimestamp := time.Now().UnixMilli()
// Wait again to ensure next operations are after resumeTimestamp // Wait again to ensure next operations are after resumeTimestamp
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)

View file

@ -4,10 +4,11 @@ import "context"
// TraceLog represents a log entry // TraceLog represents a log entry
type TraceLog struct { type TraceLog struct {
Timestamp int64 // Log timestamp Timestamp int64 // Log timestamp (milliseconds since epoch)
Level string // Log level (info, debug, error, warn) Level string // Log level (info, debug, error, warn)
Message string // Log message Message string // Log message
NodeID string // Node ID this log belongs to Data []any // Additional data arguments
NodeID string // Node ID this log belongs to
} }
// Driver defines the storage driver interface that providers must implement // Driver defines the storage driver interface that providers must implement
@ -71,7 +72,7 @@ type Driver interface {
// SaveUpdate persists a trace update event to storage // SaveUpdate persists a trace update event to storage
SaveUpdate(ctx context.Context, traceID string, update *TraceUpdate) error SaveUpdate(ctx context.Context, traceID string, update *TraceUpdate) error
// LoadUpdates loads trace update events from storage (filtering by timestamp) // LoadUpdates loads trace update events from storage (filtering by timestamp in milliseconds)
LoadUpdates(ctx context.Context, traceID string, since int64) ([]*TraceUpdate, error) LoadUpdates(ctx context.Context, traceID string, since int64) ([]*TraceUpdate, error)
// Close closes the driver and releases resources // Close closes the driver and releases resources

View file

@ -18,7 +18,7 @@ func (n *TraceNode) ToCompleteData() *NodeCompleteData {
NodeID: n.ID, NodeID: n.ID,
Status: CompleteStatusSuccess, Status: CompleteStatusSuccess,
EndTime: n.EndTime, EndTime: n.EndTime,
Duration: (n.EndTime - n.StartTime) * 1000, // Convert to milliseconds Duration: n.EndTime - n.StartTime, // Already in milliseconds
Output: n.Output, Output: n.Output,
} }
} }
@ -29,7 +29,7 @@ func (n *TraceNode) ToFailedData(err error) *NodeFailedData {
NodeID: n.ID, NodeID: n.ID,
Status: CompleteStatusFailed, Status: CompleteStatusFailed,
EndTime: n.EndTime, EndTime: n.EndTime,
Duration: (n.EndTime - n.StartTime) * 1000, // Convert to milliseconds Duration: n.EndTime - n.StartTime, // Already in milliseconds
Error: err.Error(), Error: err.Error(),
} }
} }

View file

@ -12,10 +12,10 @@ type Manager interface {
Parallel(parallelInputs []TraceParallelInput) ([]Node, error) Parallel(parallelInputs []TraceParallelInput) ([]Node, error)
// Log Operations - log to current node(s) with chainable interface // Log Operations - log to current node(s) with chainable interface
Info(format string, args ...any) Manager Info(message string, args ...any) Manager
Debug(format string, args ...any) Manager Debug(message string, args ...any) Manager
Error(format string, args ...any) Manager Error(message string, args ...any) Manager
Warn(format string, args ...any) Manager Warn(message string, args ...any) Manager
// Node Status Operations - operate on current node(s) // Node Status Operations - operate on current node(s)
SetOutput(output TraceOutput) error SetOutput(output TraceOutput) error
@ -60,10 +60,10 @@ type Manager interface {
// Context is bound to Node at creation time // Context is bound to Node at creation time
type Node interface { type Node interface {
// Log Operations - chainable interface // Log Operations - chainable interface
Info(format string, args ...any) Node Info(message string, args ...any) Node
Debug(format string, args ...any) Node Debug(message string, args ...any) Node
Error(format string, args ...any) Node Error(message string, args ...any) Node
Warn(format string, args ...any) Node Warn(message string, args ...any) Node
// Node Tree Operations // Node Tree Operations
Add(input TraceInput, option TraceNodeOption) (Node, error) Add(input TraceInput, option TraceNodeOption) (Node, error)

View file

@ -61,10 +61,10 @@ type TraceNode struct {
Status NodeStatus // Node status (pending, running, completed, failed, skipped) Status NodeStatus // Node status (pending, running, completed, failed, skipped)
Input TraceInput // Node input data Input TraceInput // Node input data
Output TraceOutput // Node output data Output TraceOutput // Node output data
CreatedAt int64 // Creation timestamp CreatedAt int64 // Creation timestamp (milliseconds since epoch)
StartTime int64 // Start timestamp StartTime int64 // Start timestamp (milliseconds since epoch)
EndTime int64 // End timestamp EndTime int64 // End timestamp (milliseconds since epoch)
UpdatedAt int64 // Last update timestamp UpdatedAt int64 // Last update timestamp (milliseconds since epoch)
// Other fields will be added during implementation // Other fields will be added during implementation
} }
@ -72,8 +72,8 @@ type TraceNode struct {
type TraceSpace struct { type TraceSpace struct {
ID string // Space ID ID string // Space ID
TraceSpaceOption // Embedded option fields (Label, Icon, Description, TTL, Metadata) TraceSpaceOption // Embedded option fields (Label, Icon, Description, TTL, Metadata)
CreatedAt int64 // Creation timestamp CreatedAt int64 // Creation timestamp (milliseconds since epoch)
UpdatedAt int64 // Last update timestamp UpdatedAt int64 // Last update timestamp (milliseconds since epoch)
// Internal data storage will be managed by implementation // Internal data storage will be managed by implementation
} }
@ -118,7 +118,7 @@ type TraceUpdate struct {
TraceID string // Trace ID TraceID string // Trace ID
NodeID string // Node ID (optional, for node/log updates) NodeID string // Node ID (optional, for node/log updates)
SpaceID string // Space ID (optional, for space updates) SpaceID string // Space ID (optional, for space updates)
Timestamp int64 // Update timestamp Timestamp int64 // Update timestamp (milliseconds since epoch)
Data any // Update data (payload structures below) Data any // Update data (payload structures below)
} }
@ -141,18 +141,18 @@ type NodeStartData struct {
// NodeCompleteData payload for "node_complete" event // NodeCompleteData payload for "node_complete" event
type NodeCompleteData struct { type NodeCompleteData struct {
NodeID string `json:"nodeId"` NodeID string `json:"nodeId"`
Status CompleteStatus `json:"status"` // "success" or "failed" Status CompleteStatus `json:"status"` // "success" or "failed"
EndTime int64 `json:"endTime"` EndTime int64 `json:"endTime"` // milliseconds since epoch
Duration int64 `json:"duration"` // in milliseconds Duration int64 `json:"duration"` // duration in milliseconds
Output TraceOutput `json:"output,omitempty"` Output TraceOutput `json:"output,omitempty"`
} }
// NodeFailedData payload for "node_failed" event (same as NodeCompleteData but with error) // NodeFailedData payload for "node_failed" event (same as NodeCompleteData but with error)
type NodeFailedData struct { type NodeFailedData struct {
NodeID string `json:"nodeId"` NodeID string `json:"nodeId"`
Status CompleteStatus `json:"status"` // "failed" Status CompleteStatus `json:"status"` // "failed"
EndTime int64 `json:"endTime"` EndTime int64 `json:"endTime"` // milliseconds since epoch
Duration int64 `json:"duration"` Duration int64 `json:"duration"` // duration in milliseconds
Error string `json:"error"` Error string `json:"error"`
} }
@ -168,15 +168,15 @@ type MemoryItem struct {
Type string `json:"type"` Type string `json:"type"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Content any `json:"content"` Content any `json:"content"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"` // milliseconds since epoch
Importance string `json:"importance,omitempty"` // "high", "medium", "low" Importance string `json:"importance,omitempty"` // "high", "medium", "low"
} }
// TraceCompleteData payload for "complete" event // TraceCompleteData payload for "complete" event
type TraceCompleteData struct { type TraceCompleteData struct {
TraceID string `json:"traceId"` TraceID string `json:"traceId"`
Status TraceStatus `json:"status"` // "completed" Status TraceStatus `json:"status"` // "completed"
TotalDuration int64 `json:"totalDuration"` TotalDuration int64 `json:"totalDuration"` // duration in milliseconds
} }
// SpaceDeletedData payload for "space_deleted" event // SpaceDeletedData payload for "space_deleted" event
@ -197,9 +197,9 @@ type TraceInfo struct {
Driver string `json:"driver"` Driver string `json:"driver"`
Status TraceStatus `json:"status"` // Trace status Status TraceStatus `json:"status"` // Trace status
Options []any `json:"options,omitempty"` Options []any `json:"options,omitempty"`
Manager Manager `json:"-"` // Not persisted Manager Manager `json:"-"` // Not persisted
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"` // milliseconds since epoch
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"` // milliseconds since epoch
CreatedBy string `json:"__yao_created_by,omitempty"` CreatedBy string `json:"__yao_created_by,omitempty"`
UpdatedBy string `json:"__yao_updated_by,omitempty"` UpdatedBy string `json:"__yao_updated_by,omitempty"`
TeamID string `json:"__yao_team_id,omitempty"` TeamID string `json:"__yao_team_id,omitempty"`