// Copyright 2017 Kumina, https://kumina.nl/ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bufio" "crypto/tls" "crypto/x509" "flag" "fmt" "io" "io/ioutil" "log" "net/http" "os" "regexp" "strconv" "strings" "github.com/prometheus/client_golang/prometheus" ) var ( unboundUpDesc = prometheus.NewDesc( prometheus.BuildFQName("unbound", "", "up"), "Whether scraping Unbound's metrics was successful.", nil, nil) unboundMetrics = []*unboundMetric{ newUnboundMetric( "answer_rcodes_total", "Total number of answers to queries, from cache or from recursion, by response code.", prometheus.CounterValue, []string{"rcode"}, "^num\\.answer\\.rcode\\.(\\w+)$"), newUnboundMetric( "answers_bogus", "Total number of answers that were bogus.", prometheus.CounterValue, nil, "^num\\.answer\\.bogus$"), newUnboundMetric( "answers_secure_total", "Total number of answers that were secure.", prometheus.CounterValue, nil, "^num\\.answer\\.secure$"), newUnboundMetric( "cache_hits_total", "Total number of queries that were successfully answered using a cache lookup.", prometheus.CounterValue, []string{"thread"}, "^thread(\\d+)\\.num\\.cachehits$"), newUnboundMetric( "cache_misses_total", "Total number of cache queries that needed recursive processing.", prometheus.CounterValue, []string{"thread"}, "^thread(\\d+)\\.num\\.cachemiss$"), newUnboundMetric( "memory_caches_bytes", "Memory in bytes in use by caches.", prometheus.GaugeValue, []string{"cache"}, "^mem\\.cache\\.(\\w+)$"), newUnboundMetric( "memory_modules_bytes", "Memory in bytes in use by modules.", prometheus.GaugeValue, []string{"module"}, "^mem\\.mod\\.(\\w+)$"), newUnboundMetric( "memory_sbrk_bytes", "Memory in bytes allocated through sbrk.", prometheus.GaugeValue, nil, "^mem\\.total\\.sbrk$"), newUnboundMetric( "prefetches_total", "Total number of cache prefetches performed.", prometheus.CounterValue, []string{"thread"}, "^thread(\\d+)\\.num\\.prefetch$"), newUnboundMetric( "queries_total", "Total number of queries received.", prometheus.CounterValue, []string{"thread"}, "^thread(\\d+)\\.num\\.queries$"), newUnboundMetric( "query_classes_total", "Total number of queries with a given query class.", prometheus.CounterValue, []string{"class"}, "^num\\.query\\.class\\.([\\w]+)$"), newUnboundMetric( "query_flags_total", "Total number of queries that had a given flag set in the header.", prometheus.CounterValue, []string{"flag"}, "^num\\.query\\.flags\\.([\\w]+)$"), newUnboundMetric( "query_ipv6_total", "Total number of queries that were made using IPv6 towards the Unbound server.", prometheus.CounterValue, nil, "^num\\.query\\.ipv6$"), newUnboundMetric( "query_opcodes_total", "Total number of queries with a given query opcode.", prometheus.CounterValue, []string{"opcode"}, "^num\\.query\\.opcode\\.([\\w]+)$"), newUnboundMetric( "query_edns_DO_total", "Total number of queries that had an EDNS OPT record with the DO (DNSSEC OK) bit set present.", prometheus.CounterValue, nil, "^num\\.query\\.edns\\.DO$"), newUnboundMetric( "query_edns_present_total", "Total number of queries that had an EDNS OPT record present.", prometheus.CounterValue, nil, "^num\\.query\\.edns\\.present$"), newUnboundMetric( "query_tcp_total", "Total number of queries that were made using TCP towards the Unbound server.", prometheus.CounterValue, nil, "^num\\.query\\.tcp$"), newUnboundMetric( "query_types_total", "Total number of queries with a given query type.", prometheus.CounterValue, []string{"type"}, "^num\\.query\\.type\\.([\\w]+)$"), newUnboundMetric( "request_list_current_all", "Current size of the request list, including internally generated queries.", prometheus.GaugeValue, []string{"thread"}, "^thread([0-9]+)\\.requestlist\\.current\\.all$"), newUnboundMetric( "request_list_current_user", "Current size of the request list, only counting the requests from client queries.", prometheus.GaugeValue, []string{"thread"}, "^thread([0-9]+)\\.requestlist\\.current\\.user$"), newUnboundMetric( "request_list_exceeded_total", "Number of queries that were dropped because the request list was full.", prometheus.CounterValue, []string{"thread"}, "^thread([0-9]+)\\.requestlist\\.exceeded$"), newUnboundMetric( "request_list_overwritten_total", "Total number of requests in the request list that were overwritten by newer entries.", prometheus.CounterValue, []string{"thread"}, "^thread([0-9]+)\\.requestlist\\.overwritten$"), newUnboundMetric( "recursive_replies_total", "Total number of replies sent to queries that needed recursive processing.", prometheus.CounterValue, []string{"thread"}, "^thread(\\d+)\\.num\\.recursivereplies$"), newUnboundMetric( "response_time_seconds_bucket", "Histogram buckets for query response time in seconds.", prometheus.CounterValue, []string{"le"}, "^histogram\\.\\d+\\.\\d+\\.to\\.0*(\\d+\\.\\d+?)0*$"), newUnboundMetric( "rrset_bogus_total", "Total number of rrsets marked bogus by the validator.", prometheus.CounterValue, nil, "^num\\.rrset\\.bogus$"), newUnboundMetric( "time_elapsed_seconds", "Time since last statistics printout in seconds.", prometheus.CounterValue, nil, "^time\\.elapsed$"), newUnboundMetric( "time_now_seconds", "Current time in seconds since 1970.", prometheus.GaugeValue, nil, "^time\\.now$"), newUnboundMetric( "time_up_seconds_total", "Uptime since server boot in seconds.", prometheus.CounterValue, nil, "^time\\.up$"), newUnboundMetric( "unwanted_queries_total", "Total number of queries that were refused or dropped because they failed the access control settings.", prometheus.CounterValue, nil, "^unwanted\\.queries$"), newUnboundMetric( "unwanted_replies_total", "Total number of replies that were unwanted or unsolicited.", prometheus.CounterValue, nil, "^unwanted\\.replies$"), } ) type unboundMetric struct { desc *prometheus.Desc valueType prometheus.ValueType pattern *regexp.Regexp } func newUnboundMetric(name string, description string, valueType prometheus.ValueType, labels []string, pattern string) *unboundMetric { return &unboundMetric{ desc: prometheus.NewDesc( prometheus.BuildFQName("unbound", "", name), description, labels, nil), valueType: valueType, pattern: regexp.MustCompile(pattern), } } func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error { scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) for scanner.Scan() { fields := strings.Split(scanner.Text(), "=") if len(fields) != 2 { return fmt.Errorf( "%q is not a valid key-value pair", scanner.Text()) } gotMatch := false for _, metric := range unboundMetrics { matches := metric.pattern.FindStringSubmatch(fields[0]) if matches != nil { value, err := strconv.ParseFloat(fields[1], 64) if err != nil { return err } ch <- prometheus.MustNewConstMetric( metric.desc, metric.valueType, value, matches[1:]...) gotMatch = true break } } if !gotMatch { // log.Printf("Metric %q doesn't match and pattern!", fields[0]) } } return scanner.Err() } func CollectFromFile(path string, ch chan<- prometheus.Metric) error { conn, err := os.Open(path) if err != nil { return err } return CollectFromReader(conn, ch) } func CollectFromSocket(host string, tlsConfig *tls.Config, ch chan<- prometheus.Metric) error { conn, err := tls.Dial("tcp", host, tlsConfig) if err != nil { return err } _, err = conn.Write([]byte("UBCT1 stats_noreset\n")) if err != nil { return err } return CollectFromReader(conn, ch) } type UnboundExporter struct { host string tlsConfig tls.Config } func NewUnboundExporter(host string, ca string, cert string, key string) (*UnboundExporter, error) { /* Server authentication. */ caData, err := ioutil.ReadFile(ca) if err != nil { return &UnboundExporter{}, err } roots := x509.NewCertPool() if !roots.AppendCertsFromPEM(caData) { return &UnboundExporter{}, fmt.Errorf("Failed to parse CA") } /* Client authentication. */ certData, err := ioutil.ReadFile(cert) if err != nil { return &UnboundExporter{}, err } keyData, err := ioutil.ReadFile(key) if err != nil { return &UnboundExporter{}, err } keyPair, err := tls.X509KeyPair(certData, keyData) if err != nil { return &UnboundExporter{}, err } return &UnboundExporter{ host: host, tlsConfig: tls.Config{ Certificates: []tls.Certificate{keyPair}, RootCAs: roots, ServerName: "unbound", }, }, nil } func (e *UnboundExporter) Describe(ch chan<- *prometheus.Desc) { ch <- unboundUpDesc for _, metric := range unboundMetrics { ch <- metric.desc } } func (e *UnboundExporter) Collect(ch chan<- prometheus.Metric) { err := CollectFromSocket(e.host, &e.tlsConfig, ch) if err == nil { ch <- prometheus.MustNewConstMetric( unboundUpDesc, prometheus.GaugeValue, 1.0) } else { log.Printf("Failed to scrape socket: %s", err) ch <- prometheus.MustNewConstMetric( unboundUpDesc, prometheus.GaugeValue, 0.0) } } func main() { var ( listenAddress = flag.String("web.listen-address", ":9167", "Address to listen on for web interface and telemetry.") metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") unboundHost = flag.String("unbound.host", "localhost:8953", "Unbound control socket hostname and port number.") unboundCa = flag.String("unbound.ca", "/etc/unbound/unbound_server.pem", "Unbound server certificate.") unboundCert = flag.String("unbound.cert", "/etc/unbound/unbound_control.pem", "Unbound client certificate.") unboundKey = flag.String("unbound.key", "/etc/unbound/unbound_control.key", "Unbound client key.") ) flag.Parse() exporter, err := NewUnboundExporter(*unboundHost, *unboundCa, *unboundCert, *unboundKey) if err != nil { panic(err) } prometheus.MustRegister(exporter) http.Handle(*metricsPath, prometheus.Handler()) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`