blackbox_exporter源码阅读

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模块)

针对不同的模块都有自己实现的采集模块,这里仅以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的具体实现,关于其他模块的实现此处不多描述。

你可能感兴趣的:(自动化运维,golang,exporter,prometheus,golang)