Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions daemon/ccotel_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package daemon
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strconv"
"time"

"github.com/google/uuid"
"github.com/malamtime/cli/model"
Expand All @@ -22,6 +25,7 @@ type CCOtelProcessor struct {
config model.ShellTimeConfig
endpoint model.Endpoint
hostname string
debug bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To prevent race conditions when writing to debug files from concurrent goroutines, a mutex should be added to this struct. This will be used in the writeDebugFile function to ensure thread-safe file access. You will also need to add import "sync" to the file's import block.

	debug    bool
	debugMtx sync.Mutex

}

// NewCCOtelProcessor creates a new CCOtel processor
Expand All @@ -31,20 +35,55 @@ func NewCCOtelProcessor(config model.ShellTimeConfig) *CCOtelProcessor {
hostname = "unknown"
}

debug := config.CCOtel != nil && config.CCOtel.Debug != nil && *config.CCOtel.Debug

return &CCOtelProcessor{
config: config,
endpoint: model.Endpoint{
Token: config.Token,
APIEndpoint: config.APIEndpoint,
},
hostname: hostname,
debug: debug,
}
}

// writeDebugFile appends JSON-formatted data to a debug file
func (p *CCOtelProcessor) writeDebugFile(filename string, data interface{}) {
debugDir := filepath.Join(os.TempDir(), "shelltime")
if err := os.MkdirAll(debugDir, 0755); err != nil {
slog.Error("CCOtel: Failed to create debug directory", "error", err)
return
}

filePath := filepath.Join(debugDir, filename)
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
slog.Error("CCOtel: Failed to open debug file", "error", err, "path", filePath)
return
}
defer f.Close()

jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
slog.Error("CCOtel: Failed to marshal debug data", "error", err)
return
}

timestamp := time.Now().Format(time.RFC3339)
if _, err := f.WriteString(fmt.Sprintf("\n--- %s ---\n%s\n", timestamp, jsonData)); err != nil {
slog.Error("CCOtel: Failed to write debug data", "error", err)
}
}
Comment on lines +52 to 77
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function writes to a file but doesn't handle concurrent access, which can lead to race conditions and corrupted debug logs since ProcessMetrics and ProcessLogs can be called from multiple goroutines. To ensure thread safety, the file writing operations should be synchronized using a mutex.

Additionally, using fmt.Sprintf with f.WriteString can be inefficient for large JSON payloads as it allocates a single large string. Using fmt.Fprintf is more memory-efficient.

The suggested change below incorporates both thread safety with a mutex and more efficient file writing. Note that this change depends on another suggestion to add the debugMtx field to the CCOtelProcessor struct.

func (p *CCOtelProcessor) writeDebugFile(filename string, data interface{}) {
	p.debugMtx.Lock()
	defer p.debugMtx.Unlock()

	debugDir := filepath.Join(os.TempDir(), "shelltime")
	if err := os.MkdirAll(debugDir, 0755); err != nil {
		slog.Error("CCOtel: Failed to create debug directory", "error", err)
		return
	}

	filePath := filepath.Join(debugDir, filename)
	f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		slog.Error("CCOtel: Failed to open debug file", "error", err, "path", filePath)
		return
	}
	defer f.Close()

	jsonData, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		slog.Error("CCOtel: Failed to marshal debug data", "error", err)
		return
	}

	timestamp := time.Now().Format(time.RFC3339)
	if _, err := fmt.Fprintf(f, "\n--- %s ---\n%s\n", timestamp, jsonData); err != nil {
		slog.Error("CCOtel: Failed to write debug data", "error", err)
	}
}


// ProcessMetrics receives OTEL metrics and forwards to backend immediately
func (p *CCOtelProcessor) ProcessMetrics(ctx context.Context, req *collmetricsv1.ExportMetricsServiceRequest) (*collmetricsv1.ExportMetricsServiceResponse, error) {
slog.Debug("CCOtel: Processing metrics request", "resourceMetricsCount", len(req.GetResourceMetrics()))

if p.debug {
p.writeDebugFile("ccotel-debug-metrics.txt", req)
}

for _, rm := range req.GetResourceMetrics() {
resource := rm.GetResource()

Expand Down Expand Up @@ -94,6 +133,10 @@ func (p *CCOtelProcessor) ProcessMetrics(ctx context.Context, req *collmetricsv1
func (p *CCOtelProcessor) ProcessLogs(ctx context.Context, req *collogsv1.ExportLogsServiceRequest) (*collogsv1.ExportLogsServiceResponse, error) {
slog.Debug("CCOtel: Processing logs request", "resourceLogsCount", len(req.GetResourceLogs()))

if p.debug {
p.writeDebugFile("ccotel-debug-logs.txt", req)
}

for _, rl := range req.GetResourceLogs() {
resource := rl.GetResource()

Expand Down
1 change: 1 addition & 0 deletions model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type CCUsage struct {
type CCOtel struct {
Enabled *bool `toml:"enabled"`
GRPCPort int `toml:"grpcPort"` // default: 4317
Debug *bool `toml:"debug"` // write raw JSON to debug files
}

// CodeTracking configuration for coding activity heartbeat tracking
Expand Down