feat(observability): add MCP tool name logging in access log
Some checks failed
CI / build-and-test (push) Failing after -32m45s
Some checks failed
CI / build-and-test (push) Failing after -32m45s
* Include tool name from request in access log entries * Update user agent header in HTTP requests * Add tests for MCP tool name logging
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -15,6 +19,7 @@ import (
|
||||
type contextKey string
|
||||
|
||||
const requestIDContextKey contextKey = "request_id"
|
||||
const mcpToolContextKey contextKey = "mcp_tool"
|
||||
|
||||
func Chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
@@ -58,18 +63,26 @@ func Recover(log *slog.Logger) func(http.Handler) http.Handler {
|
||||
func AccessLog(log *slog.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if tool := mcpToolFromRequest(r); tool != "" {
|
||||
r = r.WithContext(context.WithValue(r.Context(), mcpToolContextKey, tool))
|
||||
}
|
||||
|
||||
recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
||||
started := time.Now()
|
||||
next.ServeHTTP(recorder, r)
|
||||
|
||||
log.Info("http request",
|
||||
attrs := []any{
|
||||
slog.String("request_id", RequestIDFromContext(r.Context())),
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.Int("status", recorder.status),
|
||||
slog.Duration("duration", time.Since(started)),
|
||||
slog.String("remote_addr", requestip.FromRequest(r)),
|
||||
)
|
||||
}
|
||||
if tool, _ := r.Context().Value(mcpToolContextKey).(string); strings.TrimSpace(tool) != "" {
|
||||
attrs = append(attrs, slog.String("tool", tool))
|
||||
}
|
||||
log.Info("http request", attrs...)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -101,3 +114,52 @@ func (s *statusRecorder) WriteHeader(statusCode int) {
|
||||
s.status = statusCode
|
||||
s.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func mcpToolFromRequest(r *http.Request) string {
|
||||
if r == nil || r.Method != http.MethodPost || !strings.HasPrefix(r.URL.Path, "/mcp") || r.Body == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
r.Body = io.NopCloser(bytes.NewReader(raw))
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Support both single and batch JSON-RPC payloads.
|
||||
if strings.HasPrefix(strings.TrimSpace(string(raw)), "[") {
|
||||
var batch []rpcEnvelope
|
||||
if err := json.Unmarshal(raw, &batch); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, msg := range batch {
|
||||
if tool := msg.toolName(); tool != "" {
|
||||
return tool
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var msg rpcEnvelope
|
||||
if err := json.Unmarshal(raw, &msg); err != nil {
|
||||
return ""
|
||||
}
|
||||
return msg.toolName()
|
||||
}
|
||||
|
||||
type rpcEnvelope struct {
|
||||
Method string `json:"method"`
|
||||
Params struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"params"`
|
||||
}
|
||||
|
||||
func (m rpcEnvelope) toolName() string {
|
||||
if m.Method != "tools/call" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(m.Params.Name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user