【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Hooks 源码精讲:Hooks 可观测性的无侵入式实现
【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Hooks 源码精讲:Hooks 可观测性的无侵入式实现
https://github.com/VictoriaMetrics/mcp-victoriametrics/tree/v1.20.1
基于 mcp-victoriametrics v1.20.1 源码,从 SRE 运维核心诉求出发,拆解 Hooks 机制的设计本质、解决的运维痛点,以及在生产环境中如何通过 Hooks 实现 MCP Server 的全维度可观测性。
一、SRE 视角:为什么 MCP Server 必须有 Hooks?
作为 SRE,我们对服务的核心诉求是可观测性、可维护性、低侵入性—— 而 MCP(Model Context Protocol)Server 作为 AI Agent 与 VictoriaMetrics 交互的核心中间件,天然存在以下运维痛点:
VictoriaMetrics 团队设计的 Hooks 机制,本质是基于 切面编程(AOP) 思想的无侵入式扩展方案—— 在不修改 MCP Server 核心业务逻辑的前提下,通过 hooks 挂载监控、日志、追踪等横切逻辑,完美解决上述 SRE 痛点。
🔍 深入理解:AOP 思想如何映射到 Hooks 实现?
AOP(面向切面编程)有三个核心概念,在 mcp-victoriametrics 的 Hooks 机制中均有清晰的对应:
❶ 切点(Pointcut) —— MCP 请求生命周期的各个阶段
上游框架 mcp-go 的 server.Hooks 结构体预定义了所有切点:OnBeforeAny、OnAfterCallTool、OnError 等。每个切点对应请求处理流水线上的一个确定位置,业务代码不需要知道它们的存在。
❷ 通知 / 增强(Advice) —— hooks.go 中注册的回调函数
hooks.New(ms) 注册 metrics 采集逻辑(Counter 计数),hooks.NewLoggerHooks() 注册结构化日志逻辑。这些就是"横切关注点"——监控和日志跟任何一个具体的 Tool / Resource / Prompt 的业务逻辑都没有关系,但又需要在每个请求上生效。
❸ 织入(Weaving) —— hooks.Merge() + server.WithHooks()
main.go 中通过 Merge 将多组钩子的回调切片逐字段 append 合并,再通过 WithHooks(combinedHooks) 一次性注入 MCP Server。框架在处理每个请求时,会在对应切点自动遍历执行所有注册的回调。
📌 "无侵入"体现在哪?
main.go 中业务注册代码(tools.RegisterToolQuery(s, c) 等)和观测性代码(hooks.New(ms) 等)是完全平行的两条线,互不依赖。去掉日志只需把 Merge(metricsHooks, loggingHooks) 改为 Merge(metricsHooks);新增 tracing 只需写一个 NewTracingHooks() 加到 Merge 参数里——业务逻辑一行不用改。
⚡ 与传统 AOP(如 Java Spring AOP)的区别
传统 AOP 依赖动态代理或字节码增强,是运行时隐式织入。而 Hooks 是编译时显式注册——通过 Go 的函数切片 + 回调模式实现,更轻量、更透明,没有"魔法"。本质上是观察者模式(Observer Pattern)的变体,但从解决的问题域来看(横切关注点的分离),确实是 AOP 的思想。
本文将从源码层面,拆解 Hooks 如何满足 SRE 的核心诉求,以及生产环境中如何基于 Hooks 做运维落地。
二、Hooks 核心概念:SRE 必须理解的基础
2.1、什么是 Hooks?
在 mcp-victoriametrics 中,server.Hooks(vendor/github.com/mark3labs/mcp-go/server/hooks.go) 是 mcp-go 框架定义的生命周期回调容器,其核心结构是 事件类型→回调函数切片 的映射。要理解 mcp-victoriametrics 的钩子机制,首先要追溯到它的上游依赖。
server.Hooks 并非本项目定义,而是 mcp-go 框架(v0.45.0)中由 go generate 自动生成的核心结构体,位于 https://github.com/mark3labs/mcp-go/blob/main/server/hooks.go(上游框架仓库)。本项目的 Hooks 使用代码位于 https://github.com/VictoriaMetrics/mcp-victoriametrics/blob/v1.20.1/cmd/mcp-victoriametrics/hooks/hooks.go。它的设计思路很直接:每种 MCP 事件对应一个回调函数切片,支持按需注册多个钩子,在请求处理的各个阶段依次触发。
https://github.com/mark3labs/mcp-go/blob/main/server/hooks.go
// Code generated by `go generate`. DO NOT EDIT.
// 代码由go generate自动生成,禁止手动编辑
// source: server/internal/gen/hooks.go.tmpl
package server // 声明server包,包含MCP服务器核心逻辑
import (
"context" // 导入上下文包,用于请求生命周期管理
"encoding/json" // JSON编解码
"errors" // 错误处理基础包
"fmt" // 格式化输入输出
"github.com/mark3labs/mcp-go/mcp" // 导入MCP核心协议定义包
)
// OnRegisterSessionHookFunc 会话注册钩子函数类型
// 当新会话注册时调用该钩子
type OnRegisterSessionHookFunc func(ctx context.Context, session ClientSession)
// OnUnregisterSessionHookFunc 会话注销钩子函数类型
// 当会话被注销时调用该钩子
type OnUnregisterSessionHookFunc func(ctx context.Context, session ClientSession)
// BeforeAnyHookFunc 通用前置钩子函数类型
// 在请求解析完成后、具体方法执行前调用
type BeforeAnyHookFunc func(ctx context.Context, id any, method mcp.MCPMethod, message any)
// OnSuccessHookFunc 成功回调钩子函数类型
// 在请求成功生成结果后、返回给客户端前调用
type OnSuccessHookFunc func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any)
// OnErrorHookFunc 错误回调钩子函数类型
// 在请求解析或方法执行过程中发生错误时调用
//
// 使用示例:
// ```
// hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
// // 使用errors.Is检查特定错误类型
// if errors.Is(err, ErrUnsupported) {
// // 处理不支持的能力错误
// log.Printf("Capability not supported: %v", err)
// }
//
// // 使用errors.As获取具体错误类型
// var parseErr = &UnparsableMessageError{}
// if errors.As(err, &parseErr) {
// // 访问错误类型的特定方法/字段
// log.Printf("Failed to parse message for method %s: %v",
// parseErr.GetMethod(), parseErr.Unwrap())
// // 获取解析失败的原始消息
// rawMsg := parseErr.GetMessage()
// }
//
// // 检查资源/提示/工具相关错误
// switch {
// case errors.Is(err, ErrResourceNotFound):
// log.Printf("Resource not found: %v", err)
// case errors.Is(err, ErrPromptNotFound):
// log.Printf("Prompt not found: %v", err)
// case errors.Is(err, ErrToolNotFound):
// log.Printf("Tool not found: %v", err)
// }
// })
// ```
type OnErrorHookFunc func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error)
// OnRequestInitializationFunc 请求初始化钩子函数类型
// 在处理差异化请求方法之前调用
// 若函数执行过程中发生错误,服务会立即返回对应的错误信息
type OnRequestInitializationFunc func(ctx context.Context, id any, message any) error
// OnBeforeSamplingCreateMessageFunc 采样创建消息前置钩子类型
// 针对SamplingCreateMessage方法的专属前置钩子
type OnBeforeSamplingCreateMessageFunc func(ctx context.Context, id any, message *mcp.CreateMessageRequest)
// OnAfterSamplingCreateMessageFunc 采样创建消息后置钩子类型
// 针对SamplingCreateMessage方法的专属后置钩子
type OnAfterSamplingCreateMessageFunc func(ctx context.Context, id any, message *mcp.CreateMessageRequest, result *mcp.CreateMessageResult)
// OnBeforeListRootsFunc 列出根节点前置钩子类型
// 针对ListRoots方法的专属前置钩子
type OnBeforeListRootsFunc func(ctx context.Context, id any, message *mcp.ListRootsRequest)
// OnAfterListRootsFunc 列出根节点后置钩子类型
// 针对ListRoots方法的专属后置钩子
type OnAfterListRootsFunc func(ctx context.Context, id any, message *mcp.ListRootsRequest, result *mcp.ListRootsResult)
// OnBeforeElicitationCreateFunc 启发式创建前置钩子类型
// 针对ElicitationCreate方法的专属前置钩子
type OnBeforeElicitationCreateFunc func(ctx context.Context, id any, message *mcp.ElicitationRequest)
// OnAfterElicitationCreateFunc 启发式创建后置钩子类型
// 针对ElicitationCreate方法的专属后置钩子
type OnAfterElicitationCreateFunc func(ctx context.Context, id any, message *mcp.ElicitationRequest, result *mcp.ElicitationResult)
// Hooks 钩子管理器结构体
// 聚合所有类型的钩子函数切片,支持注册和触发各类钩子
type Hooks struct {
OnRegisterSession []OnRegisterSessionHookFunc // 会话注册钩子切片
OnUnregisterSession []OnUnregisterSessionHookFunc // 会话注销钩子切片
OnBeforeAny []BeforeAnyHookFunc // 通用前置钩子切片
OnSuccess []OnSuccessHookFunc // 成功回调钩子切片
OnError []OnErrorHookFunc // 错误回调钩子切片
OnRequestInitialization []OnRequestInitializationFunc // 请求初始化钩子切片
OnBeforeSamplingCreateMessage []OnBeforeSamplingCreateMessageFunc // 采样创建消息前置钩子切片
OnAfterSamplingCreateMessage []OnAfterSamplingCreateMessageFunc // 采样创建消息后置钩子切片
OnBeforeListRoots []OnBeforeListRootsFunc // 列出根节点前置钩子切片
OnAfterListRoots []OnAfterListRootsFunc // 列出根节点后置钩子切片
OnBeforeElicitationCreate []OnBeforeElicitationCreateFunc // 启发式创建前置钩子切片
OnAfterElicitationCreate []OnAfterElicitationCreateFunc // 启发式创建后置钩子切片
// ... 以及 Initialize/Ping/SetLevel/ListResources/ListResourceTemplates/
// ReadResource/ListPrompts/GetPrompt/ListTools/CallTool 等方法的 Before/After 钩子
}
// AddBeforeAny 注册通用前置钩子
// 参数hook: 要注册的通用前置钩子函数
func (c *Hooks) AddBeforeAny(hook BeforeAnyHookFunc) {
c.OnBeforeAny = append(c.OnBeforeAny, hook) // 将钩子追加到切片末尾
}
// AddOnSuccess 注册成功回调钩子
// 参数hook: 要注册的成功回调钩子函数
func (c *Hooks) AddOnSuccess(hook OnSuccessHookFunc) {
c.OnSuccess = append(c.OnSuccess, hook) // 将钩子追加到切片末尾
}
// AddOnError 注册错误回调钩子
// 当请求处理过程中发生错误时,注册的钩子函数会被调用
// 参数hook: 要注册的错误回调钩子函数
//
// 示例:
// ```
// // 创建用于测试的错误通道
// errChan := make(chan error, 1)
//
// // 注册钩子捕获并检查错误
// hooks := &Hooks{}
// hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
// // 检查能力相关错误
// if errors.Is(err, ErrUnsupported) {
// // 处理不支持的能力错误
// errChan <- err
// return
// }
//
// // 检查解析错误
// var parseErr = &UnparsableMessageError{}
// if errors.As(err, &parseErr) {
// // 处理消息解析失败错误
// fmt.Printf("Failed to parse %s request: %v\n",
// parseErr.GetMethod(), parseErr.Unwrap())
// errChan <- parseErr
// return
// }
//
// // 检查资源/提示/工具未找到错误
// if errors.Is(err, ErrResourceNotFound) ||
// errors.Is(err, ErrPromptNotFound) ||
// errors.Is(err, ErrToolNotFound) {
// // 处理未找到类错误
// errChan <- err
// return
// }
//
// // 处理其他错误
// errChan <- err
// })
//
// server := NewMCPServer("test-server", "1.0.0", WithHooks(hooks))
// ```
func (c *Hooks) AddOnError(hook OnErrorHookFunc) {
c.OnError = append(c.OnError, hook) // 将钩子追加到切片末尾
}
// beforeAny 触发所有通用前置钩子
// 参数ctx: 请求上下文
// 参数id: 请求ID
// 参数method: MCP方法类型
// 参数message: 请求消息体
func (c *Hooks) beforeAny(ctx context.Context, id any, method mcp.MCPMethod, message any) {
if c == nil { // 空指针保护
return
}
for _, hook := range c.OnBeforeAny { // 遍历所有注册的通用前置钩子
hook(ctx, id, method, message) // 执行单个钩子函数
}
}
// onSuccess 触发所有成功回调钩子
// 参数ctx: 请求上下文
// 参数id: 请求ID
// 参数method: MCP方法类型
// 参数message: 请求消息体
// 参数result: 请求处理结果
func (c *Hooks) onSuccess(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) {
if c == nil { // 空指针保护
return
}
for _, hook := range c.OnSuccess { // 遍历所有注册的成功回调钩子
hook(ctx, id, method, message, result) // 执行单个钩子函数
}
}
// onError 触发所有错误回调钩子
// 入参err包含实际发生的错误对象,实现了标准error接口,可能是包装错误或自定义错误类型
// 支持Go标准错误处理模式:
// - 使用errors.Is(err, ErrUnsupported)检查哨兵错误
// - 使用errors.As(err, &customErr)提取自定义错误类型
//
// 常见错误类型包括:
// - ErrUnsupported: 能力未启用时触发
// - UnparsableMessageError: 请求解析失败时触发
// - ErrResourceNotFound: 资源未找到时触发
// - ErrPromptNotFound: 提示未找到时触发
// - ErrToolNotFound: 工具未找到时触发
//
// 参数ctx: 请求上下文
// 参数id: 请求ID
// 参数method: MCP方法类型
// 参数message: 请求消息体
// 参数err: 发生的错误对象
func (c *Hooks) onError(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
if c == nil { // 空指针保护
return
}
for _, hook := range c.OnError { // 遍历所有注册的错误回调钩子
hook(ctx, id, method, message, err) // 执行单个钩子函数
}
}
// AddOnRegisterSession 注册会话注册钩子
// 参数hook: 要注册的会话注册钩子函数
func (c *Hooks) AddOnRegisterSession(hook OnRegisterSessionHookFunc) {
c.OnRegisterSession = append(c.OnRegisterSession, hook) // 将钩子追加到切片末尾
}
// RegisterSession 触发所有会话注册钩子
// 参数ctx: 请求上下文
// 参数session: 新注册的客户端会话
func (c *Hooks) RegisterSession(ctx context.Context, session ClientSession) {
if c == nil { // 空指针保护
return
}
for _, hook := range c.OnRegisterSession { // 遍历所有注册的会话注册钩子
hook(ctx, session) // 执行单个钩子函数
}
}
// AddOnUnregisterSession 注册会话注销钩子
// 参数hook: 要注册的会话注销钩子函数
func (c *Hooks) AddOnUnregisterSession(hook OnUnregisterSessionHookFunc) {
c.OnUnregisterSession = append(c.OnUnregisterSession, hook) // 将钩子追加到切片末尾
}
// UnregisterSession 触发所有会话注销钩子
// 参数ctx: 请求上下文
// 参数session: 待注销的客户端会话
func (c *Hooks) UnregisterSession(ctx context.Context, session ClientSession) {
if c == nil { // 空指针保护
return
}
for _, hook := range c.OnUnregisterSession { // 遍历所有注册的会话注销钩子
hook(ctx, session) // 执行单个钩子函数
}
}
// AddOnRequestInitialization 注册请求初始化钩子
// 参数hook: 要注册的请求初始化钩子函数
func (c *Hooks) AddOnRequestInitialization(hook OnRequestInitializationFunc) {
c.OnRequestInitialization = append(c.OnRequestInitialization, hook) // 将钩子追加到切片末尾
}
// onRequestInitialization 触发所有请求初始化钩子
// 若任意钩子返回错误,立即终止并返回该错误
// 参数ctx: 请求上下文
// 参数id: 请求ID
// 参数message: 请求消息体
// 返回值: 钩子执行过程中发生的错误(若有)
func (c *Hooks) onRequestInitialization(ctx context.Context, id any, message any) error {
if c == nil { // 空指针保护
return nil
}
for _, hook := range c.OnRequestInitialization { // 遍历所有注册的请求初始化钩子
err := hook(ctx, id, message) // 执行单个钩子函数
if err != nil { // 钩子执行出错时立即返回
return err
}
}
return nil // 所有钩子执行成功
}
// AddBeforeSamplingCreateMessage 注册采样创建消息前置钩子
// 参数hook: 要注册的采样创建消息前置钩子函数
func (c *Hooks) AddBeforeSamplingCreateMessage(hook OnBeforeSamplingCreateMessageFunc) {
c.OnBeforeSamplingCreateMessage = append(c.OnBeforeSamplingCreateMessage, hook) // 将钩子追加到切片末尾
}
// AddAfterSamplingCreateMessage 注册采样创建消息后置钩子
// 参数hook: 要注册的采样创建消息后置钩子函数
func (c *Hooks) AddAfterSamplingCreateMessage(hook OnAfterSamplingCreateMessageFunc) {
c.OnAfterSamplingCreateMessage = append(c.OnAfterSamplingCreateMessage, hook) // 将钩子追加到切片末尾
}
// beforeSamplingCreateMessage 触发采样创建消息的前置钩子
// 先执行通用前置钩子,再执行该方法专属前置钩子
// 参数ctx: 请求上下文
// 参数id: 请求ID
// 参数message: 采样创建消息请求体
func (c *Hooks) beforeSamplingCreateMessage(ctx context.Context, id any, message *mcp.CreateMessageRequest) {
c.beforeAny(ctx, id, mcp.MethodSamplingCreateMessage, message) // 先触发通用前置钩子
if c == nil { // 空指针保护
return
}
for _, hook := range c.OnBeforeSamplingCreateMessage { // 遍历采样创建消息专属前置钩子
hook(ctx, id, message) // 执行单个钩子函数
}
}
// afterSamplingCreateMessage 触发采样创建消息的后置钩子
// 先执行通用成功回调钩子,再执行该方法专属后置钩子
// 参数ctx: 请求上下文
// 参数id: 请求ID
// 参数message: 采样创建消息请求体
// 参数result: 采样创建消息处理结果
func (c *Hooks) afterSamplingCreateMessage(ctx context.Context, id any, message *mcp.CreateMessageRequest, result *mcp.CreateMessageResult) {
c.onSuccess(ctx, id, mcp.MethodSamplingCreateMessage, message, result) // 先触发通用成功钩子
if c == nil { // 空指针保护
return
}
for _, hook := range c.OnAfterSamplingCreateMessage { // 遍历采样创建消息专属后置钩子
hook(ctx, id, message, result) // 执行单个钩子函数
}
}
对 SRE 而言,Hooks 的核心价值是:所有 MCP 请求的 关键节点 都提供了 挂载点,我们可以在这些挂载点上添加任意运维相关逻辑,且不触碰业务代码。
从 hooks.go.tmpl 代码中,可梳理出覆盖 MCP 请求全生命周期的核心挂载点,对应 SRE 关注的关键运维节点:
2.2、Hooks 的核心设计原则(SRE 视角)
三、Metrics Hooks:SRE 的数字仪表盘
New(ms *metrics.Set) 函数是为 SRE 打造的核心指标采集器,其设计完全围绕 可观测性 展开,我们从 SRE 运维需求拆解每个指标的价值。
3.1、核心依赖:为什么选 VictoriaMetrics/metrics 而非官方 Prometheus 库?
SRE 最关注 性能 和 易用性,这也是 VM 团队自研指标库的核心原因:
3.2、逐行拆解:每个指标对 SRE 的核心价值
💡 深入理解:为什么所有 Metrics 钩子都注册在 After 阶段?
这是一个精心的设计选择,而非偶然。AddAfterInitialize、AddAfterCallTool 等后置钩子确保只有成功处理的请求才被计数,避免了"请求还没处理完就计数"导致的指标失真。唯一的例外是 AddOnError——它注册在全局错误钩子上,专门兜底捕获所有阶段的异常。
另一个值得注意的细节:GetOrCreateCounter 的"动态创建"能力是关键——它允许在运行时根据实际的 client_name、tool_name 动态生成指标,无需预定义所有标签组合。这对 MCP Server 这种工具名、客户端名不可预知的场景至关重要。如果用 Prometheus 官方库,你需要提前 Register 所有可能的指标——在动态场景下几乎不可能。
以下是 New(ms *metrics.Set) 函数的完整源码(位于 cmd/mcp-victoriametrics/hooks/hooks.go),逐段拆解:
func New(ms *metrics.Set) *server.Hooks {
hooks := &server.Hooks{}
// ① 客户端初始化指标
hooks.AddAfterInitialize(func(_ context.Context, _ any, message *mcp.InitializeRequest, _ *mcp.InitializeResult) {
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_initialize_total{client_name="%s",client_version="%s"}`,
message.Params.ClientInfo.Name,
message.Params.ClientInfo.Version,
)).Inc()
})
// ② 列表操作指标(工具/资源/提示词)
hooks.AddAfterListTools(func(_ context.Context, _ any, _ *mcp.ListToolsRequest, _ *mcp.ListToolsResult) {
ms.GetOrCreateCounter(`mcp_victoriametrics_list_tools_total`).Inc()
})
hooks.AddAfterListResources(func(_ context.Context, _ any, _ *mcp.ListResourcesRequest, _ *mcp.ListResourcesResult) {
ms.GetOrCreateCounter(`mcp_victoriametrics_list_resources_total`).Inc()
})
hooks.AddAfterListPrompts(func(_ context.Context, _ any, _ *mcp.ListPromptsRequest, _ *mcp.ListPromptsResult) {
ms.GetOrCreateCounter(`mcp_victoriametrics_list_prompts_total`).Inc()
})
// ③ 工具调用指标(核心业务监控)
hooks.AddAfterCallTool(func(_ context.Context, _ any, message *mcp.CallToolRequest, result any) {
isError := false
if r, ok := result.(*mcp.CallToolResult); ok {
isError = r.IsError
}
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_call_tool_total{name="%s",is_error="%t"}`,
message.Params.Name,
isError,
)).Inc()
})
// ④ 提示词获取指标
hooks.AddAfterGetPrompt(func(_ context.Context, _ any, message *mcp.GetPromptRequest, _ *mcp.GetPromptResult) {
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_get_prompt_total{name="%s"}`,
message.Params.Name,
)).Inc()
})
// ⑤ 资源读取指标
hooks.AddAfterReadResource(func(_ context.Context, _ any, message *mcp.ReadResourceRequest, _ *mcp.ReadResourceResult) {
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_read_resource_total{uri="%s"}`,
message.Params.URI,
)).Inc()
})
// ⑥ 全局错误指标
hooks.AddOnError(func(_ context.Context, _ any, method mcp.MCPMethod, _ any, err error) {
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_error_total{method="%s",error="%s"}`,
method,
err,
)).Inc()
})
return hooks
}
源码关键细节解读:
-
-
- 所有指标钩子均注册在 After 阶段(AddAfterInitialize、AddAfterListTools 等),这意味着只有请求成功处理后才会计数,保证指标的准确性。
- 唯一的例外是 AddOnError,它注册在全局错误钩子上,用于捕获所有阶段的错误。
- AddAfterCallTool 的 result 参数类型是
any(而非*mcp.CallToolResult),这是 mcp-go 框架的设计——后置钩子的 result 参数使用泛型接口,需要通过类型断言result.(*mcp.CallToolResult)获取具体类型。这是防御性编程的体现。
-
3.2.1、客户端初始化指标:掌握客户端分布
hooks.AddAfterInitialize(func(_ context.Context, _ any, message *mcp.InitializeRequest, _ *mcp.InitializeResult) {
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_initialize_total{client_name="%s",client_version="%s"}`,
message.Params.ClientInfo.Name,
message.Params.ClientInfo.Version,
)).Inc()
})
SRE 运维价值:
-
-
-
- 核心问题:“哪些 AI 客户端(Claude/Cursor/ 自定义 Agent)在使用 MCP Server?各版本占比多少?”
- 故障排查:若某版本客户端调用异常,可快速定位版本范围
- 容量规划:根据客户端数量增长趋势,提前扩容 MCP Server
- 源码细节:注意这里使用的是
AddAfterInitialize(后置钩子),而非AddBeforeInitialize,确保只有初始化成功的客户端才被计数
-
-
3.2.2、列表操作指标:基础操作健康度
hooks.AddAfterListTools(func(_ context.Context, _ any, _ *mcp.ListToolsRequest, _ *mcp.ListToolsResult) {
ms.GetOrCreateCounter(`mcp_victoriametrics_list_tools_total`).Inc()
})
hooks.AddAfterListResources(func(_ context.Context, _ any, _ *mcp.ListResourcesRequest, _ *mcp.ListResourcesResult) {
ms.GetOrCreateCounter(`mcp_victoriametrics_list_resources_total`).Inc()
})
hooks.AddAfterListPrompts(func(_ context.Context, _ any, _ *mcp.ListPromptsRequest, _ *mcp.ListPromptsResult) {
ms.GetOrCreateCounter(`mcp_victoriametrics_list_prompts_total`).Inc()
})
SRE 运维价值:
-
-
-
- 基础监控:这三类操作是 MCP Server 的基础心跳,若调用量骤降,可能是客户端连接异常
- 无标签设计:列表操作无业务区分维度,冗余标签会增加 Prometheus 存储压力(SRE 需关注指标基数)
- 源码细节:注意回调函数的四个参数全部使用
_忽略(context、id、request、result),因为列表操作只需计数,不需要提取任何业务信息
-
-
3.2.3、工具调用指标:核心业务监控
hooks.AddAfterCallTool(func(_ context.Context, _ any, message *mcp.CallToolRequest, result any) {
isError := false
if r, ok := result.(*mcp.CallToolResult); ok {
isError = r.IsError
}
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_call_tool_total{name="%s",is_error="%t"}`,
message.Params.Name,
isError,
)).Inc()
})
SRE 运维价值(核心):
-
-
-
- 业务监控:可统计每个工具的 调用量 / 错误率,如query工具错误率突增,需优先排查 VM 查询逻辑
- 故障告警:可配置 Prometheus Rule:rate(mcp_victoriametrics_call_tool_total{is_error="true"}[5m]) / rate(mcp_victoriametrics_call_tool_total[5m]) > 0.1(错误率超 10% 告警)
- 类型断言细节:
result.(*mcp.CallToolResult)是防御性编程——AddAfterCallTool的回调签名中 result 类型为any,必须通过类型断言获取IsError字段。若断言失败(理论上不会),isError 保持 false 默认值,不会导致 panic(SRE 最怕监控本身出问题) - 标签设计:
is_error标签使用%t格式化布尔值(输出 "true"/"false" 字符串),而非 0/1,这是 Prometheus 标签的常见实践
-
-
3.2.4、提示词 / 资源读取指标:业务行为分析
// 提示词获取
hooks.AddAfterGetPrompt(func(_ context.Context, _ any, message *mcp.GetPromptRequest, _ *mcp.GetPromptResult) {
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_get_prompt_total{name="%s"}`,
message.Params.Name,
)).Inc()
})
// 资源读取
hooks.AddAfterReadResource(func(_ context.Context, _ any, message *mcp.ReadResourceRequest, _ *mcp.ReadResourceResult) {
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_read_resource_total{uri="%s"}`,
message.Params.URI,
)).Inc()
})
SRE 运维价值:
-
-
-
- 热点分析:哪些提示词 / 资源被频繁访问?可提前做缓存优化
- 异常检测:若某 URI 读取量突增,可能是 AI Agent 异常遍历资源,需介入排查
- 源码细节:资源读取使用
message.Params.URI作为标签值,提示词使用message.Params.Name——两者分别对应 MCP 协议中资源的 URI 标识和提示词的名称标识
-
-
3.2.5、全局错误指标:故障兜底监控
hooks.AddOnError(func(_ context.Context, _ any, method mcp.MCPMethod, _ any, err error) {
ms.GetOrCreateCounter(fmt.Sprintf(
`mcp_victoriametrics_error_total{method="%s",error="%s"}`,
method,
err,
)).Inc()
})
SRE 运维价值:
-
-
-
- 兜底监控:覆盖所有未被捕获的 MCP 方法错误,避免漏报
- 源码细节:注意回调签名中第 4 个参数(message)使用
_ any忽略,只关注 method 和 err,这是因为错误指标不需要请求体内容,只需知道哪个方法出了什么错 - 潜在风险:
error标签直接使用err的字符串表示(通过%s格式化调用err.Error()),可能导致指标基数爆炸(SRE 需注意:生产环境可优化为错误类型枚举,如 “invalid_param”“connection_error”)
-
-
⚠️ 深入理解:error 标签的"基数爆炸"陷阱
这是整个 Hooks 实现中最值得 SRE 警惕的设计。error="%s" 直接将 err.Error() 的完整字符串作为标签值——如果错误信息包含动态内容(如请求 ID、时间戳、具体参数),每次错误都会创建一个全新的时间序列。在高错误率场景下,Prometheus 的内存和存储会被迅速耗尽,这就是所谓的"基数爆炸"(Cardinality Explosion)。
生产环境建议:将 error 标签替换为错误类型枚举。例如用 errors.Is(err, ErrToolNotFound) 判断后输出 "tool_not_found",而非原始错误字符串。这样标签基数可控(十几种错误类型 vs 无限种错误字符串),Prometheus 存储压力大幅降低。
3.2.6、指标暴露:SRE 如何接入 Prometheus
在 main.go 中,指标通过 HTTP 端点暴露,这是 SRE 最熟悉的方式:
// main.go 关键代码(cmd/mcp-victoriametrics/main.go)
mux.HandleFunc("/metrics", func(w http.ResponseWriter, _ *http.Request) {
ms.WritePrometheus(w) // 自定义MCP指标(由 New(ms) 注册的所有 Counter)
metrics.WriteProcessMetrics(w) // 进程级指标(CPU/内存/GC等)
})
源码细节:
-
-
-
ms.WritePrometheus(w)输出的是metrics.NewSet()实例中通过GetOrCreateCounter动态创建的所有指标metrics.WriteProcessMetrics(w)是全局函数,输出 Go 运行时指标(goroutine 数、GC 耗时、内存分配等),与 MCP 业务指标分开管理- 注意:此端点仅在非 stdio 模式下可用(stdio 模式下无 HTTP 服务器),这在 main.go 的
if c.IsStdio()分支中可以看到
-
-
SRE 落地建议:
-
-
-
- 配置 Prometheus 抓取:添加 job 抓取/metrics端点,间隔建议 15s(兼顾实时性和性能)
- 构建 Grafana 面板:至少包含 “客户端分布、工具调用错误率、资源访问热度、全局错误趋势” 四个核心视图
- 配置告警规则:重点监控工具调用错误率、全局错误数、初始化请求量骤降等场景
-
-
四、Logger Hooks:SRE 的故障溯源神器
NewLoggerHooks() 函数为 SRE 提供了结构化、全生命周期的日志体系,解决了 故障排查时无日志、日志无上下文 的核心痛点。
以下是 NewLoggerHooks() 的完整源码(位于 cmd/mcp-victoriametrics/hooks/hooks.go):
func NewLoggerHooks() *server.Hooks {
hooks := &server.Hooks{}
// ① 会话生命周期日志
hooks.AddOnRegisterSession(func(_ context.Context, session server.ClientSession) {
slog.Info("Session registered",
"session_id", session.SessionID(),
)
})
hooks.AddOnUnregisterSession(func(_ context.Context, session server.ClientSession) {
slog.Info("Session unregistered",
"session_id", session.SessionID(),
)
})
// ② 请求全生命周期日志(通用钩子)
hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
sessionID := extractSessionID(ctx)
slog.Info("MCP request received",
"request_id", id,
"session_id", sessionID,
"method", string(method),
"message", toJSON(message),
)
})
hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) {
sessionID := extractSessionID(ctx)
slog.Info("MCP request succeeded",
"request_id", id,
"session_id", sessionID,
"method", string(method),
"message", toJSON(message),
"result", toJSON(result),
)
})
hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
sessionID := extractSessionID(ctx)
slog.Error("MCP request failed",
"request_id", id,
"session_id", sessionID,
"method", string(method),
"message", toJSON(message),
"error", err.Error(),
)
})
// ③ 业务关键操作日志(方法级钩子)
hooks.AddAfterInitialize(func(_ context.Context, id any, msg *mcp.InitializeRequest, _ *mcp.InitializeResult) {
slog.Info("Client initialized",
"request_id", id,
"client_name", msg.Params.ClientInfo.Name,
"client_version", msg.Params.ClientInfo.Version,
"protocol_version", msg.Params.ProtocolVersion,
)
})
hooks.AddAfterCallTool(func(_ context.Context, id any, msg *mcp.CallToolRequest, result any) {
isError := false
if r, ok := result.(*mcp.CallToolResult); ok {
isError = r.IsError
}
slog.Info("Tool called",
"request_id", id,
"tool_name", msg.Params.Name,
"is_error", isError,
)
})
return hooks
}
源码关键细节解读:
-
-
- 日志钩子同时使用了通用钩子和方法级钩子:
AddBeforeAny/AddOnSuccess/AddOnError覆盖所有请求的全生命周期,AddAfterInitialize/AddAfterCallTool则对关键操作做额外的结构化日志输出。这意味着一次 CallTool 请求会产生至少 3 条日志:BeforeAny(请求进入)→ OnSuccess(请求成功)→ AfterCallTool(工具调用详情)。 - session_id 的提取方式:通过
extractSessionID(ctx)从 context 中获取,而非从参数传入。这是因为 session 信息在 MCP 框架中通过 context 传递(server.ClientSessionFromContext(ctx)),体现了 Go 的 context 传值最佳实践。 - 错误日志使用
slog.Error而非slog.Info:这是唯一使用 Error 级别的地方,便于日志系统按级别过滤和告警。
- 日志钩子同时使用了通用钩子和方法级钩子:
-
4.1、日志设计:SRE 视角的核心要求
💡 深入理解:日志钩子的"三层漏斗"设计
NewLoggerHooks 的日志设计形成了一个三层漏斗:通用层(BeforeAny / OnSuccess / OnError)覆盖所有请求的全生命周期 → 方法层(AfterInitialize / AfterCallTool)对关键操作做额外记录 → 会话层(OnRegisterSession / OnUnregisterSession)追踪连接生命周期。三层互补,不遗漏任何运维关键信息。
一个容易忽略的设计亮点:toJSON 函数在序列化失败时返回空字符串而非 panic。这体现了"监控永远不能成为故障源"的 SRE 铁律——日志是辅助手段,绝不能因为日志序列化失败而导致业务请求中断。同理,extractSessionID 在 context 中找不到 session 时返回空字符串,而非报错。
4.2、辅助函数源码解读
NewLoggerHooks 依赖两个辅助函数,它们的实现同样体现了防御性编程思想:
// extractSessionID 从 context 中提取 session ID
// 若 context 中无 session 信息(如 stdio 模式),返回空字符串
func extractSessionID(ctx context.Context) string {
session := server.ClientSessionFromContext(ctx)
if session != nil {
return session.SessionID()
}
return ""
}
// toJSON 将任意值转为 JSON 字符串用于日志输出
// 关键设计:序列化失败时返回空字符串,而非 panic 或返回 error
// 这确保了日志逻辑永远不会导致业务流程中断
func toJSON(v any) string {
if v == nil {
return ""
}
b, err := json.Marshal(v)
if err != nil {
return ""
}
return string(b)
}
源码细节:
-
-
extractSessionID使用server.ClientSessionFromContext(ctx)——这是 mcp-go 框架提供的 API,在 HTTP/SSE 模式下 context 中会携带 session 信息,但在 stdio 模式下可能为 nil,因此需要 nil 检查。toJSON的双重防御:先检查v == nil(避免 json.Marshal(nil) 输出 "null"),再检查err != nil(处理不可序列化的类型如 channel、func)。两种情况都返回空字符串,保证日志输出的安全性。
-
4.3.1、会话生命周期日志:追踪客户端连接
hooks.AddOnRegisterSession(func(_ context.Context, session server.ClientSession) {
slog.Info("Session registered", "session_id", session.SessionID())
})
hooks.AddOnUnregisterSession(func(_ context.Context, session server.ClientSession) {
slog.Info("Session unregistered", "session_id", session.SessionID())
})
SRE 运维价值:
-
-
-
- 连接监控:可统计当前活跃会话数(count by (session_id) (logfmt:session_id))
- 故障排查:若某会话频繁注册 / 注销,可能是客户端连接异常(如网络抖动、客户端 Bug)
- 源码细节:会话钩子的回调签名是
func(ctx context.Context, session server.ClientSession),与其他钩子不同——没有 id/method/message 参数,因为会话注册/注销不属于 MCP 请求生命周期,而是传输层事件
-
-
4.3.2、请求全生命周期日志:单次请求的完整画像
// 请求进入(BeforeAny 通用前置钩子)
hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
sessionID := extractSessionID(ctx)
slog.Info("MCP request received",
"request_id", id,
"session_id", sessionID,
"method", string(method),
"message", toJSON(message),
)
})
// 请求成功(OnSuccess 通用成功钩子)
hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) {
sessionID := extractSessionID(ctx)
slog.Info("MCP request succeeded",
"request_id", id,
"session_id", sessionID,
"method", string(method),
"message", toJSON(message),
"result", toJSON(result),
)
})
// 请求失败(OnError 通用错误钩子)
hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
sessionID := extractSessionID(ctx)
slog.Error("MCP request failed",
"request_id", id,
"session_id", sessionID,
"method", string(method),
"message", toJSON(message),
"error", err.Error(),
)
})
SRE 运维价值:
-
-
-
- 故障溯源:通过request_id可串联 “请求进入→业务处理→响应 / 错误” 全流程,还原故障现场
- 性能分析:可计算单次请求的耗时(通过日志时间戳差),定位慢请求
- 异常检测:Error 日志可接入告警系统(如 Loki+Promtail+Alertmanager),实时推送故障
- 源码细节:注意 OnSuccess 钩子同时记录了 message 和 result(通过 toJSON 序列化),而 OnError 钩子记录 message 和 err.Error()——错误钩子使用
err.Error()而非toJSON(err),因为 error 接口的 JSON 序列化可能丢失信息(只输出{}),直接调用Error()方法更可靠
-
-
4.3.3、业务关键操作日志:简化故障定位
// 客户端初始化详情日志
hooks.AddAfterInitialize(func(_ context.Context, id any, msg *mcp.InitializeRequest, _ *mcp.InitializeResult) {
slog.Info("Client initialized",
"request_id", id,
"client_name", msg.Params.ClientInfo.Name,
"client_version", msg.Params.ClientInfo.Version,
"protocol_version", msg.Params.ProtocolVersion,
)
})
// 工具调用详情日志
hooks.AddAfterCallTool(func(_ context.Context, id any, msg *mcp.CallToolRequest, result any) {
isError := false
if r, ok := result.(*mcp.CallToolResult); ok {
isError = r.IsError
}
slog.Info("Tool called",
"request_id", id,
"tool_name", msg.Params.Name,
"is_error", isError,
)
})
SRE 运维价值:
-
-
-
- 快速筛选:无需解析 JSON 的message字段,直接通过client_name筛选特定客户端的日志
- 版本兼容:若某客户端版本与 MCP 协议版本不兼容,可快速定位
- 源码细节:AddAfterCallTool 的日志钩子与 Metrics 钩子中的 AddAfterCallTool 使用了完全相同的类型断言模式(
result.(*mcp.CallToolResult)),但日志钩子额外记录了request_id,而 Metrics 钩子不需要——这体现了两类钩子的职责分离:Metrics 关注聚合统计,Logging 关注单次请求追踪
-
-
4.3.4、日志落地建议(SRE 实操)
-
-
- 日志采集:使用 Promtail / Fluent-Bit 采集 slog 输出的 JSON 日志,接入 Loki / ELK
- 日志查询:
- 按 session_id 查某客户端的所有操作:{job="mcp-victoriametrics"} |= "session_id: abc123"
- 按 method 查某操作的错误日志:{job="mcp-victoriametrics"} |= "method: mcp.CallTool" | json | is_error="true"
- 日志保留:核心错误日志保留 30 天,普通日志保留 7 天(兼顾故障排查和存储成本)
-
五、Merge 函数:SRE 的扩展利器
Merge函数是 Hooks 机制的 粘合剂,其设计完美契合 SRE 的 可扩展性 诉求。
5.1、核心实现:组合多个 Hooks
以下是 Merge 函数的完整源码(位于 cmd/mcp-victoriametrics/hooks/hooks.go):
func Merge(hooksList ...*server.Hooks) *server.Hooks {
combined := &server.Hooks{}
for _, h := range hooksList {
if h == nil {
continue
}
combined.OnRegisterSession = append(combined.OnRegisterSession, h.OnRegisterSession...)
combined.OnUnregisterSession = append(combined.OnUnregisterSession, h.OnUnregisterSession...)
combined.OnBeforeAny = append(combined.OnBeforeAny, h.OnBeforeAny...)
combined.OnSuccess = append(combined.OnSuccess, h.OnSuccess...)
combined.OnError = append(combined.OnError, h.OnError...)
combined.OnRequestInitialization = append(combined.OnRequestInitialization, h.OnRequestInitialization...)
combined.OnBeforeInitialize = append(combined.OnBeforeInitialize, h.OnBeforeInitialize...)
combined.OnAfterInitialize = append(combined.OnAfterInitialize, h.OnAfterInitialize...)
combined.OnBeforePing = append(combined.OnBeforePing, h.OnBeforePing...)
combined.OnAfterPing = append(combined.OnAfterPing, h.OnAfterPing...)
combined.OnBeforeSetLevel = append(combined.OnBeforeSetLevel, h.OnBeforeSetLevel...)
combined.OnAfterSetLevel = append(combined.OnAfterSetLevel, h.OnAfterSetLevel...)
combined.OnBeforeListResources = append(combined.OnBeforeListResources, h.OnBeforeListResources...)
combined.OnAfterListResources = append(combined.OnAfterListResources, h.OnAfterListResources...)
combined.OnBeforeListResourceTemplates = append(combined.OnBeforeListResourceTemplates, h.OnBeforeListResourceTemplates...)
combined.OnAfterListResourceTemplates = append(combined.OnAfterListResourceTemplates, h.OnAfterListResourceTemplates...)
combined.OnBeforeReadResource = append(combined.OnBeforeReadResource, h.OnBeforeReadResource...)
combined.OnAfterReadResource = append(combined.OnAfterReadResource, h.OnAfterReadResource...)
combined.OnBeforeListPrompts = append(combined.OnBeforeListPrompts, h.OnBeforeListPrompts...)
combined.OnAfterListPrompts = append(combined.OnAfterListPrompts, h.OnAfterListPrompts...)
combined.OnBeforeGetPrompt = append(combined.OnBeforeGetPrompt, h.OnBeforeGetPrompt...)
combined.OnAfterGetPrompt = append(combined.OnAfterGetPrompt, h.OnAfterGetPrompt...)
combined.OnBeforeListTools = append(combined.OnBeforeListTools, h.OnBeforeListTools...)
combined.OnAfterListTools = append(combined.OnAfterListTools, h.OnAfterListTools...)
combined.OnBeforeCallTool = append(combined.OnBeforeCallTool, h.OnBeforeCallTool...)
combined.OnAfterCallTool = append(combined.OnAfterCallTool, h.OnAfterCallTool...)
}
return combined
}
源码关键细节与潜在问题:
-
-
- 实际合并了 26 个钩子字段(而非 24 个):OnRegisterSession、OnUnregisterSession、OnBeforeAny、OnSuccess、OnError、OnRequestInitialization,加上 Initialize/Ping/SetLevel/ListResources/ListResourceTemplates/ReadResource/ListPrompts/GetPrompt/ListTools/CallTool 各自的 Before/After 共 20 个,总计 26 个。
- ⚠️ 潜在 Bug:缺少新增钩子字段的合并——上游 mcp-go v0.45.0 的
Hooks结构体已新增OnBeforeSamplingCreateMessage、OnAfterSamplingCreateMessage、OnBeforeListRoots、OnAfterListRoots、OnBeforeElicitationCreate、OnAfterElicitationCreate共 6 个字段,但Merge函数并未合并这些字段。这意味着如果有 Hooks 注册了这些新钩子,合并后会丢失。这是一个需要关注的兼容性问题。 - 合并顺序决定执行顺序:
append操作保证了先传入的 Hooks 中的回调先执行。在 main.go 中hooks.Merge(metricsHooks, loggingHooks),Metrics 钩子先于 Logging 钩子执行——这意味着指标计数在日志记录之前完成。 - nil 安全:
if h == nil { continue }确保传入 nil 不会 panic,但注意这里只检查了整个 Hooks 指针是否为 nil,不检查单个字段切片——因为append(nil, ...)...在 Go 中是安全的(nil slice 可以被 append)。
-
5.2、main.go 中的实际调用方式
从 cmd/mcp-victoriametrics/main.go 源码可以看到 Hooks 的实际组装和使用方式:
// main.go 关键代码
ms := metrics.NewSet()
// 分别创建 Metrics Hooks 和 Logger Hooks
metricsHooks := hooks.New(ms)
loggingHooks := hooks.NewLoggerHooks()
// 合并为一个 Hooks 实例
combinedHooks := hooks.Merge(metricsHooks, loggingHooks)
// 传入 MCP Server
s := server.NewMCPServer(
"VictoriaMetrics",
fmt.Sprintf("v%s (date: %s)", version, date),
server.WithRecovery(),
server.WithLogging(),
server.WithToolCapabilities(false),
server.WithResourceCapabilities(false, false),
server.WithPromptCapabilities(false),
server.WithHooks(combinedHooks), // 注入合并后的 Hooks
server.WithInstructions(`...`),
)
源码细节:
-
-
server.WithHooks(combinedHooks)是 mcp-go 框架提供的 Option 模式,将 Hooks 注入到 MCP Server 中。框架在处理每个请求时,会在对应的生命周期节点调用 Hooks 中注册的回调。server.WithRecovery()和server.WithLogging()是框架内置的中间件,与自定义 Hooks 互补——WithRecovery 防止 panic 导致进程崩溃,WithLogging 提供框架级日志。metrics.NewSet()创建了一个独立的指标集合(而非使用全局默认集合),这允许 MCP 业务指标与进程级指标分开管理,在/metrics端点中分别输出。
-
5.3、SRE 视角的价值:可扩展、可组合、可复用
💡 深入理解:Merge 为什么是"粘合剂"而非"继承链"?
传统 OOP 思维可能会用继承来扩展 Hooks(如 MetricsHooks extends BaseHooks),但 Go 没有继承,Merge 用的是组合(Composition)思想——每个 Hooks 实例是独立的功能单元,Merge 只是把它们的回调切片拼接在一起。这意味着你可以像搭积木一样自由组合:Merge(metrics, logging) 用于生产环境,Merge(logging) 用于调试环境,Merge(metrics, logging, tracing) 用于全链路观测。
需要警惕的是 Merge 的顺序敏感性:append 保证先传入的 Hooks 先执行。在 Merge(metricsHooks, loggingHooks) 中,Metrics 钩子先于 Logging 钩子执行——如果 Metrics 钩子 panic(虽然不应该),后续的 Logging 钩子就不会执行。这也是为什么 server.WithRecovery() 在 main.go 中被启用的原因之一。
5.4、生产环境扩展示例(SRE 实操)
若需新增 链路追踪 能力,SRE 可无需修改现有代码,仅需:
// 1. 实现Tracing Hooks
func NewTracingHooks(tracer *otel.Tracer) *server.Hooks {
hooks := &server.Hooks{}
hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
// 启动span
ctx, span := tracer.Start(ctx, string(method))
span.SetAttribute("request_id", fmt.Sprintf("%v", id))
span.SetAttribute("session_id", extractSessionID(ctx))
})
hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) {
// 结束span(成功)
span := trace.SpanFromContext(ctx)
span.End()
})
hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
// 结束span(失败)
span := trace.SpanFromContext(ctx)
span.RecordError(err)
span.End()
})
return hooks
}
// 2. 合并到现有Hooks(在 main.go 中)
metricsHooks := hooks.New(ms)
loggingHooks := hooks.NewLoggerHooks()
tracingHooks := NewTracingHooks(tracer)
combinedHooks := hooks.Merge(metricsHooks, loggingHooks, tracingHooks)
// 3. 传入MCP Server
s := server.NewMCPServer(
"VictoriaMetrics",
version,
server.WithHooks(combinedHooks),
)
SRE 价值:新增链路追踪完全不影响现有监控 / 日志逻辑,符合 开闭原则,降低变更风险。
⚠️ 注意:上述 Tracing 示例中 AddBeforeAny 钩子修改了 ctx(ctx, span := tracer.Start(ctx, ...)),但当前 mcp-go 框架的 BeforeAnyHookFunc 签名不返回 ctx,因此修改后的 ctx 不会传递到后续钩子和业务逻辑中。实际生产中需要通过其他方式(如 context.WithValue 在请求入口注入 span)来实现完整的链路追踪。
六、SRE 生产环境落地最佳实践
💡 深入理解:从源码到生产,Hooks 的"最后一公里"
源码中的 Hooks 实现是"骨架",生产落地才是"血肉"。源码给了你 7 个 Counter 指标和 5 类结构化日志,但真正让它发挥价值的是:Prometheus 的抓取配置、Grafana 的面板设计、告警规则的阈值调优、日志系统的采集链路。下面的最佳实践就是帮你把这些"骨架"变成可落地的运维体系。
一个核心原则:监控的成本不能超过监控的价值。源码中的 error="%s" 标签就是反面教材——它提供了最细粒度的错误信息,但代价是可能的基数爆炸。下面的优化建议都围绕这个原则展开:在信息量和成本之间找到最佳平衡点。
6.1、指标优化
-
-
- 控制标签基数:将mcp_victoriametrics_error_total的error标签从原始错误字符串改为错误类型枚举(如 “invalid_uri”“tool_not_found”),避免基数爆炸。
- 添加指标注释:使用ms.GetOrCreateCounterWithHelp为指标添加注释,便于 Grafana 面板理解(如mcp_victoriametrics_call_tool_total: "MCP工具调用总次数,按工具名和是否错误分类")。
- 聚合维度:对高频标签(如uri)做聚合(如按资源类型聚合,而非全 URI),降低 Prometheus 存储压力。
-
6.2、日志优化
-
-
- 日志采样:高并发场景下,对普通 Info 日志做 1:10 采样,保留 Error 日志全量,降低存储成本。
- 敏感信息过滤:若message中包含敏感数据(如 API 密钥),在toJSON函数中过滤。
- 日志分级:将AfterCallTool的错误日志升级为 Error 级别,便于告警。
-
6.3、性能优化
-
-
- 异步日志:使用slog的异步处理器(slog.NewAsyncHandler),避免日志写入阻塞业务流程。
- 指标批量导出:若 MCP Server 请求量极高,可调整metrics.Set的导出频率,降低 CPU 占用。
- 避免重复计算:对extractSessionID和toJSON的结果做缓存,减少重复计算。
-
6.4、故障演练
-
-
- 监控失效演练:模拟metrics.Set初始化失败,验证业务流程是否不受影响。
- 日志序列化失败演练:传入无法序列化的message,验证toJSON是否返回空字符串且业务正常。
- Hooks 合并失败演练:传入 nil Hooks,验证Merge函数是否能正常处理。
-
七、总结:Hooks 机制的 SRE 核心价值
对 SRE 而言,mcp-victoriametrics 的 Hooks 机制是 可观测性的最优解,其核心价值可总结为:
从 SRE 视角看,Hooks 机制不仅是代码技巧,更是运维左移的最佳实践 —— 将可观测性设计融入代码架构,而非事后补丁,这也是 VictoriaMetrics 能成为高性能监控系统的核心原因之一。
🎯 全文核心洞察:三个函数撑起整个可观测性体系
回顾整个 hooks.go,核心代码量不到 200 行,却只用了三个函数就构建了完整的可观测性体系:New(ms) 负责指标采集(7 个 Counter 覆盖全业务),NewLoggerHooks() 负责结构化日志(三层漏斗覆盖全生命周期),Merge() 负责组合扩展(积木式拼装任意观测能力)。
这种设计的精髓在于"约束即自由"——mcp-go 框架通过 server.Hooks 结构体约束了所有可能的挂载点,而 VictoriaMetrics 团队在这些约束内,用最少的代码实现了最大的运维价值。对 SRE 来说,这不仅是一个可以直接复用的可观测性方案,更是一种值得借鉴的架构思维:把横切关注点从业务代码中彻底剥离,让监控成为架构的一部分,而非事后的补丁。
附录:生产环境监控面板(Grafana)核心指标

浙公网安备 33010602011771号