mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-19 05:47:48 +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:
+24
-5
@@ -910,8 +910,27 @@ func (g *Gateway) handlePassthrough(w http.ResponseWriter, r *http.Request, u *u
|
||||
// nil unless accountJSON was true and the response's Content-Type was
|
||||
// application/json.
|
||||
type proxyResult struct {
|
||||
tee *cappedAccountingBuffer
|
||||
tee *cappedAccountingBuffer
|
||||
// status is the upstream response's own status code, once headers
|
||||
// were actually written to w — 0 when proxyUpstream returned before
|
||||
// ever writing them (a build/connection failure, or a client cancel
|
||||
// before the request was even sent). Additive field for
|
||||
// feat/target-health's passive recording (recordTargetProxyHealth,
|
||||
// target_health.go): ok=false alone conflates "the upstream never
|
||||
// answered at all" with "it answered with a 5xx", which that
|
||||
// feature's failure rule needs to distinguish. Field order (pointer,
|
||||
// then int, then the two bools) is fieldalignment-sensitive, the same
|
||||
// convention this package's other structs already follow.
|
||||
status int
|
||||
isJSON bool
|
||||
// clientCanceled is true exactly when either of proxyUpstream's own
|
||||
// errors.Is(err, context.Canceled) branches fired — the CLIENT, not
|
||||
// the upstream, ended the request, whether before the upstream call
|
||||
// was even sent or mid-copy of an already-started response.
|
||||
// feat/target-health's passive recording must record NOTHING for
|
||||
// this case (handleTargetProxy's own doc comment, mcp_a2a.go): a
|
||||
// client hanging up says nothing about the target's own health.
|
||||
clientCanceled bool
|
||||
}
|
||||
|
||||
// proxyUpstream is the shared reverse-proxy core behind both native
|
||||
@@ -1054,7 +1073,7 @@ func (g *Gateway) proxyUpstream(w http.ResponseWriter, r *http.Request, upstream
|
||||
cancel()
|
||||
if errors.Is(err, context.Canceled) {
|
||||
g.logf("%s: client canceled request: %v", logPrefix, err)
|
||||
return proxyResult{}, false
|
||||
return proxyResult{clientCanceled: true}, false
|
||||
}
|
||||
g.errorf("%s: upstream connection error: %v", logPrefix, err)
|
||||
writeOAIError(w, http.StatusBadGateway, "server_error", "upstream connection error")
|
||||
@@ -1099,10 +1118,10 @@ func (g *Gateway) proxyUpstream(w http.ResponseWriter, r *http.Request, upstream
|
||||
// — the latter falls through to the errorf below, unchanged.
|
||||
if errors.Is(err, context.Canceled) {
|
||||
g.logf("%s: client canceled request: %v", logPrefix, err)
|
||||
return proxyResult{isJSON: isJSON, tee: tee}, false
|
||||
return proxyResult{isJSON: isJSON, tee: tee, status: resp.StatusCode, clientCanceled: true}, false
|
||||
}
|
||||
g.errorf("%s: stream response body: %v", logPrefix, err)
|
||||
return proxyResult{isJSON: isJSON, tee: tee}, false
|
||||
return proxyResult{isJSON: isJSON, tee: tee, status: resp.StatusCode}, false
|
||||
}
|
||||
return proxyResult{isJSON: isJSON, tee: tee}, true
|
||||
return proxyResult{isJSON: isJSON, tee: tee, status: resp.StatusCode}, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user