Remove the word middleware (#1067)

* Rename middleware to plugin

first pass; mostly used 'sed', few spots where I manually changed
text.

This still builds a coredns binary.

* fmt error

* Rename AddMiddleware to AddPlugin

* Readd AddMiddleware to remain backwards compat
This commit is contained in:
Miek Gieben
2017-09-14 09:36:06 +01:00
committed by GitHub
parent b984aa4559
commit d8714e64e4
354 changed files with 974 additions and 969 deletions

53
plugin/metrics/README.md Normal file
View File

@@ -0,0 +1,53 @@
# prometheus
This module enables prometheus metrics for CoreDNS.
The default location for the metrics is `localhost:9153`. The metrics path is fixed to `/metrics`.
The following metrics are exported:
* coredns_dns_request_count_total{zone, proto, family}
* coredns_dns_request_duration_milliseconds{zone}
* coredns_dns_request_size_bytes{zone, proto}
* coredns_dns_request_do_count_total{zone}
* coredns_dns_request_type_count_total{zone, type}
* coredns_dns_response_size_bytes{zone, proto}
* coredns_dns_response_rcode_count_total{zone, rcode}
Each counter has a label `zone` which is the zonename used for the request/response.
Extra labels used are:
* `proto` which holds the transport of the response ("udp" or "tcp")
* The address family (`family`) of the transport (1 = IP (IP version 4), 2 = IP6 (IP version 6)).
* `type` which holds the query type. It holds most common types (A, AAAA, MX, SOA, CNAME, PTR, TXT,
NS, SRV, DS, DNSKEY, RRSIG, NSEC, NSEC3, IXFR, AXFR and ANY) and "other" which lumps together all
other types.
* The `response_rcode_count_total` has an extra label `rcode` which holds the rcode of the response.
If monitoring is enabled, queries that do not enter the plugin chain are exported under the fake
name "dropped" (without a closing dot - this is never a valid domain name).
## Syntax
~~~
prometheus [ADDRESS]
~~~
For each zone that you want to see metrics for.
It optionally takes an address to which the metrics are exported; the default
is `localhost:9153`. The metrics path is fixed to `/metrics`.
## Examples
Use an alternative address:
~~~
prometheus localhost:9253
~~~
# Bugs
When reloading, we keep the handler running, meaning that any changes to the handler's address
aren't picked up. You'll need to restart CoreDNS for that to happen.

34
plugin/metrics/handler.go Normal file
View File

@@ -0,0 +1,34 @@
package metrics
import (
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics/vars"
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
"github.com/coredns/coredns/plugin/pkg/rcode"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
// ServeDNS implements the Handler interface.
func (m *Metrics) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
qname := state.QName()
zone := plugin.Zones(m.ZoneNames()).Matches(qname)
if zone == "" {
zone = "."
}
// Record response to get status code and size of the reply.
rw := dnsrecorder.New(w)
status, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, rw, r)
vars.Report(state, zone, rcode.ToString(rw.Rcode), rw.Len, rw.Start)
return status, err
}
// Name implements the Handler interface.
func (m *Metrics) Name() string { return "prometheus" }

101
plugin/metrics/metrics.go Normal file
View File

@@ -0,0 +1,101 @@
// Package metrics implement a handler and plugin that provides Prometheus metrics.
package metrics
import (
"log"
"net"
"net/http"
"sync"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics/vars"
"github.com/prometheus/client_golang/prometheus"
)
func init() {
prometheus.MustRegister(vars.RequestCount)
prometheus.MustRegister(vars.RequestDuration)
prometheus.MustRegister(vars.RequestSize)
prometheus.MustRegister(vars.RequestDo)
prometheus.MustRegister(vars.RequestType)
prometheus.MustRegister(vars.ResponseSize)
prometheus.MustRegister(vars.ResponseRcode)
}
// Metrics holds the prometheus configuration. The metrics' path is fixed to be /metrics
type Metrics struct {
Next plugin.Handler
Addr string
ln net.Listener
mux *http.ServeMux
zoneNames []string
zoneMap map[string]bool
zoneMu sync.RWMutex
}
// AddZone adds zone z to m.
func (m *Metrics) AddZone(z string) {
m.zoneMu.Lock()
m.zoneMap[z] = true
m.zoneNames = keys(m.zoneMap)
m.zoneMu.Unlock()
}
// RemoveZone remove zone z from m.
func (m *Metrics) RemoveZone(z string) {
m.zoneMu.Lock()
delete(m.zoneMap, z)
m.zoneNames = keys(m.zoneMap)
m.zoneMu.Unlock()
}
// ZoneNames returns the zones of m.
func (m *Metrics) ZoneNames() []string {
m.zoneMu.RLock()
s := m.zoneNames
m.zoneMu.RUnlock()
return s
}
// OnStartup sets up the metrics on startup.
func (m *Metrics) OnStartup() error {
ln, err := net.Listen("tcp", m.Addr)
if err != nil {
log.Printf("[ERROR] Failed to start metrics handler: %s", err)
return err
}
m.ln = ln
ListenAddr = m.ln.Addr().String()
m.mux = http.NewServeMux()
m.mux.Handle("/metrics", prometheus.Handler())
go func() {
http.Serve(m.ln, m.mux)
}()
return nil
}
// OnShutdown tears down the metrics on shutdown and restart.
func (m *Metrics) OnShutdown() error {
if m.ln != nil {
return m.ln.Close()
}
return nil
}
func keys(m map[string]bool) []string {
sx := []string{}
for k := range m {
sx = append(sx, k)
}
return sx
}
// ListenAddr is assigned the address of the prometheus listener. Its use is mainly in tests where
// we listen on "localhost:0" and need to retrieve the actual address.
var ListenAddr string

View File

@@ -0,0 +1,83 @@
package metrics
import (
"testing"
"github.com/coredns/coredns/plugin"
mtest "github.com/coredns/coredns/plugin/metrics/test"
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
func TestMetrics(t *testing.T) {
met := &Metrics{Addr: "localhost:0", zoneMap: make(map[string]bool)}
if err := met.OnStartup(); err != nil {
t.Fatalf("Failed to start metrics handler: %s", err)
}
defer met.OnShutdown()
met.AddZone("example.org.")
tests := []struct {
next plugin.Handler
qname string
qtype uint16
metric string
expectedValue string
}{
// This all works because 1 bucket (1 zone, 1 type)
{
next: test.NextHandler(dns.RcodeSuccess, nil),
qname: "example.org",
metric: "coredns_dns_request_count_total",
expectedValue: "1",
},
{
next: test.NextHandler(dns.RcodeSuccess, nil),
qname: "example.org",
metric: "coredns_dns_request_count_total",
expectedValue: "2",
},
{
next: test.NextHandler(dns.RcodeSuccess, nil),
qname: "example.org",
metric: "coredns_dns_request_type_count_total",
expectedValue: "3",
},
{
next: test.NextHandler(dns.RcodeSuccess, nil),
qname: "example.org",
metric: "coredns_dns_response_rcode_count_total",
expectedValue: "4",
},
}
ctx := context.TODO()
for i, tc := range tests {
req := new(dns.Msg)
if tc.qtype == 0 {
tc.qtype = dns.TypeA
}
req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype)
met.Next = tc.next
rec := dnsrecorder.New(&test.ResponseWriter{})
_, err := met.ServeDNS(ctx, rec, req)
if err != nil {
t.Fatalf("Test %d: Expected no error, but got %s", i, err)
}
result := mtest.Scrape(t, "http://"+ListenAddr+"/metrics")
if tc.expectedValue != "" {
got, _ := mtest.MetricValue(tc.metric, result)
if got != tc.expectedValue {
t.Errorf("Test %d: Expected value %s for metrics %s, but got %s", i, tc.expectedValue, tc.metric, got)
}
}
}
}

100
plugin/metrics/setup.go Normal file
View File

@@ -0,0 +1,100 @@
package metrics
import (
"net"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/mholt/caddy"
)
func init() {
caddy.RegisterPlugin("prometheus", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
uniqAddr = addrs{a: make(map[string]int)}
}
func setup(c *caddy.Controller) error {
m, err := prometheusParse(c)
if err != nil {
return plugin.Error("prometheus", err)
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
m.Next = next
return m
})
for a, v := range uniqAddr.a {
if v == todo {
// During restarts we will keep this handler running, BUG.
c.OncePerServerBlock(m.OnStartup)
}
uniqAddr.a[a] = done
}
c.OnFinalShutdown(m.OnShutdown)
return nil
}
func prometheusParse(c *caddy.Controller) (*Metrics, error) {
var (
met = &Metrics{Addr: addr, zoneMap: make(map[string]bool)}
err error
)
defer func() {
uniqAddr.SetAddress(met.Addr)
}()
for c.Next() {
if len(met.ZoneNames()) > 0 {
return met, c.Err("can only have one metrics module per server")
}
for _, z := range c.ServerBlockKeys {
met.AddZone(plugin.Host(z).Normalize())
}
args := c.RemainingArgs()
switch len(args) {
case 0:
case 1:
met.Addr = args[0]
_, _, e := net.SplitHostPort(met.Addr)
if e != nil {
return met, e
}
default:
return met, c.ArgErr()
}
}
return met, err
}
var uniqAddr addrs
// Keep track on which addrs we listen, so we only start one listener.
type addrs struct {
a map[string]int
}
func (a *addrs) SetAddress(addr string) {
// If already there and set to done, we've already started this listener.
if a.a[addr] == done {
return
}
a.a[addr] = todo
}
// Addr is the address the where the metrics are exported by default.
const addr = "localhost:9153"
const (
todo = 1
done = 2
)

View File

@@ -0,0 +1,42 @@
package metrics
import (
"testing"
"github.com/mholt/caddy"
)
func TestPrometheusParse(t *testing.T) {
tests := []struct {
input string
shouldErr bool
addr string
}{
// oks
{`prometheus`, false, "localhost:9153"},
{`prometheus localhost:53`, false, "localhost:53"},
// fails
{`prometheus {}`, true, ""},
{`prometheus /foo`, true, ""},
{`prometheus a b c`, true, ""},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
m, err := prometheusParse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %v: Expected error but found nil", i)
continue
} else if !test.shouldErr && err != nil {
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
continue
}
if test.shouldErr {
continue
}
if test.addr != m.Addr {
t.Errorf("Test %v: Expected address %s but found: %s", i, test.addr, m.Addr)
}
}
}

View File

@@ -0,0 +1,225 @@
// Adapted by Miek Gieben for CoreDNS testing.
//
// License from prom2json
// Copyright 2014 Prometheus Team
// 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 test will scrape a target and you can inspect the variables.
// Basic usage:
//
// result := Scrape("http://localhost:9153/metrics")
// v := MetricValue("coredns_cache_capacity", result)
//
package test
import (
"fmt"
"io"
"mime"
"net/http"
"testing"
"github.com/matttproud/golang_protobuf_extensions/pbutil"
"github.com/prometheus/common/expfmt"
dto "github.com/prometheus/client_model/go"
)
type (
// MetricFamily holds a prometheus metric.
MetricFamily struct {
Name string `json:"name"`
Help string `json:"help"`
Type string `json:"type"`
Metrics []interface{} `json:"metrics,omitempty"` // Either metric or summary.
}
// metric is for all "single value" metrics.
metric struct {
Labels map[string]string `json:"labels,omitempty"`
Value string `json:"value"`
}
summary struct {
Labels map[string]string `json:"labels,omitempty"`
Quantiles map[string]string `json:"quantiles,omitempty"`
Count string `json:"count"`
Sum string `json:"sum"`
}
histogram struct {
Labels map[string]string `json:"labels,omitempty"`
Buckets map[string]string `json:"buckets,omitempty"`
Count string `json:"count"`
Sum string `json:"sum"`
}
)
// Scrape returns the all the vars a []*metricFamily.
func Scrape(t *testing.T, url string) []*MetricFamily {
mfChan := make(chan *dto.MetricFamily, 1024)
go fetchMetricFamilies(url, mfChan)
result := []*MetricFamily{}
for mf := range mfChan {
result = append(result, newMetricFamily(mf))
}
return result
}
// MetricValue returns the value associated with name as a string as well as the labels.
// It only returns the first metrics of the slice.
func MetricValue(name string, mfs []*MetricFamily) (string, map[string]string) {
for _, mf := range mfs {
if mf.Name == name {
// Only works with Gauge and Counter...
return mf.Metrics[0].(metric).Value, mf.Metrics[0].(metric).Labels
}
}
return "", nil
}
// MetricValueLabel returns the value for name *and* label *value*.
func MetricValueLabel(name, label string, mfs []*MetricFamily) (string, map[string]string) {
// bit hacky is this really handy...?
for _, mf := range mfs {
if mf.Name == name {
for _, m := range mf.Metrics {
for _, v := range m.(metric).Labels {
if v == label {
return m.(metric).Value, m.(metric).Labels
}
}
}
}
}
return "", nil
}
func newMetricFamily(dtoMF *dto.MetricFamily) *MetricFamily {
mf := &MetricFamily{
Name: dtoMF.GetName(),
Help: dtoMF.GetHelp(),
Type: dtoMF.GetType().String(),
Metrics: make([]interface{}, len(dtoMF.Metric)),
}
for i, m := range dtoMF.Metric {
if dtoMF.GetType() == dto.MetricType_SUMMARY {
mf.Metrics[i] = summary{
Labels: makeLabels(m),
Quantiles: makeQuantiles(m),
Count: fmt.Sprint(m.GetSummary().GetSampleCount()),
Sum: fmt.Sprint(m.GetSummary().GetSampleSum()),
}
} else if dtoMF.GetType() == dto.MetricType_HISTOGRAM {
mf.Metrics[i] = histogram{
Labels: makeLabels(m),
Buckets: makeBuckets(m),
Count: fmt.Sprint(m.GetHistogram().GetSampleCount()),
Sum: fmt.Sprint(m.GetSummary().GetSampleSum()),
}
} else {
mf.Metrics[i] = metric{
Labels: makeLabels(m),
Value: fmt.Sprint(value(m)),
}
}
}
return mf
}
func value(m *dto.Metric) float64 {
if m.Gauge != nil {
return m.GetGauge().GetValue()
}
if m.Counter != nil {
return m.GetCounter().GetValue()
}
if m.Untyped != nil {
return m.GetUntyped().GetValue()
}
return 0.
}
func makeLabels(m *dto.Metric) map[string]string {
result := map[string]string{}
for _, lp := range m.Label {
result[lp.GetName()] = lp.GetValue()
}
return result
}
func makeQuantiles(m *dto.Metric) map[string]string {
result := map[string]string{}
for _, q := range m.GetSummary().Quantile {
result[fmt.Sprint(q.GetQuantile())] = fmt.Sprint(q.GetValue())
}
return result
}
func makeBuckets(m *dto.Metric) map[string]string {
result := map[string]string{}
for _, b := range m.GetHistogram().Bucket {
result[fmt.Sprint(b.GetUpperBound())] = fmt.Sprint(b.GetCumulativeCount())
}
return result
}
func fetchMetricFamilies(url string, ch chan<- *dto.MetricFamily) {
defer close(ch)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return
}
req.Header.Add("Accept", acceptHeader)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
mediatype, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err == nil && mediatype == "application/vnd.google.protobuf" &&
params["encoding"] == "delimited" &&
params["proto"] == "io.prometheus.client.MetricFamily" {
for {
mf := &dto.MetricFamily{}
if _, err = pbutil.ReadDelimited(resp.Body, mf); err != nil {
if err == io.EOF {
break
}
return
}
ch <- mf
}
} else {
// We could do further content-type checks here, but the
// fallback for now will anyway be the text format
// version 0.0.4, so just go for it and see if it works.
var parser expfmt.TextParser
metricFamilies, err := parser.TextToMetricFamilies(resp.Body)
if err != nil {
return
}
for _, mf := range metricFamilies {
ch <- mf
}
}
}
const acceptHeader = `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.7,text/plain;version=0.0.4;q=0.3`

View File

@@ -0,0 +1,62 @@
package vars
import (
"time"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// Report reports the metrics data associcated with request.
func Report(req request.Request, zone, rcode string, size int, start time.Time) {
// Proto and Family.
net := req.Proto()
fam := "1"
if req.Family() == 2 {
fam = "2"
}
typ := req.QType()
RequestCount.WithLabelValues(zone, net, fam).Inc()
RequestDuration.WithLabelValues(zone).Observe(float64(time.Since(start) / time.Millisecond))
if req.Do() {
RequestDo.WithLabelValues(zone).Inc()
}
if _, known := monitorType[typ]; known {
RequestType.WithLabelValues(zone, dns.Type(typ).String()).Inc()
} else {
RequestType.WithLabelValues(zone, other).Inc()
}
ResponseSize.WithLabelValues(zone, net).Observe(float64(size))
RequestSize.WithLabelValues(zone, net).Observe(float64(req.Len()))
ResponseRcode.WithLabelValues(zone, rcode).Inc()
}
var monitorType = map[uint16]bool{
dns.TypeAAAA: true,
dns.TypeA: true,
dns.TypeCNAME: true,
dns.TypeDNSKEY: true,
dns.TypeDS: true,
dns.TypeMX: true,
dns.TypeNSEC3: true,
dns.TypeNSEC: true,
dns.TypeNS: true,
dns.TypePTR: true,
dns.TypeRRSIG: true,
dns.TypeSOA: true,
dns.TypeSRV: true,
dns.TypeTXT: true,
// Meta Qtypes
dns.TypeIXFR: true,
dns.TypeAXFR: true,
dns.TypeANY: true,
}
const other = "other"

View File

@@ -0,0 +1,69 @@
package vars
import (
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
// Request* and Response* are the prometheus counters and gauges we are using for exporting metrics.
var (
RequestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "request_count_total",
Help: "Counter of DNS requests made per zone, protocol and family.",
}, []string{"zone", "proto", "family"})
RequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "request_duration_milliseconds",
Buckets: append(prometheus.DefBuckets, []float64{50, 100, 200, 500, 1000, 2000, 3000, 4000, 5000, 10000}...),
Help: "Histogram of the time (in milliseconds) each request took.",
}, []string{"zone"})
RequestSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "request_size_bytes",
Help: "Size of the EDNS0 UDP buffer in bytes (64K for TCP).",
Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3},
}, []string{"zone", "proto"})
RequestDo = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "request_do_count_total",
Help: "Counter of DNS requests with DO bit set per zone.",
}, []string{"zone"})
RequestType = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "request_type_count_total",
Help: "Counter of DNS requests per type, per zone.",
}, []string{"zone", "type"})
ResponseSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "response_size_bytes",
Help: "Size of the returned response in bytes.",
Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3},
}, []string{"zone", "proto"})
ResponseRcode = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "response_rcode_count_total",
Help: "Counter of response status codes.",
}, []string{"zone", "rcode"})
)
const (
subsystem = "dns"
// Dropped indicates we dropped the query before any handling. It has no closing dot, so it can not be a valid zone.
Dropped = "dropped"
)