Skip to content

Commit 2ccc67b

Browse files
authored
feat(agents): native Prometheus metrics for agent chat runs (#10689)
Operators need a scrape-friendly signal for agent-turn health (completing, erroring, cancelled, duration) — log-derived counters proved brittle (ANSI/ timezone parsing, restart gaps). Adds localai_agent_runs_total{agent,outcome} and localai_agent_run_seconds histogram, recorded at the Chat() response handoff (single choke point of the local execution path). Lazy meter init, same pattern as the PII events counter (#10641). Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
1 parent 0a6c62b commit 2ccc67b

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

core/services/agentpool/agent_pool.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,15 @@ func (s *AgentPoolService) Chat(name, message string) (string, error) {
426426

427427
// Process asynchronously
428428
go func() {
429+
started := time.Now()
429430
response := ag.Ask(coreTypes.WithText(message))
431+
outcome := "completed"
432+
if response == nil {
433+
outcome = "cancelled"
434+
} else if response.Error != nil {
435+
outcome = "error"
436+
}
437+
recordAgentRun(name, outcome, time.Since(started).Seconds())
430438

431439
if response == nil {
432440
errMsg, _ := json.Marshal(map[string]any{

core/services/agentpool/metrics.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package agentpool
2+
3+
import (
4+
"context"
5+
"sync"
6+
7+
"go.opentelemetry.io/otel"
8+
"go.opentelemetry.io/otel/attribute"
9+
"go.opentelemetry.io/otel/metric"
10+
)
11+
12+
// Prometheus metrics for agent chat runs. Operators need a scrape-friendly
13+
// signal for "are agent turns completing, erroring or getting cancelled,
14+
// and how long do they take" — log-derived counters proved brittle
15+
// (ANSI/timezone parsing, container-restart gaps). Chat() is the single
16+
// choke point of the local execution path, so instrumenting the response
17+
// handoff covers UI chats, API chats and connector-triggered asks alike.
18+
//
19+
// Lazily initialised on first record so the package works no matter when
20+
// (or whether) the Prometheus-backed global MeterProvider is installed —
21+
// same pattern as core/services/routing/pii.
22+
var (
23+
agentMetricsOnce sync.Once
24+
runsCounter metric.Int64Counter
25+
runSeconds metric.Float64Histogram
26+
)
27+
28+
func recordAgentRun(agent, outcome string, seconds float64) {
29+
agentMetricsOnce.Do(func() {
30+
meter := otel.Meter("github.com/mudler/LocalAI")
31+
if c, err := meter.Int64Counter(
32+
"localai_agent_runs_total",
33+
metric.WithDescription("Agent chat runs, labeled by agent and outcome (completed|error|cancelled)"),
34+
); err == nil {
35+
runsCounter = c
36+
}
37+
if h, err := meter.Float64Histogram(
38+
"localai_agent_run_seconds",
39+
metric.WithDescription("Wall-clock duration of agent chat runs in seconds"),
40+
); err == nil {
41+
runSeconds = h
42+
}
43+
})
44+
attrs := metric.WithAttributes(
45+
attribute.String("agent", agent),
46+
attribute.String("outcome", outcome),
47+
)
48+
if runsCounter != nil {
49+
runsCounter.Add(context.Background(), 1, attrs)
50+
}
51+
if runSeconds != nil {
52+
runSeconds.Record(context.Background(), seconds, attrs)
53+
}
54+
}

0 commit comments

Comments
 (0)