Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:HTTP 路由及二次开发调试
Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:HTTP 路由及二次开发调试
路由相关的具体实现:
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go
API v1 的具体路由注册实现:
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L669-722
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L354-430
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L61-138
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L427-481
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L301-358
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L474-540
https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L371-388
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L536-609
https://github.com/prometheus/prometheus/blob/v3.4.0/storage/remote/read_handler.go#L61-115
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L605-672
https://github.com/prometheus/prometheus/blob/v3.4.0/storage/remote/write_handler.go#L621-660
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L180-254
查询处理器实现(web/api/v1):
https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go
联邦查询处理器(web):
https://github.com/prometheus/prometheus/blob/v3.4.0/web/federate.go#L51-106
https://github.com/prometheus/prometheus/blob/v3.4.0/web/federate.go#L103-179
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L301-358
https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#321-388
https://github.com/prometheus/prometheus/blob/v3.4.0/web/federate.go#L0-55
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L180-254
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L427-481
https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#438-511
https://github.com/prometheus/prometheus/blob/v3.4.0/web/federate.go#L199-258
https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#179-235
https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#264-325
https://github.com/prometheus/prometheus/blob/v3.4.0/web/federate.go#L253-313
在高并发监控场景中,HTTP 路由选择器的分发效率直接决定了 Prometheus Web 服务的响应能力。当每秒处理数千甚至数万请求时,路由匹配的速度将成为系统性能的关键瓶颈之一。
本文将从源码出发,完整拆解 Prometheus 3.4.0 的 HTTP 路由架构,重点分析 GET/POST 方法的前缀树实现,并探讨其高性能设计的核心逻辑。
一、分层设计的核心逻辑
Prometheus 的 HTTP 路由系统采用 "主路由 + 子路由" 的分层架构,通过职责拆分实现高内聚低耦合。这种设计既保证了静态资源、Web UI 与 API 接口的隔离,又通过中间件机制实现了功能的复用。
1.1、核心组件与依赖
Prometheus 并未使用第三方路由库,而是基于自定义的 topicroutetopic 包实现路由功能,核心依赖如下:
-
-
- route.Router:路由核心结构体,维护路径与处理器的映射关系
- 中间件机制:通过 WithInstrumentation 注入监控、日志、路径重写等通用逻辑
- http.ServeMux:基础路由分发器,用于挂载主路由与 API 子路由
- 处理器函数:每个路由对应独立的业务处理函数(如查询、联邦、版本查询等)
-
1.2、初始化流程 —— 从创建到挂载
路由系统的初始化在 web.New() 函数(https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go)中完成,核心步骤分为三步:
1.2.1、主路由创建(初始化带监控中间件的路由,用于处理静态资源和页面请求)
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L311
router := route.New().
WithInstrumentation(m.instrumentHandler). // 注入请求监控
WithInstrumentation(setPathWithPrefix("")) // 路径前缀处理
1.2.2、API v1 子路由创建(独立创建 API 路由,避免与主路由逻辑混淆)
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L690
av1 := route.New().
WithInstrumentation(h.metrics.instrumentHandlerWithPrefix("/api/v1")). // API 专属监控
WithInstrumentation(setPathWithPrefix(apiPath + "/v1")) // API 路径前缀
h.apiV1.Register(av1) // 注册 API 端点
1.2.3、路由挂载(将主路由和 API 子路由挂载到基础 ServeMux)
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L682
mux := http.NewServeMux()
mux.Handle("/", h.router) // 主路由挂载到根路径
mux.Handle(apiPath+"/v1/", http.StripPrefix(apiPath+"/v1", av1)) // API 子路由挂载
二、按功能划分的路由注册机制
Prometheus 按 "功能模块" 划分路由,每个模块对应独立的注册逻辑。
通过源码分析,可将路由分为四大类:静态资源路由、Web UI 路由、API v1 路由、联邦查询路由。
2.1、主路由注册(静态资源和页面路由)
主路由(router)的注册逻辑全部包含在 https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go 的 New 函数中,该函数在初始化 Hander 结构体时,同步完成了处理静态资源、Web UI 入口、健康检查、生命周期等路由的注册。
核心路由及对应源代码如下:
| 路由路径 | 请求方法 | 处理器函数 / 核心逻辑 | 功能描述 | 源码文件及行号(v3.4.0) |
|---|---|---|---|---|
| topic/topic | GET | 重定向到首页(根据配置指向 topic/querytopic/topic/graphtopic/topic/agenttopic) | 根路径统一跳转,适配新 / 旧 UI 及 Agent 模式 | web/web.go L415-L417 |
| topic/graphtopic | GET | 重定向到 topic/querytopic(仅非旧 UI 模式,topic!o.UseOldUItopic 为 true 时生效) | 新 UI 下统一查询入口,替代旧 topic/graphtopic 页面 | web/web.go L419-L422 |
| topic/classic/static/*filepathtopic | GET | 静态文件服务(topicserver.StaticFileServer(ui.Assets)topic),重写路径为 topic/static/*filepathtopic | 兼容旧控制台模板依赖的静态资源(如 JS/CSS) | web/web.go L424-L430 |
| topic/versiontopic | GET | topich.versiontopic(返回 JSON 格式的版本信息) | 暴露 Prometheus 版本、分支、编译时间等元数据 | web/web.go L432 |
| topic/metricstopic | GET | topicpromhttp.Handler().ServeHTTPtopic | 暴露 Prometheus 自身的监控指标(如请求量、耗时) | web/web.go L433 |
| topic/consoles/*filepathtopic | GET | topicreadyf(h.consoles)topic(需服务就绪,返回控制台模板页面) | 提供自定义控制台模板的访问入口(如自定义监控面板) | web/web.go L440 |
| topic/favicon.svgtopic | GET | 静态文件服务(topicui.Assetstopic),路径重写为 topicreactAssetsRoot + "/favicon.svg"topic | 提供浏览器标签页图标 | web/web.go L474-L486(同 topic/favicon.icotopic/topic/manifest.jsontopic) |
| topic/favicon.icotopic | GET | 同上(静态文件服务,适配不同浏览器图标需求) | 兼容旧浏览器的图标访问需求 | web/web.go L474-L486 |
| topic/manifest.jsontopic | GET | 同上(静态文件服务,返回 PWA 应用清单) | 支持将 Web UI 作为渐进式 Web 应用(PWA)添加到桌面 / 手机 | web/web.go L474-L486 |
| topic/assets/*filepathtopic | GET | 静态文件服务(topicui.Assetstopic),路径重写为 topicreactAssetsRoot + "/assets/*filepath"topic | 加载新 UI(Mantine 框架)的静态资源(如组件样式、图片) | web/web.go L488-L494(非旧 UI 模式) |
| topic/static/*filepathtopic | GET | 静态文件服务(topicui.Assetstopic),路径重写为 topicreactAssetsRoot + "/static/*filepath"topic | 加载旧 UI(React 旧框架)的静态资源(仅 topico.UseOldUItopic 为 true 时生效) | web/web.go L488-L494 |
| topic/user/*filepathtopic | GET | topicroute.FileServe(o.UserAssetsPath)topic(用户自定义静态资源服务) | 提供用户本地自定义静态资源的访问入口(如自定义图片、模板) | web/web.go L496-L498 |
| topic/-/quittopic | POST/PUT | topich.quittopic(生命周期开启时)/ 403 响应(生命周期关闭时) | 触发 Prometheus 服务优雅退出 | web/web.go L500-L514 |
| topic/-/quittopic | GET | 返回 405 响应(提示 “仅支持 POST/PUT 方法”) | 限制非法请求方法,保证接口安全性 | web/web.go L516-L519 |
| topic/-/reloadtopic | POST/PUT | topich.reloadtopic(生命周期开启时)/ 403 响应(生命周期关闭时) | 触发 Prometheus 配置重载(如刷新采集规则、告警规则) | web/web.go L500-L514 |
| topic/-/reloadtopic | GET | 返回 405 响应(提示 “仅支持 POST/PUT 方法”) | 限制非法请求方法,保证接口安全性 | web/web.go L521-L523 |
| topic/debug/*subpathtopic | GET/POST | topicserveDebugtopic(内部转发到 pprof 调试接口) | 提供 Go 程序标准调试接口(如 topic/debug/pprof/profiletopic 采集性能样本) | web/web.go L524-L525 |
| topic/-/healthytopic | GET | 返回 200 状态码 + “XXX is Healthy” 文本 | 健康检查接口(判断服务是否启动,不依赖业务就绪) | web/web.go L548-L551 |
| topic/-/healthytopic | HEAD | 返回 200 状态码(无响应体) | 轻量健康检查,适配监控系统 HEAD 请求优化 | web/web.go L552-L555 |
| topic/-/readytopic | GET | topicreadyf(...)topic(服务就绪时返回 200,否则返回 503) | 就绪检查接口(判断服务是否完成初始化,可处理业务请求) | web/web.go L557-L560 |
| topic/-/readytopic | HEAD | topicreadyf(...)topic(同上,无响应体) | 轻量就绪检查,适配监控系统 HEAD 请求优化 | web/web.go L561-L564 |
以下是对该 Newtopic 函数(Prometheus v3.4.0 web 模块路由初始化核心逻辑)的逐行注释,重点标注路由注册、配置依赖、处理器绑定等关键逻辑:
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L305
// New 初始化一个新的 web Handler(核心功能:初始化 Handler 结构体 + 注册所有 web 路由)
func New(logger *slog.Logger, o *Options) *Handler {
// 日志器容错:若传入 logger 为 nil,使用空日志器(避免空指针)
if logger == nil {
logger = promslog.NewNopLogger()
}
// 初始化监控指标:基于传入的注册器(Registerer)创建 metrics 实例,用于路由请求的指标统计
m := newMetrics(o.Registerer)
// 初始化路由管理器:
// 1. 绑定指标监控中间件(m.instrumentHandler,统计路由请求耗时、成功/失败数等)
// 2. 绑定路径前缀设置中间件(setPathWithPrefix(""),初始化路径前缀为空)
router := route.New().
WithInstrumentation(m.instrumentHandler).
WithInstrumentation(setPathWithPrefix(""))
// 获取当前工作目录(CWD):用于后续记录服务运行路径,失败时记录错误信息
cwd, err := os.Getwd()
if err != nil {
cwd = "<error retrieving current working directory>"
}
// 初始化 Handler 核心结构体:聚合所有依赖(日志、监控、路由、配置、存储等)
h := &Handler{
logger: logger, // 日志器
gatherer: o.Gatherer, // 指标采集器(用于暴露 /metrics)
metrics: m, // 自定义监控指标实例
router: router, // 路由管理器
quitCh: make(chan struct{}),// 服务退出信号通道
reloadCh: make(chan chan error),// 配置重载信号通道(带错误返回)
options: o, // 传入的 web 配置选项
versionInfo: o.Version, // 服务版本信息
birth: time.Now().UTC(), // 服务启动时间(UTC 时区)
cwd: cwd, // 服务运行目录
flagsMap: o.Flags, // 命令行参数映射
// 核心业务依赖注入(Prometheus 核心功能模块)
context: o.Context, // 根上下文(用于服务生命周期管理)
scrapeManager: o.ScrapeManager, // 采集管理器(管理目标采集任务)
ruleManager: o.RuleManager, // 规则管理器(管理告警/记录规则)
queryEngine: o.QueryEngine, // 查询引擎(处理 PromQL 查询)
lookbackDelta: o.LookbackDelta, // 查询回溯时间窗口
storage: o.Storage, // 主存储(长期指标存储)
localStorage: o.LocalStorage, // 本地存储(短期/临时指标)
exemplarStorage: o.ExemplarStorage, // 样本存储(关联指标的原始样本)
notifier: o.Notifier, // 告警通知器(发送告警到 Alertmanager)
now: model.Now, // 时间获取函数(默认当前时间,便于测试 mock)
}
// 设置服务初始状态为「未就绪」(NotReady),就绪后通过其他逻辑更新
h.SetReady(NotReady)
// 初始化 API v1 所需的「资源检索器工厂函数」:
// 作用是为 API 提供统一的资源访问入口,解耦 API 与具体模块的依赖
factorySPr := func(_ context.Context) api_v1.ScrapePoolsRetriever { return h.scrapeManager } // 采集池检索器(API 查采集池)
factoryTr := func(_ context.Context) api_v1.TargetRetriever { return h.scrapeManager } // 采集目标检索器(API 查采集目标)
factoryAr := func(_ context.Context) api_v1.AlertmanagerRetriever { return h.notifier } // Alertmanager 检索器(API 查告警接收器)
FactoryRr := func(_ context.Context) api_v1.RulesRetriever { return h.ruleManager } // 规则检索器(API 查告警/记录规则)
// 初始化 Appendable 存储:仅当启用远程写入接收(RemoteWriteReceiver)或 OTLP 写入接收时,使用主存储
var app storage.Appendable
if o.EnableRemoteWriteReceiver || o.EnableOTLPWriteReceiver {
app = h.storage
}
// 初始化 API v1 实例:绑定所有核心依赖,为后续路由提供 API 能力
h.apiV1 = api_v1.NewAPI(
h.queryEngine, // 查询引擎
h.storage, // 主存储
app, // 可追加存储(远程写入用)
h.exemplarStorage, // 样本存储
factorySPr, // 采集池检索器工厂
factoryTr, // 采集目标检索器工厂
factoryAr, // Alertmanager 检索器工厂
// 配置获取函数(带读锁,保证配置读取线程安全)
func() config.Config {
h.mtx.RLock() // 加读锁(防止配置更新时并发读取)
defer h.mtx.RUnlock() // 释放读锁
return *h.config
},
o.Flags, // 命令行参数
// API 全局 URL 配置(用于生成正确的外部链接)
api_v1.GlobalURLOptions{
ListenAddress: o.ListenAddresses[0], // 监听地址(第一个)
Host: o.ExternalURL.Host, // 外部访问 Host
Scheme: o.ExternalURL.Scheme, // 外部访问协议(http/https)
},
h.testReady, // 就绪状态检查函数(API 就绪性校验)
h.options.LocalStorage, // 本地存储
h.options.TSDBDir, // TSDB 存储目录
h.options.EnableAdminAPI, // 是否启用 Admin API(如删除指标)
logger, // 日志器
FactoryRr, // 规则检索器工厂
h.options.RemoteReadSampleLimit, // 远程读取样本数限制
h.options.RemoteReadConcurrencyLimit, // 远程读取并发数限制
h.options.RemoteReadBytesInFrame, // 远程读取单帧字节数限制
h.options.IsAgent, // 是否为 Agent 模式(区别于 Server 模式)
h.options.CORSOrigin, // CORS 跨域配置
h.runtimeInfo, // 运行时信息(如内存、CPU 占用)
h.versionInfo, // 版本信息
h.options.NotificationsGetter, // 告警通知查询器(API 查告警历史)
h.options.NotificationsSub, // 告警通知订阅器(API 订阅告警)
o.Gatherer, // 指标采集器
o.Registerer, // 指标注册器
nil, // 预留参数(暂无使用)
o.EnableRemoteWriteReceiver, // 是否启用远程写入接收
o.AcceptRemoteWriteProtoMsgs, // 是否接受 Protobuf 格式的远程写入
o.EnableOTLPWriteReceiver, // 是否启用 OTLP 写入接收
o.ConvertOTLPDelta, // 是否将 OTLP Delta 指标转为 Cumulative
o.NativeOTLPDeltaIngestion, // 是否原生支持 OTLP Delta 指标摄入
o.CTZeroIngestionEnabled, // 是否启用 CT Zero 指标摄入
)
// 路由前缀配置:若配置了非根路径前缀(RoutePrefix != "/")
if o.RoutePrefix != "/" {
// 根路径重定向:访问 "/" 时,重定向到配置的前缀路径(避免 404)
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, o.RoutePrefix, http.StatusFound)
})
// 为所有后续注册的路由添加前缀(批量生效)
router = router.WithPrefix(o.RoutePrefix)
}
// 首页路径配置:根据模式选择默认首页
homePage := "/query" // 默认首页:新 UI 的查询页面
if o.UseOldUI {
homePage = "/graph" // 旧 UI 模式:首页为 /graph
}
if o.IsAgent {
homePage = "/agent" // Agent 模式:首页为 /agent
}
// 就绪状态检查函数:复用 Handler 的 testReady 方法(判断服务是否就绪)
readyf := h.testReady
// 根路径重定向:访问路由前缀根路径(如 /prometheus/)时,重定向到配置的首页
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
// 拼接外部 URL 路径 + 首页路径(保证重定向地址正确)
http.Redirect(w, r, path.Join(o.ExternalURL.Path, homePage), http.StatusFound)
})
// 旧 UI 兼容:非旧 UI 模式下,访问 /graph 重定向到 /query(统一新 UI 入口)
if !o.UseOldUI {
router.Get("/graph", func(w http.ResponseWriter, r *http.Request) {
// 保留原查询参数(RawQuery),避免参数丢失
http.Redirect(w, r, path.Join(o.ExternalURL.Path, "/query?"+r.URL.RawQuery), http.StatusFound)
})
}
// React 前端资源根路径配置:根据 UI 模式选择资源目录
reactAssetsRoot := "/static/mantine-ui" // 新 UI:Mantine 组件库资源目录
if h.options.UseOldUI {
reactAssetsRoot = "/static/react-app" // 旧 UI:旧 React 应用资源目录
}
// 旧控制台兼容路由:为 /classic/static/*filepath 提供静态资源服务
// 用途:旧控制台库(如 console_libraries/prom.lib)依赖该路径下的资源
router.Get("/classic/static/*filepath", func(w http.ResponseWriter, r *http.Request) {
// 路径重写:将 /classic/static/xxx 转为 /static/xxx(匹配 ui.Assets 中的资源路径)
r.URL.Path = path.Join("/static", route.Param(r.Context(), "filepath"))
// 使用静态文件服务:从 ui.Assets(嵌入式资源)中读取文件
fs := server.StaticFileServer(ui.Assets)
fs.ServeHTTP(w, r)
})
// 版本信息路由:GET /version,调用 h.version 处理器返回版本信息
router.Get("/version", h.version)
// 自身监控指标路由:GET /metrics,使用 promhttp 提供标准 Prometheus 指标暴露
router.Get("/metrics", promhttp.Handler().ServeHTTP)
// 联邦查询路由:GET /federate
// 1. 就绪校验:通过 readyf 中间件,未就绪时返回 503
// 2. 压缩处理:使用 httputil.CompressionHandler 压缩响应(减少网络传输)
// 3. 处理器:调用 h.federation 处理联邦查询请求
router.Get("/federate", readyf(httputil.CompressionHandler{
Handler: http.HandlerFunc(h.federation),
}.ServeHTTP))
// 控制台页面路由:GET /consoles/*filepath
// 1. 就绪校验:未就绪时拒绝访问
// 2. 处理器:调用 h.consoles 加载控制台页面资源(支持自定义控制台)
router.Get("/consoles/*filepath", readyf(h.consoles))
// React 应用服务函数:动态生成 React 首页 HTML(替换占位符为实际配置)
serveReactApp := func(w http.ResponseWriter, _ *http.Request) {
// 拼接 React 首页 HTML 路径(从嵌入式资源中读取)
indexPath := reactAssetsRoot + "/index.html"
f, err := ui.Assets.Open(indexPath)
if err != nil {
// 资源读取失败:返回 500 并提示错误
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error opening React index.html: %v", err)
return
}
// 延迟关闭文件句柄(避免资源泄漏)
defer func() { _ = f.Close() }()
// 读取 HTML 文件内容
idx, err := io.ReadAll(f)
if err != nil {
// 内容读取失败:返回 500 并提示错误
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error reading React index.html: %v", err)
return
}
// 替换 HTML 中的占位符为实际配置(让前端获取后端配置)
replacedIdx := bytes.ReplaceAll(idx, []byte("CONSOLES_LINK_PLACEHOLDER"), []byte(h.consolesPath())) // 控制台链接
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("TITLE_PLACEHOLDER"), []byte(h.options.PageTitle)) // 页面标题
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("AGENT_MODE_PLACEHOLDER"), []byte(strconv.FormatBool(h.options.IsAgent))) // 是否为 Agent 模式
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("READY_PLACEHOLDER"), []byte(strconv.FormatBool(h.isReady()))) // 服务就绪状态
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("LOOKBACKDELTA_PLACEHOLDER"), []byte(model.Duration(h.options.LookbackDelta).String())) // 查询回溯窗口
// 返回替换后的 HTML 内容(前端渲染入口)
w.Write(replacedIdx)
}
// React 路由路径配置:根据 UI 模式选择前端路由列表
reactRouterPaths := newUIReactRouterPaths // 新 UI 前端路由(如 /query、/alerts)
reactRouterServerPaths := newUIReactRouterServerPaths // 新 UI Server 模式路由
if h.options.UseOldUI {
reactRouterPaths = oldUIReactRouterPaths // 旧 UI 前端路由
reactRouterServerPaths = oldUIReactRouterServerPaths // 旧 UI Server 模式路由
}
// 注册 React 前端路由:为所有前端路由绑定 serveReactApp 处理器(前端路由复用首页 HTML)
for _, p := range reactRouterPaths {
router.Get(p, serveReactApp)
}
// Agent 模式额外路由:注册 Agent 模式专属的前端路由
if h.options.IsAgent {
for _, p := range reactRouterAgentPaths {
router.Get(p, serveReactApp)
}
} else {
// Server 模式额外路由:注册 Server 模式专属的前端路由
for _, p := range reactRouterServerPaths {
router.Get(p, serveReactApp)
}
}
// 根路径静态资源路由:为浏览器图标、应用清单提供服务(需根路径访问,如 /favicon.ico)
for _, p := range []string{"/favicon.svg", "/favicon.ico", "/manifest.json"} {
// 拼接资源在嵌入式资产中的实际路径
assetPath := reactAssetsRoot + p
router.Get(p, func(w http.ResponseWriter, r *http.Request) {
// 路径重写:将根路径资源请求转为嵌入式资产中的路径
r.URL.Path = assetPath
fs := server.StaticFileServer(ui.Assets)
fs.ServeHTTP(w, r)
})
}
// React 静态资源目录配置:根据 UI 模式选择资源目录
reactStaticAssetsDir := "/assets" // 新 UI:静态资源目录(如 /assets/css、/assets/js)
if h.options.UseOldUI {
reactStaticAssetsDir = "/static" // 旧 UI:静态资源目录(如 /static/css)
}
// React 静态资源路由:为 /assets/* 或 /static/* 提供静态资源服务
router.Get(reactStaticAssetsDir+"/*filepath", func(w http.ResponseWriter, r *http.Request) {
// 路径重写:拼接资产根路径 + 静态资源目录 + 文件路径(匹配嵌入式资源)
r.URL.Path = path.Join(reactAssetsRoot+reactStaticAssetsDir, route.Param(r.Context(), "filepath"))
fs := server.StaticFileServer(ui.Assets)
fs.ServeHTTP(w, r)
})
// 用户自定义静态资源路由:若配置了 UserAssetsPath(用户本地资源目录)
if o.UserAssetsPath != "" {
// 注册 /user/*filepath 路由,从用户指定目录加载静态资源
router.Get("/user/*filepath", route.FileServe(o.UserAssetsPath))
}
// 生命周期 API 配置:若启用生命周期管理(EnableLifecycle = true)
if o.EnableLifecycle {
// 服务退出路由:POST/PUT /-/quit,调用 h.quit 触发服务退出
router.Post("/-/quit", h.quit)
router.Put("/-/quit", h.quit)
// 配置重载路由:POST/PUT /-/reload,调用 h.reload 触发配置重载
router.Post("/-/reload", h.reload
2.2、API v1 路由注册(监控核心接口)
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L359
API v1 路由不包含在主路由中,而是通过独立的 topicapi_v1.APItopictopic(https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L62topictopic)topic 结构体注册,最终挂载到主 topicServeMuxtopic 上。整个流程分为两步:
2.2.1、初始化 API v1 实例
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L359
在 New 函数中,通过 api_v1.NewAPI 初始化 API 处理实例,传入查询引擎、存储、配置等核心依赖,为路由注册做准备。
h.apiV1 = api_v1.NewAPI(h.queryEngine, h.storage, app, h.exemplarStorage, factorySPr, factoryTr, factoryAr,
func() config.Config { /* 配置获取逻辑 */ },
o.Flags,
api_v1.GlobalURLOptions{ /* URL 配置 */ },
// 其他依赖参数...
)
2.2.2、注册并挂载 API v1 路由
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L682-695
- 第 690-692 行:创建 API v1 专属路由 av1,并添加监控中间件
- 第 693 行:调用 h.apiV1.Register(av1) 注册所有 API 端点
- 第 695 行:通过 http.StripPrefix 将 API v1 路由挂载到主 ServeMux
- 第 374-389 行:在 web/api/v1/api.go 中注册核心监控接口,包括 /query、/query_range、/labels 等
在 Handler.Run 函数中,创建 API v1 专属路由(av1 https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L690),调用 h.apiV1.Register(av1) 注册所有 API 端点(https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L374-389),最后通过 http.StripPrefix 挂载到主 ServeMux(https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L690)。
# https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L690
mux := http.NewServeMux() mux.Handle("/", h.router) apiPath := "/api" if h.options.RoutePrefix != "/" { apiPath = h.options.RoutePrefix + apiPath h.logger.Info("Router prefix", "prefix", h.options.RoutePrefix) } av1 := route.New(). WithInstrumentation(h.metrics.instrumentHandlerWithPrefix("/api/v1")). WithInstrumentation(setPathWithPrefix(apiPath + "/v1")) h.apiV1.Register(av1) mux.Handle(apiPath+"/v1/", http.StripPrefix(apiPath+"/v1", av1))
-
-
-
- 注册核心端点:包含 /query/,/query_range/,/labels 等监控接口,逻辑在 https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go 的 Register 函数(https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L339)中(如 L371-L388 注册 /query/,/query_range)。
- 挂载代码:
// 源码位置:web/web.go L611-L620 av1 := route.New(). WithInstrumentation(h.metrics.instrumentHandlerWithPrefix("/api/v1")). WithInstrumentation(setPathWithPrefix(apiPath + "/v1")) h.apiV1.Register(av1) mux.Handle(apiPath+"/v1/", http.StripPrefix(apiPath+"/v1", av1))
-
-
2.3、联邦查询路由(跨实例数据聚合)
联邦查询路由属于主路由的一部分,并非独立模块,其注册和处理逻辑如下:
- 路由注册:在 New 函数中,通过 router.Get("/federate", ...) 注册,处理器为 h.federation(https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go#L440-442)。
- 核心逻辑:h.federation 函数(定义在 https://github.com/prometheus/prometheus/blob/v3.4.0/web/federate.go L51-L106)负责解析 match[] 参数、查询本地存储数据,并通过 expfmt 格式返回聚合结果,支持跨 Prometheus 实例的数据拉取。
三、路由注册与源码映射表
该表基于 https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go 和 https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go 核心源码,按 "主路由(静态 / 基础功能)" 、"API v1 路由(监控核心)" 和 "联邦查询路由" 三大类划分,明确每个路由的路径、方法、处理器及精确源码位置,确保与实际代码完全对齐。
3.1、主路由注册,请参考本文 `主路由注册(静态资源和页面路由)`
3.2、API v1 路由:监控核心功能路由
API v1 路由通过 api_v1.API 结构体独立注册,在 web/handler.go 的 Run 函数中挂载到主 ServeMux,负责处理所有监控核心操作(如查询、标签管理、元数据获取)。
| 路由路径 | 请求方法 | 处理器函数(topicapi_v1.APItopic 内方法) | 功能描述 | 源码文件及行号(v3.4.0) |
|---|---|---|---|---|
| topic/querytopic | GET/POST | topicapi.querytopic | 即时查询(获取单个时间点的监控指标数据,支持 PromQL) | web/api/v1/api.go L374-L375 |
| topic/query_rangetopic | GET/POST | topicapi.queryRangetopic | 范围查询(获取指定时间段内的监控指标数据,支持 PromQL) | web/api/v1/api.go L376-L377 |
| topic/query_exemplarstopic | GET/POST | topicapi.queryExemplarstopic | 样本查询(获取监控指标对应的 exemplar 数据,用于追踪链路关联) | web/api/v1/api.go L378-L379 |
| topic/format_querytopic | GET/POST | topicapi.formatQuerytopic | 查询语句格式化(美化 PromQL 语句,便于阅读和调试) | web/api/v1/api.go L381-L382 |
| topic/parse_querytopic | GET/POST | topicapi.parseQuerytopic | 查询语句解析(校验 PromQL 语法正确性,返回解析结果) | web/api/v1/api.go L383-L384 |
| topic/labelstopic | GET/POST | topicapi.labelNamestopic | 标签名查询(获取所有监控指标的标签键(label key)列表) | web/api/v1/api.go L386-L387 |
| topic/label/:name/valuestopic | GET/POST | topicapi.labelValuestopic | 标签值查询(获取指定标签键对应的所有标签值(label value)列表) | web/api/v1/api.go L388 |
| topic/seriestopic | GET/POST | topicapi.seriestopic | 时间序列查询(根据标签筛选条件,获取匹配的时间序列列表) | web/api/v1/api.go L390-L391(源码中紧随 topic/label/:name/valuestopic 注册) |
| topic/metadatatopic | GET/POST | topicapi.metadatatopic | 指标元数据查询(获取监控指标的类型、帮助信息、标签等元数据) | web/api/v1/api.go L393-L394 |
3.2、联邦查询路由——跨实例数据聚合路由
联邦查询路由属于主路由的子集,专门用于跨 Prometheus 实例聚合数据,注册和处理逻辑独立于普通 API。
| 路由路径 | 请求方法 | 处理器函数 / 核心逻辑 | 功能描述 | 源码文件及行号(v3.4.0) |
|---|---|---|---|---|
| topic/federatetopic | GET | topicreadyf(httputil.CompressionHandler{ Handler: http.HandlerFunc(h.federation) })topic | 联邦查询入口(接收 topicmatch[]topic 参数,返回符合条件的监控指标,支持跨实例聚合) | 注册逻辑:web/web.go L435-L438;处理逻辑:web/federate.go L51-L106 |
四、Prometheus 3.4.0 二次开发时路由调试
在实际部署或二次开发中,路由匹配异常(如请求 404、接口无响应)是常见问题。本文从“日志定位”、“有效性验证”、“问题排查”三个核心场景出发,提供可直接操作的步骤,帮助快速定位并解决路由相关问题。
4.1、如何通过日志定位路由匹配过程
Prometheus 默认不输出路由匹配细节,需通过 调整日志级别 和 启用请求追踪日志,完整记录请求从接收、路由匹配到处理器调用的全流程。
路由匹配的核心日志(如路径解析、中间件执行)属于 topicdebugtopic 级别,需通过启动参数或配置文件开启(推荐,临时生效):
./prometheus --log.level=debug --config.file=prometheus.yml
-
-
- --log.level=debug:将全局日志级别设为 debug,路由相关日志(如 route 包的匹配日志)会被输出。
-
通过配置文件开启(长期生效):
# 在 prometheus.yml 中添加日志配置(需 Prometheus 3.0+ 支持): global: scrape_interval: 15s log: level: debug # 全局日志级别 format: logfmt # 日志格式,logfmt 或 json(json 便于机器解析)
开启 topicdebugtopic 日志后,可通过关键字过滤路由相关日志,核心日志类型及含义如下:
| 日志关键字 | 日志含义 | 示例日志(简化) |
|---|---|---|
| topicroute matchedtopic | 路由匹配成功,包含请求方法、路径、匹配到的处理器名称 | topiclevel=debug ts=2024-05-20T10:30:00Z caller=route/route.go:123 msg="route matched" method=GET path=/api/v1/query handler=api_v1.querytopic |
| topicroute not foundtopic | 路由匹配失败,请求路径或方法不存在 | topiclevel=debug ts=2024-05-20T10:31:00Z caller=route/route.go:135 msg="route not found" method=POST path=/api/v1/unknowntopic |
| topicinstrumentHandlertopic | 中间件执行日志,包含请求耗时、处理器名称(监控中间件输出) | topiclevel=debug ts=2024-05-20T10:32:00Z caller=web/metrics.go:45 msg="instrumentHandler" handler=api_v1.query duration=120mstopic |
| topichttp server: servingtopic | HTTP 服务器接收请求的初始日志,包含客户端 IP、请求方法、路径 | topiclevel=debug ts=2024-05-20T10:29:00Z caller=web/handler.go:680 msg="http server: serving" client=192.168.1.100:54321 method=GET path=/metricstopic |
4.2、启用请求追踪,关联全链路日志
若需追踪单个请求的完整链路(从接收→路由匹配→中间件→处理器→响应),可通过 HTTP 请求头传递追踪 ID,并在日志中关联该 ID:
4.2.1、客户端发送请求时添加追踪头(如 topicX-Trace-IDtopic)
curl -H "X-Trace-ID: abc123" http://localhost:9090/api/v1/query?query=up
4.2.2、修改 Prometheus 日志配置(需二次开发或使用自定义中间件)
在 withStackTracer 中间件(https://github.com/prometheus/prometheus/blob/v3.4.0/web/web.go L178-L195)中添加追踪头日志,示例修改:
func withStackTracer(h http.Handler, l *slog.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 提取追踪头
traceID := r.Header.Get("X-Trace-ID")
if traceID == "" {
traceID = uuid.NewString() // 无追踪头时生成随机 ID
}
// 日志中添加 traceID 字段
l = l.With("traceID", traceID)
defer func() {
if err := recover(); err != nil {
// 异常日志关联 traceID
l.Error("panic while serving request", "client", r.RemoteAddr, "url", r.URL, "err", err, "stack", buf)
}
}()
// 请求处理日志关联 traceID
l.Debug("serving request", "method", r.Method, "path", r.URL.Path, "traceID", traceID)
h.ServeHTTP(w, r)
})
}

浙公网安备 33010602011771号