blackbox_exporter是Prometheus监控系统中用于在agent机器上采集http,DNS,ICMP相关信息,通过prometheus传递的参数和target,映射到对应的agent的web接口上进行处理。比如下面的采集配置来自于prometheus的配置文件
scrape_configs:
- job_name: 'blackbox'
metrics_path: /probe
params:
module: [http_2xx] # Look for a HTTP 200 response.
static_configs:
- targets:
- http://prometheus.io # Target to probe with http.
- https://prometheus.io # Target to probe with https.
- http://example.com:8080 # Target to probe with http on port 8080.
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9115 # The blackbox exporter's real hostname:port.
配置信息中定义了采集的路径,并定义了http传递的参数为module:[http_2xx]并且传递了提供采集的目的机器列表。这与直接发送http请求的效果一致http://localhost:9115/probe?target=http://prometheus.io&module=http_2xx,只不过需要发送多个请求去获得列表的内容。
配置文件主要用来配置支持的模块及采集的方式,比如我们可以定义dns采集模块dns_udp和dns_tcp分别用来采集target(测试目标机器)对于指定域名的数据采集
dns:
prober: dns
timeout: 5s
dns:
query_type: "A"
query_name: "example.com"
dns_tcp:
prober: dns
timeout: 5s
dns:
transport_protocol: "tcp"
preferred_ip_protocol: "ip4"
query_name: "example.com"
我们可以看下具体的配置文件的导入是如何实现的:
if err := sc.ReloadConfig(*configFile); err != nil {
level.Error(logger).Log("msg", "Error loading config", "err", err)
os.Exit(1)
}
上面的代码执行去导入blackbox.yml(默认配置)的内容,如果导入成功的话,所有的配置信息将保存在sc.C【Config结构体】下面。Config的定义如下。包含了一个map结构,对应了自定义的模块名称及module的配置信息。
type Config struct {
Modules map[string]Module `yaml:"modules"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
}
module 包含了具体的配置内容,但是为了满足支持的组件,module定义如下。任何一个module中都包含了所有Probe支持的结构体,只需要指定一个组件使用,存储在Prober中,具体的配置则保存在各自的结构体中,比如如果probe: dns则查找配置需要到DNS中去获取。
type Module struct {
Prober string `yaml:"prober,omitempty"`
Timeout time.Duration `yaml:"timeout,omitempty"`
HTTP HTTPProbe `yaml:"http,omitempty"`
TCP TCPProbe `yaml:"tcp,omitempty"`
ICMP ICMPProbe `yaml:"icmp,omitempty"`
DNS DNSProbe `yaml:"dns,omitempty"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
}
具体的导入工作通过内部实现的不同组件重载yaml模块下的Unmarshaler接口实现yml文件到结构体的导入。比如下面的是DNSProbe结构体的导入。另外checkOverflow用于检查是否有未知的配置写到了配置文件中。
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (s *DNSProbe) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain DNSProbe
if err := unmarshal((*plain)(s)); err != nil {
return err
}
if err := checkOverflow(s.XXX, "dns probe"); err != nil {
return err
}
if s.QueryName == "" {
return errors.New("Query name must be set for DNS module")
}
return nil
}
这样导入后的配置Config结构体则包含了所有的自定义模块的信息,包含模块的名称和具体的配置。这样当我们使用接口访问的时候,则通过指定模块名称和target即可实现针对目标对象的采集。
blackbox_exporter默认情况下的/metrics路径只采集go运行的相关信息,核心的处理逻辑放在了/probe路由处理函数中处理。
http.HandleFunc("/probe", func(w http.ResponseWriter, r *http.Request) {
sc.Lock()
conf := sc.C
sc.Unlock()
probeHandler(w, r, conf, logger, rh)
})
为了防止重载期间对于数据的采集问题,使用锁机制保护访问, 实际调用的函数位于probeHandler中。
func probeHandler(w http.ResponseWriter, r *http.Request, c *config.Config, logger log.Logger, rh *resultHistory) {
moduleName := r.URL.Query().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)
return
}
函数首先查询module参数是否存在,如果不存在则默认使用http_2xx模块(前提是config中配置了该模块)。当获得名称后则查询config.Modules中是否存在对应的模块如果没有的话则返回400错误。
var timeoutSeconds float64
if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" {
var err error
timeoutSeconds, err = strconv.ParseFloat(v, 64)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError)
return
}
}
if timeoutSeconds == 0 {
timeoutSeconds = 10
}
if module.Timeout.Seconds() < timeoutSeconds && module.Timeout.Seconds() > 0 {
timeoutSeconds = module.Timeout.Seconds()
}
如果header中指定了采集的超时时间,则使用此值作为超时探测时间,如果没有设置则设置10s,两者取最小值。
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second)))
defer cancel()
r = r.WithContext(ctx)
上下文用来管理超时,当超时时间到达后,直接关闭任何的查询连接,并返回。
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",
})
params := r.URL.Query()
target := params.Get("target")
if target == "" {
http.Error(w, "Target parameter is missing", http.StatusBadRequest)
return
}
定义基本的采集指标,这两个指标不管是任何配置模块,均会输出在结果中,用来记录是否成功,以及采集时间。获取target的参数,如果没有设置target,同样退出。
prober, ok := Probers[module.Prober]
if !ok {
http.Error(w, fmt.Sprintf("Unknown prober %q", module.Prober), http.StatusBadRequest)
return
}
根据配置中定义的Prober名称获得具体的模块配置。比如Prober为dns则查询Probers中DNS的处理函数。去执行后续的操作。
start := time.Now()
registry := prometheus.NewRegistry()
registry.MustRegister(probeSuccessGauge)
registry.MustRegister(probeDurationGauge)
success := prober(ctx, target, module, registry, sl)
duration := time.Since(start).Seconds()
probeDurationGauge.Set(duration)
if success {
probeSuccessGauge.Set(1)
level.Info(sl).Log("msg", "Probe succeeded", "duration_seconds", duration)
} else {
level.Error(sl).Log("msg", "Probe failed", "duration_seconds", duration)
}
具体的查询逻辑,首先注册两个之前定义的指标对象,并执行实际的抓取操作(prober)传递了超时的上下文,查询对象以及loger等参数。通过返回值和时间来设置对应的指标数据。
另外每个不同模块都具有不同的指标内容,这在不同的模块采集过程中定义。
针对不同的模块都有自己实现的采集模块,这里仅以DNS模块的采集实现来看一下代码的具体实现:
func ProbeDNS(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) bool {
var dialProtocol string
probeDNSAnswerRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_dns_answer_rrs",
Help: "Returns number of entries in the answer resource record list",
})
probeDNSAuthorityRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_dns_authority_rrs",
Help: "Returns number of entries in the authority resource record list",
})
probeDNSAdditionalRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_dns_additional_rrs",
Help: "Returns number of entries in the additional resource record list",
})
registry.MustRegister(probeDNSAnswerRRSGauge)
registry.MustRegister(probeDNSAuthorityRRSGauge)
registry.MustRegister(probeDNSAdditionalRRSGauge)
函数开头定义和注册了三个Gauge对象,这三个对象用来存储查询的结果(应答记录,权威记录以及其他记录数量)
根据实际的配置比如协议,另外targer如果包含端口则分开主机和端口,默认端口为53. 并通过go-DNS库(github.com/miekg/dns)来实现发送根据配置好的DNS请求。
probeDNSAnswerRRSGauge.Set(float64(len(response.Answer)))
probeDNSAuthorityRRSGauge.Set(float64(len(response.Ns)))
probeDNSAdditionalRRSGauge.Set(float64(len(response.Extra)))
最后更新指标信息。并且如果配置了独立的验证则通过定义的valid函数来执行制定配置的验证信息比如验证响应信息是否符合要求
level.Info(logger).Log("msg", "Validating Answer RRs")
if !validRRs(&response.Answer, &module.DNS.ValidateAnswer, logger) {
level.Error(logger).Log("msg", "Answer RRs validation failed")
return false
}
level.Info(logger).Log("msg", "Validating Authority RRs")
if !validRRs(&response.Ns, &module.DNS.ValidateAuthority, logger) {
level.Error(logger).Log("msg", "Authority RRs validation failed")
return false
}
level.Info(logger).Log("msg", "Validating Additional RRs")
if !validRRs(&response.Extra, &module.DNS.ValidateAdditional, logger) {
level.Error(logger).Log("msg", "Additional RRs validation failed")
return false
}
具体的验证代码如下:
func validRRs(rrs *[]dns.RR, v *config.DNSRRValidator, logger log.Logger) bool {
// Fail the probe if there are no RRs of a given type, but a regexp match is required
// (i.e. FailIfNotMatchesRegexp is set).
if len(*rrs) == 0 && len(v.FailIfNotMatchesRegexp) > 0 {
level.Error(logger).Log("msg", "fail_if_not_matches_regexp specified but no RRs returned")
return false
}
for _, rr := range *rrs {
level.Info(logger).Log("msg", "Validating RR", "rr", rr)
for _, re := range v.FailIfMatchesRegexp {
match, err := regexp.MatchString(re, rr.String())
if err != nil {
level.Error(logger).Log("msg", "Error matching regexp", "regexp", re, "err", err)
return false
}
if match {
level.Error(logger).Log("msg", "RR matched regexp", "regexp", re, "rr", rr)
return false
}
}
for _, re := range v.FailIfNotMatchesRegexp {
match, err := regexp.MatchString(re, rr.String())
if err != nil {
level.Error(logger).Log("msg", "Error matching regexp", "regexp", re, "err", err)
return false
}
if !match {
level.Error(logger).Log("msg", "RR did not match regexp", "regexp", re, "rr", rr)
return false
}
}
}
return true
}
另外black_exporter支持通过web进行reload的方式,以及通过发送SIGNAL的方式进行配置文件的重载。信号的重载模式通过启动一个独立的goroutine来实现,捕获信号,并执行重载操作:
hup := make(chan os.Signal)
reloadCh := make(chan chan error)
signal.Notify(hup, syscall.SIGHUP)
go func() {
for {
select {
case <-hup:
if err := sc.ReloadConfig(*configFile); err != nil {
level.Error(logger).Log("msg", "Error reloading config", "err", err)
continue
}
level.Info(logger).Log("msg", "Reloaded config file")
case rc := <-reloadCh:
if err := sc.ReloadConfig(*configFile); err != nil {
level.Error(logger).Log("msg", "Error reloading config", "err", err)
rc <- err
} else {
level.Info(logger).Log("msg", "Reloaded config file")
rc <- nil
}
}
}
}()
当通过web方式触发的时候,通过向管道发送信号,则实现独立goroutine中的重载操作,重载过程中会阻塞其他的采集工作。
http.HandleFunc("/-/reload",
func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(w, "This endpoint requires a POST request.\n")
return
}
rc := make(chan error)
reloadCh <- rc
if err := <-rc; err != nil {
http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError)
}
})
对于标准的go运行环境的检测通过promhttp.Handler直接实现。
http.Handle(“/metrics”, promhttp.Handler())
这就是black_exporter的具体实现,关于其他模块的实现此处不多描述。