【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 交互的核心中间件,天然存在以下运维痛点: 

运维痛点直接后果
业务逻辑与监控日志耦合 改监控要动业务代码,易引入 Bug,迭代效率低
无精细化指标 无法回答 “哪个 AI 客户端调用最多?哪个工具错误率最高?” 等核心问题
无全链路日志 故障排查时无法还原请求完整生命周期,定位问题耗时久
指标 / 日志扩展难 新增监控维度需要重构核心逻辑,扩展性差

VictoriaMetrics 团队设计的 Hooks 机制,本质是基于 切面编程(AOP) 思想的无侵入式扩展方案—— 在不修改 MCP Server 核心业务逻辑的前提下,通过 hooks 挂载监控、日志、追踪等横切逻辑,完美解决上述 SRE 痛点。

🔍 深入理解:AOP 思想如何映射到 Hooks 实现?

AOP(面向切面编程)有三个核心概念,在 mcp-victoriametrics 的 Hooks 机制中均有清晰的对应:

❶ 切点(Pointcut) —— MCP 请求生命周期的各个阶段

上游框架 mcp-goserver.Hooks 结构体预定义了所有切点:OnBeforeAnyOnAfterCallToolOnError 等。每个切点对应请求处理流水线上的一个确定位置,业务代码不需要知道它们的存在。

❷ 通知 / 增强(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 关注的关键运维节点: 

挂载点类型触发时机(关键节点)核心价值(SRE 视角)
OnRegisterSession 新客户端会话注册时 监控会话创建、记录客户端接入信息、做会话级限流 / 鉴权
OnUnregisterSession 客户端会话注销时 清理会话资源、统计会话存活时长、告警异常会话退出
OnBeforeAny 所有请求解析完成后、业务方法执行前(通用前置) 全量请求审计、统一埋点(记录请求入参)、前置限流 / 黑白名单、上下文注入(如 TraceID)
OnBeforeXXX(方法级) 特定 MCP 方法(如 ListTools/CallTool)执行前(细分前置) 针对核心方法做精细化管控(如 CallTool 方法的权限校验、参数校验、流量控制)
OnSuccess 业务方法执行成功、结果返回客户端前 记录请求耗时、统计成功率、上报业务指标(如 CallTool 调用成功数)、修改返回结果(脱敏)
OnAfterXXX(方法级) 特定 MCP 方法执行成功后(细分后置) 针对核心方法做精细化监控(如 ListPrompts 结果校验、缓存预热)
OnError 请求解析 / 业务执行过程中发生错误时 统一错误监控、异常告警(如 ErrResourceNotFound 告警)、错误日志格式化、故障自愈尝试
OnRequestInitialization 处理差异化请求方法前的初始化阶段 请求初始化校验(如参数合法性、资源配额)、提前终止非法请求、初始化运维上下文

2.2、Hooks 的核心设计原则(SRE 视角)

设计原则运维价值
无侵入性 监控 / 日志逻辑与业务逻辑解耦,业务迭代不影响运维观测
关注点分离 Metrics、Logging、Tracing 可分别实现,按需组合
惰性执行 仅在事件触发时执行回调,无请求时无性能损耗
兼容 Prometheus 指标命名 / 格式符合 Prometheus 规范,降低运维接入成本

三、Metrics Hooks:SRE 的数字仪表盘

New(ms *metrics.Set) 函数是为 SRE 打造的核心指标采集器,其设计完全围绕 可观测性 展开,我们从 SRE 运维需求拆解每个指标的价值。

3.1、核心依赖:为什么选 VictoriaMetrics/metrics 而非官方 Prometheus 库?

SRE 最关注 性能 易用性,这也是 VM 团队自研指标库的核心原因:

特性VictoriaMetrics/metricsPrometheus client_golangSRE 视角的优势
性能 原子操作实现 Counter,无锁设计 基于 mutex,高并发下有锁竞争 高并发场景下更低的性能损耗
指标创建 GetOrCreateCounter 动态创建 需预注册所有指标 无需提前定义所有标签组合,适配动态场景(如任意工具名 / URI)
内存占用 更轻量,无冗余封装 功能丰富但冗余多 降低容器内存占用,减少 OOM 风险
Prometheus 兼容 支持 WritePrometheus 接口 原生支持 无缝接入现有 Prometheus/Grafana 体系

3.2、逐行拆解:每个指标对 SRE 的核心价值

💡 深入理解:为什么所有 Metrics 钩子都注册在 After 阶段?

这是一个精心的设计选择,而非偶然。AddAfterInitializeAddAfterCallTool 等后置钩子确保只有成功处理的请求才被计数,避免了"请求还没处理完就计数"导致的指标失真。唯一的例外是 AddOnError——它注册在全局错误钩子上,专门兜底捕获所有阶段的异常。

另一个值得注意的细节:GetOrCreateCounter 的"动态创建"能力是关键——它允许在运行时根据实际的 client_nametool_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 视角的核心要求

日志特性实现方式运维价值
结构化 使用 slog(JSON 格式) 可被 ELK/Loki 等日志系统解析,支持字段过滤 / 聚合
全上下文 包含 request_id/session_id/method 可通过 request_id 串联单次请求的全生命周期,快速定位问题
分级日志 Info(正常流程)/Error(异常) 可配置日志采集规则,Error 级别日志优先告警
无侵入序列化 toJSON 函数(失败返回空字符串) 日志序列化失败不影响主流程,避免 “监控导致业务故障”

💡 深入理解:日志钩子的"三层漏斗"设计

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 结构体已新增 OnBeforeSamplingCreateMessageOnAfterSamplingCreateMessageOnBeforeListRootsOnAfterListRootsOnBeforeElicitationCreateOnAfterElicitationCreate 共 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 视角的价值:可扩展、可组合、可复用 

特性运维价值
多 Hooks 合并 Metrics 和 Logging 可独立开发、测试、部署,按需组合(如测试环境可关闭 Metrics)
兼容 nil 避免某类 Hooks 未初始化导致程序崩溃,提升鲁棒性
全字段覆盖 覆盖 26 个钩子字段(但需注意上游新增字段的同步,见上文潜在 Bug 分析)

💡 深入理解: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 机制是 可观测性的最优解,其核心价值可总结为:

维度价值
可观测性 提供 “指标 + 日志 + 可扩展追踪” 的全维度观测能力,覆盖 MCP Server 所有生命周期
可维护性 监控 / 日志逻辑与业务解耦,变更风险低,迭代效率高
可扩展性 新增观测维度无需修改核心代码,仅需实现新的 Hooks 并合并
鲁棒性 防御性编程(如 nil 处理、序列化失败返回空)避免监控逻辑导致业务故障
兼容性 指标兼容 Prometheus,日志兼容主流日志系统,降低运维接入成本

从 SRE 视角看,Hooks 机制不仅是代码技巧,更是运维左移的最佳实践 —— 将可观测性设计融入代码架构,而非事后补丁,这也是 VictoriaMetrics 能成为高性能监控系统的核心原因之一。 

🎯 全文核心洞察:三个函数撑起整个可观测性体系

回顾整个 hooks.go,核心代码量不到 200 行,却只用了三个函数就构建了完整的可观测性体系:New(ms) 负责指标采集(7 个 Counter 覆盖全业务),NewLoggerHooks() 负责结构化日志(三层漏斗覆盖全生命周期),Merge() 负责组合扩展(积木式拼装任意观测能力)。

这种设计的精髓在于"约束即自由"——mcp-go 框架通过 server.Hooks 结构体约束了所有可能的挂载点,而 VictoriaMetrics 团队在这些约束内,用最少的代码实现了最大的运维价值。对 SRE 来说,这不仅是一个可以直接复用的可观测性方案,更是一种值得借鉴的架构思维:把横切关注点从业务代码中彻底剥离,让监控成为架构的一部分,而非事后的补丁。

附录:生产环境监控面板(Grafana)核心指标

图表指标表达式说明
客户端分布 sum(mcp_victoriametrics_initialize_total) by (client_name) 各 AI 客户端的连接数
工具调用错误率 rate(mcp_victoriametrics_call_tool_total{is_error="true"}[5m]) / rate(mcp_victoriametrics_call_tool_total[5m]) 按工具名分组展示错误率
资源访问热度 topk(10, sum(mcp_victoriametrics_read_resource_total) by (uri)) 访问量前 10 的资源 URI
全局错误数 rate(mcp_victoriametrics_error_total[5m]) 按 MCP 方法分组展示错误数
活跃会话数 count by (instance) (changes(mcp_victoriametrics_initialize_total[1m])) 每分钟新增会话数
posted @ 2026-04-12 14:14  左扬  阅读(44)  评论(0)    收藏  举报