Skip to content

Commit 1678fe0

Browse files
committed
use uber/zap for logger
Signed-off-by: Akihiko Kuroda <[email protected]>
1 parent 216dbfc commit 1678fe0

File tree

4 files changed

+258
-2
lines changed

4 files changed

+258
-2
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ require (
3131
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
3232
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
3333
github.com/yusufpapurcu/wmi v1.2.4 // indirect
34+
go.uber.org/multierr v1.11.0 // indirect
35+
go.uber.org/zap v1.27.0 // indirect
3436
golang.org/x/sys v0.25.0 // indirect
3537
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
3638
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI
7070
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
7171
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
7272
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
73+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
74+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
75+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
76+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
7377
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7478
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7579
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

internal/common/file_logger.go

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1620
var (
@@ -50,7 +54,8 @@ func generateUUID() string {
5054

5155
// FileLogger handles logging of workflow and agent activities to files
5256
type 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
74141
func (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
79147
func (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
102261
type TokenUsage struct {
103262
PromptTokens int `json:"prompt_tokens,omitempty"`

internal/common/file_logger_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ func TestNewFileLogger(t *testing.T) {
5454
if logger.LogDir != tempDir {
5555
t.Errorf("Expected LogDir to be %s, got %s", tempDir, logger.LogDir)
5656
}
57+
// Check that loggers map is initialized
58+
if logger.loggers == nil {
59+
t.Error("Expected loggers map to be initialized")
60+
}
5761
})
5862

5963
// Test with default log directory
@@ -124,6 +128,7 @@ func TestGenerateWorkflowID(t *testing.T) {
124128
if ids[id] {
125129
t.Errorf("Generated duplicate ID: %s", id)
126130
}
131+
127132
ids[id] = true
128133
}
129134
}
@@ -475,3 +480,89 @@ func TestLogWorkflowRun(t *testing.T) {
475480
}
476481

477482
// Made with Bob
483+
484+
func TestGetLogger(t *testing.T) {
485+
// Create a temporary directory for testing
486+
tempDir, err := os.MkdirTemp("", "file_logger_test")
487+
if err != nil {
488+
t.Fatalf("Failed to create temp directory: %v", err)
489+
}
490+
defer os.RemoveAll(tempDir)
491+
492+
logger, err := NewFileLogger(tempDir)
493+
if err != nil {
494+
t.Fatalf("NewFileLogger failed: %v", err)
495+
}
496+
defer logger.Close() // Clean up resources
497+
498+
// Test getting a new logger
499+
t.Run("GetNewLogger", func(t *testing.T) {
500+
workflowID := "test-workflow"
501+
zapLogger, err := logger.getLogger(workflowID)
502+
if err != nil {
503+
t.Fatalf("getLogger failed: %v", err)
504+
}
505+
if zapLogger == nil {
506+
t.Error("Expected non-nil logger")
507+
}
508+
509+
// Check that logger was cached
510+
if cachedLogger, ok := logger.loggers[workflowID]; !ok || cachedLogger != zapLogger {
511+
t.Error("Logger was not properly cached")
512+
}
513+
})
514+
515+
// Test getting an existing logger
516+
t.Run("GetExistingLogger", func(t *testing.T) {
517+
workflowID := "test-workflow-2"
518+
firstLogger, err := logger.getLogger(workflowID)
519+
if err != nil {
520+
t.Fatalf("First getLogger failed: %v", err)
521+
}
522+
523+
secondLogger, err := logger.getLogger(workflowID)
524+
if err != nil {
525+
t.Fatalf("Second getLogger failed: %v", err)
526+
}
527+
528+
if firstLogger != secondLogger {
529+
t.Error("Expected same logger instance to be returned")
530+
}
531+
})
532+
}
533+
534+
func TestClose(t *testing.T) {
535+
// Create a temporary directory for testing
536+
tempDir, err := os.MkdirTemp("", "file_logger_test")
537+
if err != nil {
538+
t.Fatalf("Failed to create temp directory: %v", err)
539+
}
540+
defer os.RemoveAll(tempDir)
541+
542+
logger, err := NewFileLogger(tempDir)
543+
if err != nil {
544+
t.Fatalf("NewFileLogger failed: %v", err)
545+
}
546+
547+
// Create some loggers
548+
workflowIDs := []string{"workflow1", "workflow2", "workflow3"}
549+
for _, id := range workflowIDs {
550+
_, err := logger.getLogger(id)
551+
if err != nil {
552+
t.Fatalf("Failed to get logger for %s: %v", id, err)
553+
}
554+
}
555+
556+
// Verify loggers exist
557+
if len(logger.loggers) != len(workflowIDs) {
558+
t.Errorf("Expected %d loggers, got %d", len(workflowIDs), len(logger.loggers))
559+
}
560+
561+
// Close loggers
562+
logger.Close()
563+
564+
// Verify loggers map is empty
565+
if len(logger.loggers) != 0 {
566+
t.Errorf("Expected empty loggers map after Close, got %d entries", len(logger.loggers))
567+
}
568+
}

0 commit comments

Comments
 (0)