From f1007771997bd0401516eda87a7e0ac92f269680 Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Fri, 9 May 2025 13:37:13 -0500 Subject: wip: logging improvements --- internal/logging/logger.go | 66 ----------------- internal/logging/logging.go | 51 ++++++++++++++ internal/logging/manager.go | 48 +++++++++++++ internal/logging/message.go | 19 ----- internal/logging/service.go | 167 ++++++++++++++++++++++++++++++++++++++++++++ internal/logging/writer.go | 68 +++--------------- 6 files changed, 277 insertions(+), 142 deletions(-) delete mode 100644 internal/logging/logger.go create mode 100644 internal/logging/logging.go create mode 100644 internal/logging/manager.go delete mode 100644 internal/logging/message.go create mode 100644 internal/logging/service.go (limited to 'internal/logging') diff --git a/internal/logging/logger.go b/internal/logging/logger.go deleted file mode 100644 index 31462a542..000000000 --- a/internal/logging/logger.go +++ /dev/null @@ -1,66 +0,0 @@ -package logging - -import ( - "fmt" - "log/slog" - "os" - "runtime/debug" - "time" - - "github.com/opencode-ai/opencode/internal/status" -) - -func Info(msg string, args ...any) { - slog.Info(msg, args...) -} - -func Debug(msg string, args ...any) { - slog.Debug(msg, args...) -} - -func Warn(msg string, args ...any) { - slog.Warn(msg, args...) -} - -func Error(msg string, args ...any) { - slog.Error(msg, args...) -} - -// RecoverPanic is a common function to handle panics gracefully. -// It logs the error, creates a panic log file with stack trace, -// and executes an optional cleanup function before returning. -func RecoverPanic(name string, cleanup func()) { - if r := recover(); r != nil { - // Log the panic - errorMsg := fmt.Sprintf("Panic in %s: %v", name, r) - Error(errorMsg) - status.Error(errorMsg) - - // Create a timestamped panic log file - timestamp := time.Now().Format("20060102-150405") - filename := fmt.Sprintf("opencode-panic-%s-%s.log", name, timestamp) - - file, err := os.Create(filename) - if err != nil { - errMsg := fmt.Sprintf("Failed to create panic log: %v", err) - Error(errMsg) - status.Error(errMsg) - } else { - defer file.Close() - - // Write panic information and stack trace - fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r) - fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339)) - fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack()) - - infoMsg := fmt.Sprintf("Panic details written to %s", filename) - Info(infoMsg) - status.Info(infoMsg) - } - - // Execute cleanup function if provided - if cleanup != nil { - cleanup() - } - } -} diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 000000000..af14a0ded --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,51 @@ +package logging + +import ( + "fmt" + "log/slog" + "os" + "runtime/debug" + "time" + + "github.com/opencode-ai/opencode/internal/status" +) + +// RecoverPanic is a common function to handle panics gracefully. +// It logs the error, creates a panic log file with stack trace, +// and executes an optional cleanup function before returning. +func RecoverPanic(name string, cleanup func()) { + if r := recover(); r != nil { + // Log the panic + errorMsg := fmt.Sprintf("Panic in %s: %v", name, r) + slog.Error(errorMsg) + status.Error(errorMsg) + + // Create a timestamped panic log file + timestamp := time.Now().Format("20060102-150405") + filename := fmt.Sprintf("opencode-panic-%s-%s.log", name, timestamp) + + file, err := os.Create(filename) + if err != nil { + errMsg := fmt.Sprintf("Failed to create panic log: %v", err) + slog.Error(errMsg) + status.Error(errMsg) + } else { + defer file.Close() + + // Write panic information and stack trace + fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r) + fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack()) + + infoMsg := fmt.Sprintf("Panic details written to %s", filename) + slog.Info(infoMsg) + status.Info(infoMsg) + } + + // Execute cleanup function if provided + if cleanup != nil { + cleanup() + } + } +} + diff --git a/internal/logging/manager.go b/internal/logging/manager.go new file mode 100644 index 000000000..e8e96520b --- /dev/null +++ b/internal/logging/manager.go @@ -0,0 +1,48 @@ +package logging + +import ( + "context" + "sync" +) + +// Manager handles logging management +type Manager struct { + service Service + mu sync.RWMutex +} + +// Global instance of the logging manager +var globalManager *Manager + +// InitManager initializes the global logging manager with the provided service +func InitManager(service Service) { + globalManager = &Manager{ + service: service, + } + + // Subscribe to log events if needed + go func() { + ctx := context.Background() + _ = service.Subscribe(ctx) // Just subscribing to keep the channel open + }() +} + +// GetService returns the logging service +func GetService() Service { + if globalManager == nil { + return nil + } + + globalManager.mu.RLock() + defer globalManager.mu.RUnlock() + + return globalManager.service +} + +func Create(ctx context.Context, log Log) error { + if globalManager == nil { + return nil + } + return globalManager.service.Create(ctx, log) +} + diff --git a/internal/logging/message.go b/internal/logging/message.go deleted file mode 100644 index b8a42d966..000000000 --- a/internal/logging/message.go +++ /dev/null @@ -1,19 +0,0 @@ -package logging - -import ( - "time" -) - -// LogMessage is the event payload for a log message -type LogMessage struct { - ID string - Time time.Time - Level string - Message string `json:"msg"` - Attributes []Attr -} - -type Attr struct { - Key string - Value string -} diff --git a/internal/logging/service.go b/internal/logging/service.go new file mode 100644 index 000000000..8cf8039de --- /dev/null +++ b/internal/logging/service.go @@ -0,0 +1,167 @@ +package logging + +import ( + "context" + "database/sql" + "encoding/json" + "time" + + "github.com/google/uuid" + "github.com/opencode-ai/opencode/internal/db" + "github.com/opencode-ai/opencode/internal/pubsub" +) + +// Log represents a log entry in the system +type Log struct { + ID string + SessionID string + Timestamp int64 + Level string + Message string + Attributes map[string]string + CreatedAt int64 +} + +// Service defines the interface for log operations +type Service interface { + pubsub.Suscriber[Log] + Create(ctx context.Context, log Log) error + ListBySession(ctx context.Context, sessionID string) ([]Log, error) + ListAll(ctx context.Context, limit int) ([]Log, error) +} + +// service implements the Service interface +type service struct { + *pubsub.Broker[Log] + q db.Querier +} + +// NewService creates a new logging service +func NewService(q db.Querier) Service { + broker := pubsub.NewBroker[Log]() + return &service{ + Broker: broker, + q: q, + } +} + +// Create adds a new log entry to the database +func (s *service) Create(ctx context.Context, log Log) error { + // Generate ID if not provided + if log.ID == "" { + log.ID = uuid.New().String() + } + + // Set timestamp if not provided + if log.Timestamp == 0 { + log.Timestamp = time.Now().Unix() + } + + // Set created_at if not provided + if log.CreatedAt == 0 { + log.CreatedAt = time.Now().Unix() + } + + // Convert attributes to JSON string + var attributesJSON sql.NullString + if len(log.Attributes) > 0 { + attributesBytes, err := json.Marshal(log.Attributes) + if err != nil { + return err + } + attributesJSON = sql.NullString{ + String: string(attributesBytes), + Valid: true, + } + } + + // Convert session ID to SQL nullable string + var sessionID sql.NullString + if log.SessionID != "" { + sessionID = sql.NullString{ + String: log.SessionID, + Valid: true, + } + } + + // Insert log into database + err := s.q.CreateLog(ctx, db.CreateLogParams{ + ID: log.ID, + SessionID: sessionID, + Timestamp: log.Timestamp, + Level: log.Level, + Message: log.Message, + Attributes: attributesJSON, + CreatedAt: log.CreatedAt, + }) + + if err != nil { + return err + } + + // Publish event + s.Publish(pubsub.CreatedEvent, log) + return nil +} + +// ListBySession retrieves logs for a specific session +func (s *service) ListBySession(ctx context.Context, sessionID string) ([]Log, error) { + dbLogs, err := s.q.ListLogsBySession(ctx, sql.NullString{ + String: sessionID, + Valid: true, + }) + if err != nil { + return nil, err + } + + logs := make([]Log, len(dbLogs)) + for i, dbLog := range dbLogs { + logs[i] = s.fromDBItem(dbLog) + } + return logs, nil +} + +// ListAll retrieves all logs with a limit +func (s *service) ListAll(ctx context.Context, limit int) ([]Log, error) { + dbLogs, err := s.q.ListAllLogs(ctx, int64(limit)) + if err != nil { + return nil, err + } + + logs := make([]Log, len(dbLogs)) + for i, dbLog := range dbLogs { + logs[i] = s.fromDBItem(dbLog) + } + return logs, nil +} + +// fromDBItem converts a database log item to a Log struct +func (s *service) fromDBItem(item db.Log) Log { + log := Log{ + ID: item.ID, + Timestamp: item.Timestamp, + Level: item.Level, + Message: item.Message, + CreatedAt: item.CreatedAt, + } + + // Convert session ID if valid + if item.SessionID.Valid { + log.SessionID = item.SessionID.String + } + + // Parse attributes JSON if present + if item.Attributes.Valid { + attributes := make(map[string]string) + if err := json.Unmarshal([]byte(item.Attributes.String), &attributes); err == nil { + log.Attributes = attributes + } else { + // Initialize empty map if parsing fails + log.Attributes = make(map[string]string) + } + } else { + log.Attributes = make(map[string]string) + } + + return log +} diff --git a/internal/logging/writer.go b/internal/logging/writer.go index 7191f7720..4b5bcc4fe 100644 --- a/internal/logging/writer.go +++ b/internal/logging/writer.go @@ -5,59 +5,19 @@ import ( "context" "fmt" "strings" - "sync" "time" "github.com/go-logfmt/logfmt" - "github.com/opencode-ai/opencode/internal/pubsub" + "github.com/opencode-ai/opencode/internal/session" ) -const ( - // Maximum number of log messages to keep in memory - maxLogMessages = 1000 -) - -type LogData struct { - messages []LogMessage - *pubsub.Broker[LogMessage] - lock sync.Mutex -} - -func (l *LogData) Add(msg LogMessage) { - l.lock.Lock() - defer l.lock.Unlock() - - // Add new message - l.messages = append(l.messages, msg) - - // Trim if exceeding max capacity - if len(l.messages) > maxLogMessages { - l.messages = l.messages[len(l.messages)-maxLogMessages:] - } - - l.Publish(pubsub.CreatedEvent, msg) -} - -func (l *LogData) List() []LogMessage { - l.lock.Lock() - defer l.lock.Unlock() - return l.messages -} - -var defaultLogData = &LogData{ - messages: make([]LogMessage, 0, maxLogMessages), - Broker: pubsub.NewBroker[LogMessage](), -} - type writer struct{} func (w *writer) Write(p []byte) (int, error) { d := logfmt.NewDecoder(bytes.NewReader(p)) for d.ScanRecord() { - msg := LogMessage{ - ID: fmt.Sprintf("%d", time.Now().UnixNano()), - Time: time.Now(), - } + msg := Log{} + for d.ScanKeyval() { switch string(d.Key()) { case "time": @@ -65,19 +25,21 @@ func (w *writer) Write(p []byte) (int, error) { if err != nil { return 0, fmt.Errorf("parsing time: %w", err) } - msg.Time = parsed + msg.Timestamp = parsed.UnixMilli() case "level": msg.Level = strings.ToLower(string(d.Value())) case "msg": msg.Message = string(d.Value()) default: - msg.Attributes = append(msg.Attributes, Attr{ - Key: string(d.Key()), - Value: string(d.Value()), - }) + if msg.Attributes == nil { + msg.Attributes = make(map[string]string) + } + msg.Attributes[string(d.Key())] = string(d.Value()) } } - defaultLogData.Add(msg) + + msg.SessionID = session.CurrentSessionID() + Create(context.Background(), msg) } if d.Err() != nil { return 0, d.Err() @@ -89,11 +51,3 @@ func NewWriter() *writer { w := &writer{} return w } - -func Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] { - return defaultLogData.Subscribe(ctx) -} - -func List() []LogMessage { - return defaultLogData.List() -} -- cgit v1.2.3