summaryrefslogtreecommitdiffhomepage
path: root/internal/logging
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-09 13:37:13 -0500
committeradamdottv <[email protected]>2025-05-09 13:37:13 -0500
commitf1007771997bd0401516eda87a7e0ac92f269680 (patch)
treed26198d031516eaebcc885870b470925492d8775 /internal/logging
parentf41b7bbd0a0cc731fd7c471b7ee8b26f14a21755 (diff)
downloadopencode-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.go48
-rw-r--r--internal/logging/message.go19
-rw-r--r--internal/logging/service.go167
-rw-r--r--internal/logging/writer.go68
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()
-}