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號