Merge pull request #56 from hobbyistlabs-coder/etl-llm-telemetry-10889982003714703245

feat: implement LLM provider telemetry using expvar
This commit is contained in:
hobbyistlabs-coder 2026-03-19 09:57:46 -04:00 committed by GitHub
commit e99a5ae7f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 27 additions and 1 deletions

View file

@ -9,7 +9,7 @@ This document tracks the tasks required to implement the "Ultimate Visibility" E
- [x] **Goroutine Tracking:** Implement a metric to track the number of active Goroutines. - [x] **Goroutine Tracking:** Implement a metric to track the number of active Goroutines.
- [x] **Memory Tracking:** Implement a metric to track heap allocation and GC pauses. - [x] **Memory Tracking:** Implement a metric to track heap allocation and GC pauses.
- [x] **AgentLoop Telemetry:** Add specific instrumentation to the `AgentLoop` (iteration duration, tool execution duration, failure counts). - [x] **AgentLoop Telemetry:** Add specific instrumentation to the `AgentLoop` (iteration duration, tool execution duration, failure counts).
- [ ] **LLM Provider Telemetry:** Track API call latency, token usage, and failover reasons for LLM providers. - [x] **LLM Provider Telemetry:** Track API call latency, token usage, and failover reasons for LLM providers.
- [ ] **API Gateway Telemetry:** Track request rates (RPS), latency percentiles, and error rates for HTTP and WebSocket endpoints. - [ ] **API Gateway Telemetry:** Track request rates (RPS), latency percentiles, and error rates for HTTP and WebSocket endpoints.
- [ ] **Tracing Instrumentation:** Introduce trace IDs at entry points (HTTP, WebSocket) and propagate them via context to track end-to-end execution flow. - [ ] **Tracing Instrumentation:** Introduce trace IDs at entry points (HTTP, WebSocket) and propagate them via context to track end-to-end execution flow.

View file

@ -2,11 +2,18 @@ package providers
import ( import (
"context" "context"
"expvar"
"fmt" "fmt"
"strings" "strings"
"time" "time"
) )
var (
metricsLLMProviderDuration = expvar.NewMap("llm_provider_duration_seconds")
metricsLLMProviderFailover = expvar.NewMap("llm_provider_failover_counts")
metricsLLMProviderTokenUsage = expvar.NewMap("llm_provider_token_usage")
)
// FallbackChain orchestrates model fallback across multiple candidates. // FallbackChain orchestrates model fallback across multiple candidates.
type FallbackChain struct { type FallbackChain struct {
cooldown *CooldownTracker cooldown *CooldownTracker
@ -139,8 +146,15 @@ func (fc *FallbackChain) Execute(
resp, err := run(ctx, candidate.Provider, candidate.Model) resp, err := run(ctx, candidate.Provider, candidate.Model)
elapsed := time.Since(start) elapsed := time.Since(start)
metricsLLMProviderDuration.AddFloat(candidate.Provider, elapsed.Seconds())
if err == nil { if err == nil {
// Success. // Success.
if resp != nil && resp.Usage != nil {
metricsLLMProviderTokenUsage.Add(candidate.Provider+"_prompt", int64(resp.Usage.PromptTokens))
metricsLLMProviderTokenUsage.Add(candidate.Provider+"_completion", int64(resp.Usage.CompletionTokens))
metricsLLMProviderTokenUsage.Add(candidate.Provider+"_total", int64(resp.Usage.TotalTokens))
}
fc.cooldown.MarkSuccess(candidate.Provider) fc.cooldown.MarkSuccess(candidate.Provider)
result.Response = resp result.Response = resp
result.Provider = candidate.Provider result.Provider = candidate.Provider
@ -170,10 +184,13 @@ func (fc *FallbackChain) Execute(
Error: err, Error: err,
Duration: elapsed, Duration: elapsed,
}) })
metricsLLMProviderFailover.Add(candidate.Provider+"_"+string(FailoverUnknown), 1)
return nil, fmt.Errorf("fallback: unclassified error from %s/%s: %w", return nil, fmt.Errorf("fallback: unclassified error from %s/%s: %w",
candidate.Provider, candidate.Model, err) candidate.Provider, candidate.Model, err)
} }
metricsLLMProviderFailover.Add(candidate.Provider+"_"+string(failErr.Reason), 1)
// Non-retriable error: abort immediately. // Non-retriable error: abort immediately.
if !failErr.IsRetriable() { if !failErr.IsRetriable() {
result.Attempts = append(result.Attempts, FallbackAttempt{ result.Attempts = append(result.Attempts, FallbackAttempt{
@ -231,7 +248,14 @@ func (fc *FallbackChain) ExecuteImage(
resp, err := run(ctx, candidate.Provider, candidate.Model) resp, err := run(ctx, candidate.Provider, candidate.Model)
elapsed := time.Since(start) elapsed := time.Since(start)
metricsLLMProviderDuration.AddFloat(candidate.Provider, elapsed.Seconds())
if err == nil { if err == nil {
if resp != nil && resp.Usage != nil {
metricsLLMProviderTokenUsage.Add(candidate.Provider+"_prompt", int64(resp.Usage.PromptTokens))
metricsLLMProviderTokenUsage.Add(candidate.Provider+"_completion", int64(resp.Usage.CompletionTokens))
metricsLLMProviderTokenUsage.Add(candidate.Provider+"_total", int64(resp.Usage.TotalTokens))
}
result.Response = resp result.Response = resp
result.Provider = candidate.Provider result.Provider = candidate.Provider
result.Model = candidate.Model result.Model = candidate.Model
@ -251,6 +275,7 @@ func (fc *FallbackChain) ExecuteImage(
// Image dimension/size errors are non-retriable. // Image dimension/size errors are non-retriable.
errMsg := strings.ToLower(err.Error()) errMsg := strings.ToLower(err.Error())
if IsImageDimensionError(errMsg) || IsImageSizeError(errMsg) { if IsImageDimensionError(errMsg) || IsImageSizeError(errMsg) {
metricsLLMProviderFailover.Add(candidate.Provider+"_"+string(FailoverFormat), 1)
result.Attempts = append(result.Attempts, FallbackAttempt{ result.Attempts = append(result.Attempts, FallbackAttempt{
Provider: candidate.Provider, Provider: candidate.Provider,
Model: candidate.Model, Model: candidate.Model,
@ -267,6 +292,7 @@ func (fc *FallbackChain) ExecuteImage(
} }
// Any other error: record and try next. // Any other error: record and try next.
metricsLLMProviderFailover.Add(candidate.Provider+"_"+string(FailoverUnknown), 1)
result.Attempts = append(result.Attempts, FallbackAttempt{ result.Attempts = append(result.Attempts, FallbackAttempt{
Provider: candidate.Provider, Provider: candidate.Provider,
Model: candidate.Model, Model: candidate.Model,