Skip to content

Commit 8d09f27

Browse files
Add structured logging infrastructure (#59)
Implement comprehensive structured logging using slog across agent runtime, task reconciliation, and model providers with: • New logging layer: agent/logger.go with standardized log key constants • Context propagation: agent/context.go for task/agent/model tracking • Model providers: OpenAI, Anthropic, Gemini with operation telemetry • Task reconciliation: Detailed logging of reconciliation phases and tool execution • Filesystem tools: Logging for file operations and search results • Code interpreter: Execution timing and output tracking • Runtime lifecycle: Component initialization and shutdown logging Key features: - Centralized log key constants for consistency (KeyTaskID, KeyModel, etc.) - Operation timing with LogOperationStart/End helpers - Token usage logging with cache hit ratio calculations - Retryable error logging with retry-after timing This enables better observability into task execution, model invocations, and tool operations for debugging and performance monitoring. Co-authored-by: construct-agent <noreply@construct.sh>
1 parent 206b6d8 commit 8d09f27

13 files changed

Lines changed: 656 additions & 64 deletions

File tree

backend/agent/client.go

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"log/slog"
78

89
"github.com/furisto/construct/backend/memory"
910
"github.com/furisto/construct/backend/memory/schema/types"
@@ -27,55 +28,61 @@ func NewModelProviderFactory(encryption *secret.Encryption, memory *memory.Clien
2728
func (f *ModelProviderFactory) CreateClient(
2829
ctx context.Context,
2930
modelProviderID uuid.UUID,
30-
) (model.ModelProvider, error) {
31+
) (providerClient model.ModelProvider, err error) {
32+
logger := slog.With(
33+
KeyComponent, "model_provider_factory",
34+
KeyModelProvider, modelProviderID,
35+
)
36+
3137
provider, err := f.memory.ModelProvider.Get(ctx, modelProviderID)
3238
if err != nil {
39+
LogError(logger, "fetch model provider", err)
3340
return nil, fmt.Errorf("failed to fetch model provider: %w", err)
3441
}
42+
logger = logger.With(KeyProvider, string(provider.ProviderType))
3543

3644
providerAuth, err := f.encryption.Decrypt(provider.Secret, []byte(secret.ModelProviderAssociated(provider.ID)))
3745
if err != nil {
46+
LogError(logger, "decrypt model provider secret", err)
3847
return nil, fmt.Errorf("failed to decrypt model provider secret: %w", err)
3948
}
49+
logger.Debug("model provider secret decrypted")
4050

4151
var auth struct {
4252
APIKey string `json:"apiKey"`
4353
}
54+
4455
err = json.Unmarshal(providerAuth, &auth)
4556
if err != nil {
57+
LogError(logger, "unmarshal model provider auth", err)
4658
return nil, fmt.Errorf("failed to unmarshal model provider auth: %w", err)
4759
}
4860

61+
logger.Debug("creating model provider client")
4962
switch provider.ProviderType {
5063
case types.ModelProviderTypeAnthropic:
51-
apiProvider, err := model.NewAnthropicProvider(auth.APIKey)
52-
if err != nil {
53-
return nil, fmt.Errorf("failed to create Anthropic provider: %w", err)
54-
}
55-
return apiProvider, nil
64+
providerClient, err = model.NewAnthropicProvider(auth.APIKey)
5665

5766
case types.ModelProviderTypeOpenAI:
58-
apiProvider, err := model.NewOpenAICompletionProvider(auth.APIKey)
59-
if err != nil {
60-
return nil, fmt.Errorf("failed to create OpenAI provider: %w", err)
61-
}
62-
return apiProvider, nil
67+
providerClient, err = model.NewOpenAICompletionProvider(auth.APIKey)
6368

6469
case types.ModelProviderTypeGemini:
65-
apiProvider, err := model.NewGeminiProvider(auth.APIKey)
66-
if err != nil {
67-
return nil, fmt.Errorf("failed to create Gemini provider: %w", err)
68-
}
69-
return apiProvider, nil
70+
providerClient, err = model.NewGeminiProvider(auth.APIKey)
7071

7172
case types.ModelProviderTypeXAI:
72-
apiProvider, err := model.NewOpenAICompletionProvider(auth.APIKey, model.WithURL("https://api.xai.com/v1"))
73-
if err != nil {
74-
return nil, fmt.Errorf("failed to create XAI provider: %w", err)
75-
}
76-
return apiProvider, nil
73+
providerClient, err = model.NewOpenAICompletionProvider(auth.APIKey, model.WithURL("https://api.xai.com/v1"))
7774

7875
default:
76+
logger.Error("unknown model provider type",
77+
KeyProvider, string(provider.ProviderType),
78+
)
7979
return nil, fmt.Errorf("unknown model provider type: %s", provider.ProviderType)
8080
}
81+
82+
if err != nil {
83+
LogError(logger, "create provider", err)
84+
return nil, fmt.Errorf("failed to create Anthropic provider: %w", err)
85+
}
86+
87+
return providerClient, nil
8188
}

backend/agent/logger.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package agent
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"time"
7+
)
8+
9+
// Log key constants for consistency across the codebase
10+
const (
11+
// Task and agent identifiers
12+
KeyTaskID = "task_id"
13+
KeyAgentID = "agent_id"
14+
KeyMessageID = "message_id"
15+
16+
// Model and provider information
17+
KeyModel = "model"
18+
KeyProvider = "provider"
19+
KeyModelProvider = "model_provider"
20+
21+
// Timing and performance
22+
KeyDuration = "duration_ms"
23+
KeyStartTime = "start_time"
24+
KeyEndTime = "end_time"
25+
KeyLatency = "latency_ms"
26+
KeyThroughput = "throughput"
27+
28+
// Token and cost tracking
29+
KeyInputTokens = "input_tokens"
30+
KeyOutputTokens = "output_tokens"
31+
KeyTotalTokens = "total_tokens"
32+
KeyCacheWriteTokens = "cache_write_tokens"
33+
KeyCacheReadTokens = "cache_read_tokens"
34+
KeyCost = "cost"
35+
KeyTokensPerSecond = "tokens_per_second"
36+
KeyCacheHitRatio = "cache_hit_ratio"
37+
38+
// Tool and execution information
39+
KeyTool = "tool"
40+
KeyToolCount = "tool_count"
41+
KeyToolName = "tool_name"
42+
KeyToolDuration = "tool_duration_ms"
43+
KeyToolResult = "tool_result"
44+
KeyToolStats = "tool_stats"
45+
46+
// Message and queue information
47+
KeyMessageCount = "message_count"
48+
KeyProcessedCount = "processed_count"
49+
KeyUnprocessedCount = "unprocessed_count"
50+
KeyQueueDepth = "queue_depth"
51+
KeyActiveTaskCount = "active_task_count"
52+
KeyPhase = "phase"
53+
KeyNextPhase = "next_phase"
54+
55+
// Configuration and environment
56+
KeyConcurrency = "concurrency"
57+
KeyToolsRegistered = "tools_registered"
58+
KeyListenerAddress = "listener_address"
59+
KeyListenerType = "listener_type"
60+
61+
// Error and retry information
62+
KeyRetryable = "retryable"
63+
KeyRetryAfter = "retry_after_ms"
64+
KeyRetryCount = "retry_count"
65+
KeyErrorType = "error_type"
66+
KeyError = "error"
67+
68+
// Operations and components
69+
KeyOperation = "operation"
70+
KeyComponent = "component"
71+
)
72+
73+
// LoggerConfig holds logging configuration
74+
type LoggerConfig struct {
75+
Level slog.Level
76+
VerboseTools bool
77+
VerboseModels bool
78+
}
79+
80+
// DefaultLoggerConfig returns default logging configuration
81+
func DefaultLoggerConfig() *LoggerConfig {
82+
return &LoggerConfig{
83+
Level: slog.LevelInfo,
84+
VerboseTools: false,
85+
VerboseModels: false,
86+
}
87+
}
88+
89+
// LogOperationStart logs the start of a significant operation
90+
func LogOperationStart(logger *slog.Logger, operation string, keyValues ...any) {
91+
args := append([]any{KeyOperation, operation}, keyValues...)
92+
logger.Debug(fmt.Sprintf("%s started", operation), args...)
93+
}
94+
95+
// LogOperationEnd logs the end of a significant operation
96+
func LogOperationEnd(logger *slog.Logger, operation string, start time.Time, keyValues ...any) {
97+
args := append(
98+
[]any{
99+
KeyOperation, operation,
100+
KeyDuration, time.Since(start).Milliseconds(),
101+
},
102+
keyValues...,
103+
)
104+
logger.Debug(fmt.Sprintf("%s completed", operation), args...)
105+
}
106+
107+
// LogTokenUsage logs token usage from an API call
108+
func LogTokenUsage(logger *slog.Logger, level slog.Level, inputTokens, outputTokens, cacheWrite, cacheRead int64, cost float64, duration time.Duration, keyValues ...any) {
109+
totalTokens := inputTokens + outputTokens + cacheWrite + cacheRead
110+
tokensPerSec := 0.0
111+
if duration.Seconds() > 0 {
112+
tokensPerSec = float64(outputTokens) / duration.Seconds()
113+
}
114+
cacheHitRatio := 0.0
115+
if inputTokens+cacheRead > 0 {
116+
cacheHitRatio = float64(cacheRead) / float64(inputTokens+cacheRead)
117+
}
118+
119+
args := append(
120+
[]any{
121+
KeyInputTokens, inputTokens,
122+
KeyOutputTokens, outputTokens,
123+
KeyTotalTokens, totalTokens,
124+
KeyCacheWriteTokens, cacheWrite,
125+
KeyCacheReadTokens, cacheRead,
126+
KeyCost, fmt.Sprintf("$%.6f", cost),
127+
KeyTokensPerSecond, fmt.Sprintf("%.2f", tokensPerSec),
128+
KeyCacheHitRatio, fmt.Sprintf("%.1f%%", cacheHitRatio*100),
129+
KeyDuration, duration.Milliseconds(),
130+
},
131+
keyValues...,
132+
)
133+
logger.Log(nil, level, "token usage", args...)
134+
}
135+
136+
// LogPhaseTransition logs task phase transitions
137+
func LogPhaseTransition(logger *slog.Logger, taskID string, from, to string, keyValues ...any) {
138+
args := append(
139+
[]any{
140+
KeyTaskID, taskID,
141+
"from_phase", from,
142+
"to_phase", to,
143+
},
144+
keyValues...,
145+
)
146+
logger.Debug("task phase transition", args...)
147+
}
148+
149+
// LogComponentStartup logs component initialization
150+
func LogComponentStartup(logger *slog.Logger, component string, keyValues ...any) {
151+
args := append(
152+
[]any{KeyComponent, component},
153+
keyValues...,
154+
)
155+
logger.Info(fmt.Sprintf("%s initializing", component), args...)
156+
}
157+
158+
// LogComponentShutdown logs component shutdown
159+
func LogComponentShutdown(logger *slog.Logger, component string, shutdownStart time.Time, keyValues ...any) {
160+
args := append(
161+
[]any{
162+
KeyComponent, component,
163+
KeyDuration, time.Since(shutdownStart).Milliseconds(),
164+
},
165+
keyValues...,
166+
)
167+
logger.Info(fmt.Sprintf("%s shutdown complete", component), args...)
168+
}
169+
170+
// LogError logs an error with structured context
171+
func LogError(logger *slog.Logger, operation string, err error, keyValues ...any) {
172+
args := append(
173+
[]any{
174+
KeyOperation, operation,
175+
KeyError, err.Error(),
176+
},
177+
keyValues...,
178+
)
179+
logger.Error(fmt.Sprintf("failed: %s", operation), args...)
180+
}

0 commit comments

Comments
 (0)