// Copyright 2015 The Prometheus Authors // 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 discovery import ( "fmt" "net" "strings" "sync" "time" "github.com/golang/glog" "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus" clientmodel "github.com/prometheus/client_golang/model" "github.com/prometheus/prometheus/config" ) const ( resolvConf = "/etc/resolv.conf" dnsSourcePrefix = "dns" // Constants for instrumentation. namespace = "prometheus" interval = "interval" ) var ( dnsSDLookupsCount = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: namespace, Name: "dns_sd_lookups_total", Help: "The number of DNS-SD lookups.", }) dnsSDLookupFailuresCount = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: namespace, Name: "dns_sd_lookup_failures_total", Help: "The number of DNS-SD lookup failures.", }) ) func init() { prometheus.MustRegister(dnsSDLookupFailuresCount) prometheus.MustRegister(dnsSDLookupsCount) } // DNSDiscovery periodically performs DNS-SD requests. It implements // the TargetProvider interface. type DNSDiscovery struct { name string done chan struct{} ticker *time.Ticker m sync.RWMutex } // NewDNSDiscovery returns a new DNSDiscovery which periodically refreshes its targets. func NewDNSDiscovery(name string, refreshInterval time.Duration) *DNSDiscovery { return &DNSDiscovery{ name: name, done: make(chan struct{}), ticker: time.NewTicker(refreshInterval), } } // Run implements the TargetProvider interface. func (dd *DNSDiscovery) Run(ch chan<- *config.TargetGroup) { defer close(ch) // Get an initial set right away. if err := dd.refresh(ch); err != nil { glog.Errorf("Error refreshing DNS targets: %s", err) } for { select { case <-dd.ticker.C: if err := dd.refresh(ch); err != nil { glog.Errorf("Error refreshing DNS targets: %s", err) } case <-dd.done: return } } } // Stop implements the TargetProvider interface. func (dd *DNSDiscovery) Stop() { glog.V(1).Info("Stopping DNS discovery for %s...", dd.name) dd.ticker.Stop() dd.done <- struct{}{} glog.V(1).Info("DNS discovery for %s stopped.", dd.name) } // Sources implements the TargetProvider interface. func (dd *DNSDiscovery) Sources() []string { return []string{dnsSourcePrefix + ":" + dd.name} } func (dd *DNSDiscovery) refresh(ch chan<- *config.TargetGroup) error { response, err := lookupSRV(dd.name) dnsSDLookupsCount.Inc() if err != nil { dnsSDLookupFailuresCount.Inc() return err } tg := &config.TargetGroup{} for _, record := range response.Answer { addr, ok := record.(*dns.SRV) if !ok { glog.Warningf("%q is not a valid SRV record", record) continue } // Remove the final dot from rooted DNS names to make them look more usual. addr.Target = strings.TrimRight(addr.Target, ".") target := clientmodel.LabelValue(fmt.Sprintf("%s:%d", addr.Target, addr.Port)) tg.Targets = append(tg.Targets, clientmodel.LabelSet{ clientmodel.AddressLabel: target, }) } tg.Source = dnsSourcePrefix + ":" + dd.name ch <- tg return nil } func lookupSRV(name string) (*dns.Msg, error) { conf, err := dns.ClientConfigFromFile(resolvConf) if err != nil { return nil, fmt.Errorf("could not load resolv.conf: %s", err) } client := &dns.Client{} response := &dns.Msg{} for _, server := range conf.Servers { servAddr := net.JoinHostPort(server, conf.Port) for _, suffix := range conf.Search { response, err = lookup(name, dns.TypeSRV, client, servAddr, suffix, false) if err != nil { glog.Warningf("resolving %s.%s failed: %s", name, suffix, err) continue } if len(response.Answer) > 0 { return response, nil } } response, err = lookup(name, dns.TypeSRV, client, servAddr, "", false) if err == nil { return response, nil } } return response, fmt.Errorf("could not resolve %s: No server responded", name) } func lookup(name string, queryType uint16, client *dns.Client, servAddr string, suffix string, edns bool) (*dns.Msg, error) { msg := &dns.Msg{} lname := strings.Join([]string{name, suffix}, ".") msg.SetQuestion(dns.Fqdn(lname), queryType) if edns { opt := &dns.OPT{ Hdr: dns.RR_Header{ Name: ".", Rrtype: dns.TypeOPT, }, } opt.SetUDPSize(dns.DefaultMsgSize) msg.Extra = append(msg.Extra, opt) } response, _, err := client.Exchange(msg, servAddr) if err != nil { return nil, err } if msg.Id != response.Id { return nil, fmt.Errorf("DNS ID mismatch, request: %d, response: %d", msg.Id, response.Id) } if response.MsgHdr.Truncated { if client.Net == "tcp" { return nil, fmt.Errorf("got truncated message on tcp") } if edns { // Truncated even though EDNS is used client.Net = "tcp" } return lookup(name, queryType, client, servAddr, suffix, !edns) } return response, nil }