mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-13 04:58:50 +00:00
fix(target-health): omit target metrics family when unconfigured
writeTargetMetrics now returns before emitting even llmgateway_target_healthy's own HELP/TYPE header when neither an MCP server nor an agent is configured, mirroring writeStoreHealthMetrics' identical behaviour when Redis is unconfigured. Claude-Session: https://claude.ai/code/session_01EebV5jyWvYocByNQ3oss1w
This commit is contained in:
@@ -1684,7 +1684,9 @@ plugin's config.
|
||||
- `llmgateway_target_healthy{kind,target}` — `1`/`0` gauge for an MCP
|
||||
server or A2A agent's tracked health; `kind` is `"mcp"` or `"agent"`.
|
||||
No sample at all for a target this tracker has never observed — see
|
||||
[MCP and A2A](#mcp-and-a2a)'s "Target health" section.
|
||||
[MCP and A2A](#mcp-and-a2a)'s "Target health" section. The whole
|
||||
family, HELP and TYPE lines included, is absent entirely when no MCP
|
||||
servers or agents are configured.
|
||||
- `llmgateway_limit_store_up` — `1`/`0` gauge for this replica's own
|
||||
connection to the configured limit store (Redis); absent entirely
|
||||
when no store is configured. Every store-backed family above simply
|
||||
|
||||
@@ -694,7 +694,15 @@ func (g *Gateway) writeProviderMetrics(m *metricWriter) {
|
||||
// unreadable". Iterates every CONFIGURED MCP server and agent (not just
|
||||
// ones the tracker happens to have an entry for), so a target removed
|
||||
// from config since its last observation contributes no stale series.
|
||||
// F7, feat/target-health review: returns before emitting even the family's
|
||||
// own HELP/TYPE header when no MCP server or agent is configured at all —
|
||||
// mirrors writeStoreHealthMetrics' identical "nothing to report, no family
|
||||
// either" early return above, rather than a HELP/TYPE line promising a
|
||||
// metric family that can never carry a sample in this deployment.
|
||||
func (g *Gateway) writeTargetMetrics(m *metricWriter) {
|
||||
if len(g.cfg.MCPServers) == 0 && len(g.cfg.Agents) == 0 {
|
||||
return
|
||||
}
|
||||
m.family("llmgateway_target_healthy", "gauge",
|
||||
"1 when an MCP server or A2A agent's health (last passive observation or active probe) is healthy, 0 when its consecutive-failure count has reached targetHealth.failureThreshold. No sample while never yet observed."+aggregationNoteTargetHealthy)
|
||||
|
||||
|
||||
@@ -1521,6 +1521,58 @@ func TestMetrics_TargetHealthy_NoSampleWhileUnknown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMetrics_TargetHealthy_NoSampleWhileNeverSucceeded proves F4's
|
||||
// second no-sample case: a target that HAS been observed, but has never
|
||||
// once succeeded and is still below failureThreshold, reads "unknown"
|
||||
// (TestTargetHealthTracker_FirstObservationFailure_ReadsUnknown) and
|
||||
// therefore still emits no llmgateway_target_healthy sample — the same
|
||||
// "a gap is honest, a fabricated value is not" rule the never-observed
|
||||
// case above applies, now extended to "observed but never succeeded".
|
||||
func TestMetrics_TargetHealthy_NoSampleWhileNeverSucceeded(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := newMetricsTestConfig()
|
||||
cfg.MCPServers = map[string]*TargetConfig{"alpha": {URL: "http://mcp-alpha.internal"}}
|
||||
h, gw := newMetricsGatewayHandle(t, cfg)
|
||||
|
||||
gw.targetHealth.record(targetKindMCP, "alpha", "", false, errors.New("boom"), time.Millisecond, targetHealthSourceProbe)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, metricsRequest(http.MethodGet, metricsPathDefault, "sk-admin1"))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
_, samples := parsePrometheusText(t, rec.Body.Bytes())
|
||||
for _, s := range samples {
|
||||
if s.name == "llmgateway_target_healthy" {
|
||||
t.Errorf("unexpected sample %v — a target that has never succeeded must emit nothing", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMetrics_TargetHealthy_FamilyAbsentWhenNoTargetsConfigured proves
|
||||
// F7: writeTargetMetrics omits even llmgateway_target_healthy's own
|
||||
// HELP/TYPE header when no MCP server or agent is configured at all —
|
||||
// mirroring writeStoreHealthMetrics' identical "nothing to report, no
|
||||
// family either" behavior when Redis is unconfigured.
|
||||
func TestMetrics_TargetHealthy_FamilyAbsentWhenNoTargetsConfigured(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := newMetricsTestConfig()
|
||||
h, _ := newMetricsGatewayHandle(t, cfg)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, metricsRequest(http.MethodGet, metricsPathDefault, "sk-admin1"))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
types, _ := parsePrometheusText(t, rec.Body.Bytes())
|
||||
if _, ok := types["llmgateway_target_healthy"]; ok {
|
||||
t.Error("want llmgateway_target_healthy's TYPE line absent when no MCP servers or agents are configured")
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "llmgateway_target_healthy") {
|
||||
t.Errorf("body unexpectedly mentions llmgateway_target_healthy: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestMetrics_TargetHealthy_ReflectsRecordedState proves the gauge
|
||||
// renders 1 for a healthy MCP server and 0 for an unhealthy agent, with
|
||||
// the documented kind label values ("mcp"/"agent" — NOT the internal
|
||||
|
||||
Reference in New Issue
Block a user