-
Notifications
You must be signed in to change notification settings - Fork 121
Add tool error detection to telemetry middleware #2092
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -130,6 +130,7 @@ func (m *HTTPMiddleware) Handler(next http.Handler) http.Handler { | |
ResponseWriter: w, | ||
statusCode: http.StatusOK, | ||
bytesWritten: 0, | ||
isToolCall: mcpparser.GetMCPMethod(ctx) == string(mcp.MethodToolsCall), | ||
} | ||
|
||
// Add HTTP attributes | ||
|
@@ -147,6 +148,9 @@ func (m *HTTPMiddleware) Handler(next http.Handler) http.Handler { | |
// Call the next handler with the instrumented context | ||
next.ServeHTTP(rw, r.WithContext(ctx)) | ||
|
||
// Finalize tool error detection now that response is complete | ||
rw.finalizeToolErrorDetection() | ||
|
||
// Record completion metrics and finalize span | ||
duration := time.Since(startTime) | ||
m.finalizeSpan(span, rw, duration) | ||
|
@@ -390,19 +394,44 @@ func (*HTTPMiddleware) finalizeSpan(span trace.Span, rw *responseWriter, duratio | |
attribute.Float64("http.duration_ms", float64(duration.Nanoseconds())/1e6), | ||
) | ||
|
||
// Set span status based on HTTP status code | ||
// Add MCP tool error indicator if detected | ||
if rw.isToolCall { | ||
span.SetAttributes(attribute.Bool("mcp.tool.error", rw.hasToolError)) | ||
} | ||
|
||
// Set span status based on HTTP status code AND MCP tool errors | ||
if rw.statusCode >= 400 { | ||
span.SetStatus(codes.Error, fmt.Sprintf("HTTP %d", rw.statusCode)) | ||
} else if rw.hasToolError { | ||
span.SetStatus(codes.Error, "MCP tool execution error") | ||
} else { | ||
span.SetStatus(codes.Ok, "") | ||
} | ||
} | ||
|
||
// detectMCPToolError performs lightweight detection of MCP tool execution errors | ||
// Returns true if the response likely contains a tool execution error | ||
func detectMCPToolError(data []byte) bool { | ||
// Attempt to parse JSON and check for isError field | ||
var resp struct { | ||
Result struct { | ||
IsError bool `json:"isError"` | ||
} `json:"result"` | ||
} | ||
if err := json.Unmarshal(data, &resp); err != nil { | ||
return false | ||
} | ||
return resp.Result.IsError | ||
} | ||
|
||
// responseWriter wraps http.ResponseWriter to capture response details. | ||
type responseWriter struct { | ||
http.ResponseWriter | ||
statusCode int | ||
bytesWritten int64 | ||
statusCode int | ||
bytesWritten int64 | ||
hasToolError bool // tracks if MCP tool execution error is detected | ||
isToolCall bool // tracks if this is a tools/call request | ||
responseBuffer []byte // buffer to collect response data for tool calls | ||
} | ||
|
||
// WriteHeader captures the status code. | ||
|
@@ -411,13 +440,29 @@ func (rw *responseWriter) WriteHeader(statusCode int) { | |
rw.ResponseWriter.WriteHeader(statusCode) | ||
} | ||
|
||
// Write captures the number of bytes written. | ||
// Write captures the number of bytes written and buffers data for tool calls. | ||
func (rw *responseWriter) Write(data []byte) (int, error) { | ||
n, err := rw.ResponseWriter.Write(data) | ||
rw.bytesWritten += int64(n) | ||
|
||
// Buffer response data for tool calls to enable proper error detection | ||
if rw.isToolCall && !rw.hasToolError { | ||
rw.responseBuffer = append(rw.responseBuffer, data...) | ||
} | ||
|
||
return n, err | ||
} | ||
|
||
// finalizeToolErrorDetection performs error detection on the complete buffered response. | ||
// This should be called after the response is completely written. | ||
func (rw *responseWriter) finalizeToolErrorDetection() { | ||
if rw.isToolCall && !rw.hasToolError && len(rw.responseBuffer) > 0 { | ||
rw.hasToolError = detectMCPToolError(rw.responseBuffer) | ||
// Clear buffer to free memory | ||
rw.responseBuffer = nil | ||
} | ||
Comment on lines
+459
to
+463
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inefficient error detection: Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
} | ||
|
||
// recordMetrics records request metrics. | ||
func (m *HTTPMiddleware) recordMetrics(ctx context.Context, r *http.Request, rw *responseWriter, duration time.Duration) { | ||
// Get MCP method from context if available | ||
|
@@ -426,10 +471,12 @@ func (m *HTTPMiddleware) recordMetrics(ctx context.Context, r *http.Request, rw | |
mcpMethod = "unknown" | ||
} | ||
|
||
// Determine status (success/error) | ||
// Determine status (success/error/tool_error) | ||
status := "success" | ||
if rw.statusCode >= 400 { | ||
status = "error" | ||
} else if rw.hasToolError { | ||
status = "tool_error" | ||
} | ||
|
||
// Common attributes for all metrics | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unbounded memory usage: response buffer grows without limits for tool calls. Consider adding a maximum buffer size to prevent potential memory exhaustion on large responses.
Copilot uses AI. Check for mistakes.