diff options
| author | adamdottv <[email protected]> | 2025-05-09 13:37:13 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-05-09 13:37:13 -0500 |
| commit | f1007771997bd0401516eda87a7e0ac92f269680 (patch) | |
| tree | d26198d031516eaebcc885870b470925492d8775 /internal/logging | |
| parent | f41b7bbd0a0cc731fd7c471b7ee8b26f14a21755 (diff) | |
| download | opencode-f1007771997bd0401516eda87a7e0ac92f269680.tar.gz opencode-f1007771997bd0401516eda87a7e0ac92f269680.zip | |
wip: logging improvements
Diffstat (limited to 'internal/logging')
| -rw-r--r-- | internal/logging/logging.go (renamed from internal/logging/logger.go) | 23 | ||||
| -rw-r--r-- | internal/logging/manager.go | 48 | ||||
| -rw-r--r-- | internal/logging/message.go | 19 | ||||
| -rw-r--r-- | internal/logging/service.go | 167 | ||||
| -rw-r--r-- | internal/logging/writer.go | 68 |
5 files changed, 230 insertions, 95 deletions
diff --git a/internal/logging/logger.go b/internal/logging/logging.go index 31462a542..af14a0ded 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logging.go @@ -10,22 +10,6 @@ import ( "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. @@ -33,7 +17,7 @@ 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) + slog.Error(errorMsg) status.Error(errorMsg) // Create a timestamped panic log file @@ -43,7 +27,7 @@ func RecoverPanic(name string, cleanup func()) { file, err := os.Create(filename) if err != nil { errMsg := fmt.Sprintf("Failed to create panic log: %v", err) - Error(errMsg) + slog.Error(errMsg) status.Error(errMsg) } else { defer file.Close() @@ -54,7 +38,7 @@ func RecoverPanic(name string, cleanup func()) { fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack()) infoMsg := fmt.Sprintf("Panic details written to %s", filename) - Info(infoMsg) + slog.Info(infoMsg) status.Info(infoMsg) } @@ -64,3 +48,4 @@ func RecoverPanic(name string, cleanup func()) { } } } + 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() -} |
