mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-14 05:18:54 +00:00
feat(target-health): track MCP and agent target health
Add a per-replica, in-memory health tracker for MCP servers and A2A agents (target_health.go). The tracker records observations from real proxied traffic (handleTargetProxy, mcp_a2a.go) and federated calls (mcpFederatedToolsList/mcpFederatedToolsCall, mcp_federation.go), and derives a three-state readout (unknown, healthy, unhealthy) from a configurable consecutive-failure threshold. Add TargetHealthConfig to Config, validated at construction. The tracker never gates routing: federation and the target proxy keep contacting every configured server regardless of its recorded health. Extend proxyResult (routes_passthrough.go) with status and clientCanceled so handleTargetProxy can classify a proxied response without proxyUpstream knowing anything about target health. Add maybeSweepTargetHealth, an opt-in, lazy, single-flight active probe sweep with no timer and no long-lived goroutine, matching this plugin's lack of a Close hook under Traefik hot reload. A probe uses the initialize/close handshake for a Streamable HTTP MCP server, a plain unread GET for a legacy /sse server, and a GET of the agent card path for an A2A agent. handleMCPServers and handleAgents now trigger a sweep at the top of each call. Claude-Session: https://claude.ai/code/session_01EebV5jyWvYocByNQ3oss1w
This commit is contained in:
+11
-1
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Target kinds handleTargetProxy and resolveTarget dispatch on, selecting
|
||||
@@ -63,6 +64,7 @@ type agentListing struct {
|
||||
// servers, filtered by group.allowsMCP and sorted by name for a
|
||||
// deterministic response.
|
||||
func (g *Gateway) handleMCPServers(w http.ResponseWriter, grp *group) {
|
||||
g.maybeSweepTargetHealth()
|
||||
names := make([]string, 0, len(g.cfg.MCPServers))
|
||||
for name := range g.cfg.MCPServers {
|
||||
if grp.allowsMCP(name) {
|
||||
@@ -86,6 +88,7 @@ func (g *Gateway) handleMCPServers(w http.ResponseWriter, grp *group) {
|
||||
// proxy URL plus its configured card path, defaulting to
|
||||
// defaultAgentCardPath when AgentConfig.Card is empty.
|
||||
func (g *Gateway) handleAgents(w http.ResponseWriter, grp *group) {
|
||||
g.maybeSweepTargetHealth()
|
||||
names := make([]string, 0, len(g.cfg.Agents))
|
||||
for name := range g.cfg.Agents {
|
||||
if grp.allowsAgent(name) {
|
||||
@@ -216,7 +219,14 @@ func (g *Gateway) handleTargetProxy(w http.ResponseWriter, r *http.Request, u *u
|
||||
upstreamURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
g.proxyUpstream(w, r, upstreamURL, g.targetClient, nil, nil, false, kind+" target (name "+name+")", g.targetTimeout)
|
||||
// feat/target-health: passive recording (target_health.go) — latency
|
||||
// measured around the whole proxyUpstream call, matching what an
|
||||
// active probe of the same target measures. See
|
||||
// recordTargetProxyHealth's own doc comment for the exact
|
||||
// success/failure rule.
|
||||
start := time.Now()
|
||||
result, ok := g.proxyUpstream(w, r, upstreamURL, g.targetClient, nil, nil, false, kind+" target (name "+name+")", g.targetTimeout)
|
||||
g.recordTargetProxyHealth(kind, name, result, ok, time.Since(start))
|
||||
}
|
||||
|
||||
// validateTargetURLs checks every configured MCP-server and agent entry at
|
||||
|
||||
Reference in New Issue
Block a user