diff --git a/core/services/agentpool/agent_pool.go b/core/services/agentpool/agent_pool.go index 8744b8a35..21d3e2661 100644 --- a/core/services/agentpool/agent_pool.go +++ b/core/services/agentpool/agent_pool.go @@ -426,7 +426,15 @@ func (s *AgentPoolService) Chat(name, message string) (string, error) { // Process asynchronously go func() { + started := time.Now() response := ag.Ask(coreTypes.WithText(message)) + outcome := "completed" + if response == nil { + outcome = "cancelled" + } else if response.Error != nil { + outcome = "error" + } + recordAgentRun(name, outcome, time.Since(started).Seconds()) if response == nil { errMsg, _ := json.Marshal(map[string]any{ diff --git a/core/services/agentpool/metrics.go b/core/services/agentpool/metrics.go new file mode 100644 index 000000000..191da8780 --- /dev/null +++ b/core/services/agentpool/metrics.go @@ -0,0 +1,54 @@ +package agentpool + +import ( + "context" + "sync" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +// Prometheus metrics for agent chat runs. Operators need a scrape-friendly +// signal for "are agent turns completing, erroring or getting cancelled, +// and how long do they take" — log-derived counters proved brittle +// (ANSI/timezone parsing, container-restart gaps). Chat() is the single +// choke point of the local execution path, so instrumenting the response +// handoff covers UI chats, API chats and connector-triggered asks alike. +// +// Lazily initialised on first record so the package works no matter when +// (or whether) the Prometheus-backed global MeterProvider is installed — +// same pattern as core/services/routing/pii. +var ( + agentMetricsOnce sync.Once + runsCounter metric.Int64Counter + runSeconds metric.Float64Histogram +) + +func recordAgentRun(agent, outcome string, seconds float64) { + agentMetricsOnce.Do(func() { + meter := otel.Meter("github.com/mudler/LocalAI") + if c, err := meter.Int64Counter( + "localai_agent_runs_total", + metric.WithDescription("Agent chat runs, labeled by agent and outcome (completed|error|cancelled)"), + ); err == nil { + runsCounter = c + } + if h, err := meter.Float64Histogram( + "localai_agent_run_seconds", + metric.WithDescription("Wall-clock duration of agent chat runs in seconds"), + ); err == nil { + runSeconds = h + } + }) + attrs := metric.WithAttributes( + attribute.String("agent", agent), + attribute.String("outcome", outcome), + ) + if runsCounter != nil { + runsCounter.Add(context.Background(), 1, attrs) + } + if runSeconds != nil { + runSeconds.Record(context.Background(), seconds, attrs) + } +}