http.HandleFunc(path.Join(*routePrefix, "/probe"), func(w http.ResponseWriter, r *http.Request) {
sc.Lock()
conf := sc.C
sc.Unlock()
prober.Handler(w, r, conf, logger, rh, *timeoutOffset, nil, moduleUnknownCounter, allowedLevel)
})
我们了解到blackbox_exporter中都是通过请求/probe来进行端口探测的,那么今天我们来详尽的分析prober.Handler相关源码。
目录
- 函数签名
- 1. 获取 URL 查询参数 `params`
- 2. 获取探针模块名称
- 3. 解析超时设置
- 4. 创建上下文(Context)
- 5. 创建 Prometheus 指标
- 6. 获取目标(target)
- 7. 获取探针类型和处理
- 8. 设置 `hostname`(针对 HTTP 或 TCP 探测)
- 9. 设置日志级别
- 10. 开始探测
- 11. 记录结果
- 12. 返回调试输出(如果启用)
- 13. 返回 Prometheus 格式的指标
- 总结
函数签名
func Handler(w http.ResponseWriter, r *http.Request, c *config.Config, logger *slog.Logger, rh *ResultHistory, timeoutOffset float64, params url.Values, moduleUnknownCounter prometheus.Counter, logLevelProber *promslog.AllowedLevel)
w http.ResponseWriter
:HTTP 响应对象,用于向客户端发送响应。r *http.Request
:HTTP 请求对象,包含请求信息。c *config.Config
:包含配置的对象,包含了可用的探针模块等配置。logger *slog.Logger
:日志记录器,用于输出日志。rh *ResultHistory
:用于记录结果历史的对象。timeoutOffset float64
:超时偏移量,可能用于调整默认的超时设置。params url.Values
:URL 查询参数,通常包含了目标和其他探测信息。moduleUnknownCounter prometheus.Counter
:Prometheus 计数器,用于统计未知模块的次数。logLevelProber *promslog.AllowedLevel
:日志级别,控制探测日志的详细程度。
1. 获取 URL 查询参数 params
if params == nil {
params = r.URL.Query()
}
- 如果
params
参数为空(即传入的 URL 查询参数为空),则使用 HTTP 请求的查询参数r.URL.Query()
。
2. 获取探针模块名称
moduleName := params.Get("module")
if moduleName == "" {
moduleName = "http_2xx"
}
module, ok := c.Modules[moduleName]
if !ok {
http.Error(w, fmt.Sprintf("Unknown module %q", moduleName), http.StatusBadRequest)
logger.Debug("Unknown module", "module", moduleName)
if moduleUnknownCounter != nil {
moduleUnknownCounter.Add(1)
}
return
}
- 获取 URL 查询参数中的
module
参数。如果没有传递module
参数,默认设置为http_2xx
。 - 通过
moduleName
从配置c.Modules
中获取对应的模块配置。如果模块不存在,返回 HTTP 错误400 BadRequest
。
3. 解析超时设置
timeoutSeconds, err := getTimeout(r, module, timeoutOffset)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError)
return
}
- 调用
getTimeout
函数从请求头或模块配置中解析超时设置,超时偏移量会影响最终的超时值。 - 如果解析超时出错,则返回
500 InternalServerError
。
4. 创建上下文(Context)
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSeconds*float64(time.Second)))
defer cancel()
r = r.WithContext(ctx)
- 使用
context.WithTimeout
创建一个带有超时设置的上下文ctx
,并将其与请求r
关联。超时会在timeoutSeconds
秒后触发。
5. 创建 Prometheus 指标
probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_success",
Help: "Displays whether or not the probe was a success",
})
probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_duration_seconds",
Help: "Returns how long the probe took to complete in seconds",
})
- 创建两个 Prometheus Gauge 类型的指标:
probe_success
: 表示探测是否成功(1:成功,0:失败)。probe_duration_seconds
: 表示探测完成的时长(单位:秒)。
6. 获取目标(target)
target := params.Get("target")
if target == "" {
http.Error(w, "Target parameter is missing", http.StatusBadRequest)
return
}
- 获取 URL 查询参数中的
target
参数,表示需要探测的目标地址。如果没有提供目标地址,则返回400 BadRequest
错误。
7. 获取探针类型和处理
prober, ok := Probers[module.Prober]
if !ok {
http.Error(w, fmt.Sprintf("Unknown prober %q", module.Prober), http.StatusBadRequest)
return
}
- 根据
module.Prober
获取对应的探针。如果探针类型不存在,则返回400 BadRequest
错误。
8. 设置 hostname
(针对 HTTP 或 TCP 探测)
hostname := params.Get("hostname")
if module.Prober == "http" && hostname != "" {
err = setHTTPHost(hostname, &module)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
if module.Prober == "tcp" && hostname != "" {
if module.TCP.TLSConfig.ServerName == "" {
module.TCP.TLSConfig.ServerName = hostname
}
}
- 如果是
http
探针并且hostname
不为空,则调用setHTTPHost
设置 HTTP 请求的 Host 头。 - 如果是
tcp
探针并且hostname
不为空,则设置 TLS 配置中的ServerName
。
9. 设置日志级别
if logLevelProber == nil {
logLevelProber = &promslog.AllowedLevel{}
}
if logLevelProber.String() == "" {
_ = logLevelProber.Set("info")
}
sl := newScrapeLogger(logger, moduleName, target, logLevelProber)
slLogger := slog.New(sl)
- 如果没有提供
logLevelProber
,则使用默认的日志级别 “info”。 - 创建一个新的日志记录器
slLogger
,用于记录探测过程中的信息。
10. 开始探测
slLogger.Info("Beginning probe", "probe", module.Prober, "timeout_seconds", timeoutSeconds)
start := time.Now()
registry := prometheus.NewRegistry()
registry.MustRegister(probeSuccessGauge)
registry.MustRegister(probeDurationGauge)
success := prober(ctx, target, module, registry, slLogger)
duration := time.Since(start).Seconds()
probeDurationGauge.Set(duration)
if success {
probeSuccessGauge.Set(1)
slLogger.Info("Probe succeeded", "duration_seconds", duration)
} else {
slLogger.Error("Probe failed", "duration_seconds", duration)
}
- 记录日志开始探测。
- 使用
prober
(即相应的探针函数)开始实际的探测操作,并记录探测的持续时间。 - 根据探测结果,设置
probe_success
和probe_duration_seconds
指标。
11. 记录结果
debugOutput := DebugOutput(&module, &sl.buffer, registry)
rh.Add(moduleName, target, debugOutput, success)
- 调用
DebugOutput
函数生成调试输出,并将结果添加到ResultHistory
(用于记录历史结果)。
12. 返回调试输出(如果启用)
if r.URL.Query().Get("debug") == "true" {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(debugOutput))
return
}
- 如果查询参数
debug=true
,则返回调试输出。
13. 返回 Prometheus 格式的指标
h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
h.ServeHTTP(w, r)
- 创建 Prometheus 格式的 HTTP 处理程序,并返回探测结果的指标数据。
总结
它通过解析请求参数来执行指定类型的探测(如 HTTP、TCP 探测),并生成相应的 Prometheus 指标。返回的指标可以被 Prometheus 服务器抓取并进行监控。此外,代码还处理了探测过程中的日志记录和调试输出。
prober(ctx, target, module, registry, slLogger)是整个执行探测的核心部分,下一篇将重点分析此函数