@@ -10,7 +10,11 @@ import (
1010 "fmt"
1111 "os"
1212 "path/filepath"
13+ "strings"
1314 "time"
15+
16+ "go.uber.org/zap"
17+ "go.uber.org/zap/zapcore"
1418)
1519
1620var (
@@ -50,7 +54,8 @@ func generateUUID() string {
5054
5155// FileLogger handles logging of workflow and agent activities to files
5256type FileLogger struct {
53- LogDir string
57+ LogDir string
58+ loggers map [string ]* zap.Logger // Map of workflow IDs to loggers
5459}
5560
5661// NewFileLogger creates a new FileLogger instance
@@ -66,17 +71,81 @@ func NewFileLogger(logDir string) (*FileLogger, error) {
6671 }
6772
6873 return & FileLogger {
69- LogDir : dir ,
74+ LogDir : dir ,
75+ loggers : make (map [string ]* zap.Logger ),
7076 }, nil
7177}
7278
79+ // createLogger creates a new zap logger for a specific workflow
80+ func (l * FileLogger ) createLogger (workflowID string ) (* zap.Logger , error ) {
81+ logPath := filepath .Join (l .LogDir , fmt .Sprintf ("maestro_run_%s.jsonl" , workflowID ))
82+
83+ // Create encoder config for JSON format
84+ encoderConfig := zapcore.EncoderConfig {
85+ TimeKey : "timestamp" ,
86+ LevelKey : zapcore .OmitKey , // Omit log level as it's not in original format
87+ NameKey : zapcore .OmitKey ,
88+ CallerKey : zapcore .OmitKey ,
89+ FunctionKey : zapcore .OmitKey ,
90+ MessageKey : zapcore .OmitKey , // We'll use custom fields instead of message
91+ StacktraceKey : zapcore .OmitKey ,
92+ LineEnding : zapcore .DefaultLineEnding ,
93+ EncodeLevel : zapcore .LowercaseLevelEncoder ,
94+ EncodeTime : zapcore .ISO8601TimeEncoder ,
95+ EncodeDuration : zapcore .MillisDurationEncoder ,
96+ EncodeCaller : zapcore .ShortCallerEncoder ,
97+ }
98+
99+ // Create file for logging
100+ file , err := os .OpenFile (logPath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0644 )
101+ if err != nil {
102+ return nil , fmt .Errorf ("failed to open log file: %w" , err )
103+ }
104+
105+ // Create core with JSON encoder and file writer
106+ core := zapcore .NewCore (
107+ zapcore .NewJSONEncoder (encoderConfig ),
108+ zapcore .AddSync (file ),
109+ zap .InfoLevel ,
110+ )
111+
112+ // Create logger
113+ return zap .New (core ), nil
114+ }
115+
116+ // getLogger gets or creates a logger for the specified workflow
117+ func (l * FileLogger ) getLogger (workflowID string ) (* zap.Logger , error ) {
118+ if logger , ok := l .loggers [workflowID ]; ok {
119+ return logger , nil
120+ }
121+
122+ logger , err := l .createLogger (workflowID )
123+ if err != nil {
124+ return nil , err
125+ }
126+
127+ l .loggers [workflowID ] = logger
128+ return logger , nil
129+ }
130+
131+ // Close closes all loggers and releases resources
132+ func (l * FileLogger ) Close () {
133+ for _ , logger := range l .loggers {
134+ // Sync ensures all buffered logs are written
135+ _ = logger .Sync ()
136+ }
137+ l .loggers = make (map [string ]* zap.Logger )
138+ }
139+
73140// GenerateWorkflowID generates a unique workflow ID
74141func (l * FileLogger ) GenerateWorkflowID () string {
75142 return generateUUID ()
76143}
77144
78145// writeJSONLine writes a JSON line to the specified log file
146+ // Kept for backward compatibility with tests
79147func (l * FileLogger ) writeJSONLine (logPath string , data interface {}) error {
148+ // For backward compatibility with tests, use the direct file approach
80149 jsonData , err := json .Marshal (data )
81150 if err != nil {
82151 return fmt .Errorf ("failed to marshal JSON: %w" , err )
@@ -98,6 +167,96 @@ func (l *FileLogger) writeJSONLine(logPath string, data interface{}) error {
98167 return nil
99168}
100169
170+ // writeJSONLineWithZap writes a JSON line to the specified log file using zap
171+ // This is an internal method used by the new implementation
172+ func (l * FileLogger ) writeJSONLineWithZap (logPath string , data interface {}) error {
173+ // Extract the workflow ID from the log path
174+ base := filepath .Base (logPath )
175+ // Expected format: maestro_run_{workflowID}.jsonl
176+ workflowID := ""
177+ prefix := "maestro_run_"
178+ suffix := ".jsonl"
179+
180+ if len (base ) > len (prefix ) && strings .HasPrefix (base , prefix ) && strings .HasSuffix (base , suffix ) {
181+ workflowID = base [len (prefix ) : len (base )- len (suffix )]
182+ } else {
183+ // If we can't extract the workflow ID, create a temporary logger
184+ encoderConfig := zapcore.EncoderConfig {
185+ TimeKey : "timestamp" ,
186+ LevelKey : zapcore .OmitKey ,
187+ NameKey : zapcore .OmitKey ,
188+ CallerKey : zapcore .OmitKey ,
189+ FunctionKey : zapcore .OmitKey ,
190+ MessageKey : zapcore .OmitKey ,
191+ StacktraceKey : zapcore .OmitKey ,
192+ LineEnding : zapcore .DefaultLineEnding ,
193+ EncodeLevel : zapcore .LowercaseLevelEncoder ,
194+ EncodeTime : zapcore .ISO8601TimeEncoder ,
195+ EncodeDuration : zapcore .MillisDurationEncoder ,
196+ EncodeCaller : zapcore .ShortCallerEncoder ,
197+ }
198+
199+ file , err := os .OpenFile (logPath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0644 )
200+ if err != nil {
201+ return fmt .Errorf ("failed to open log file: %w" , err )
202+ }
203+ defer file .Close ()
204+
205+ core := zapcore .NewCore (
206+ zapcore .NewJSONEncoder (encoderConfig ),
207+ zapcore .AddSync (file ),
208+ zap .InfoLevel ,
209+ )
210+
211+ logger := zap .New (core )
212+ defer logger .Sync ()
213+
214+ // Convert data to zap fields
215+ jsonData , err := json .Marshal (data )
216+ if err != nil {
217+ return fmt .Errorf ("failed to marshal JSON: %w" , err )
218+ }
219+
220+ var fields map [string ]interface {}
221+ if err := json .Unmarshal (jsonData , & fields ); err != nil {
222+ return fmt .Errorf ("failed to unmarshal JSON: %w" , err )
223+ }
224+
225+ zapFields := make ([]zap.Field , 0 , len (fields ))
226+ for k , v := range fields {
227+ zapFields = append (zapFields , zap .Any (k , v ))
228+ }
229+
230+ logger .Info ("" , zapFields ... )
231+ return nil
232+ }
233+
234+ // Get or create a logger for this workflow
235+ logger , err := l .getLogger (workflowID )
236+ if err != nil {
237+ return err
238+ }
239+
240+ // Convert data to zap fields
241+ jsonData , err := json .Marshal (data )
242+ if err != nil {
243+ return fmt .Errorf ("failed to marshal JSON: %w" , err )
244+ }
245+
246+ var fields map [string ]interface {}
247+ if err := json .Unmarshal (jsonData , & fields ); err != nil {
248+ return fmt .Errorf ("failed to unmarshal JSON: %w" , err )
249+ }
250+
251+ zapFields := make ([]zap.Field , 0 , len (fields ))
252+ for k , v := range fields {
253+ zapFields = append (zapFields , zap .Any (k , v ))
254+ }
255+
256+ logger .Info ("" , zapFields ... )
257+ return nil
258+ }
259+
101260// TokenUsage represents token usage information
102261type TokenUsage struct {
103262 PromptTokens int `json:"prompt_tokens,omitempty"`
0 commit comments