From ce572546e54cf17145f155335d2180e7758a76f5 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Mon, 7 Aug 2017 16:37:52 -0700 Subject: [PATCH 1/6] vendor: update go-metrics --- .../armon/go-metrics/circonus/circonus.go | 27 ++++ .../armon/go-metrics/datadog/dogstatsd.go | 73 ++++++----- vendor/github.com/armon/go-metrics/inmem.go | 95 ++++++++++---- .../armon/go-metrics/inmem_endpoint.go | 118 ++++++++++++++++++ .../armon/go-metrics/inmem_signal.go | 6 +- vendor/github.com/armon/go-metrics/metrics.go | 99 +++++++++++++-- vendor/github.com/armon/go-metrics/sink.go | 32 +++-- vendor/github.com/armon/go-metrics/start.go | 33 +++++ vendor/github.com/armon/go-metrics/statsd.go | 24 ++++ .../github.com/armon/go-metrics/statsite.go | 23 ++++ vendor/vendor.json | 6 +- 11 files changed, 463 insertions(+), 73 deletions(-) create mode 100644 vendor/github.com/armon/go-metrics/inmem_endpoint.go diff --git a/vendor/github.com/armon/go-metrics/circonus/circonus.go b/vendor/github.com/armon/go-metrics/circonus/circonus.go index c6e3974b5dd4..eb41b9945514 100644 --- a/vendor/github.com/armon/go-metrics/circonus/circonus.go +++ b/vendor/github.com/armon/go-metrics/circonus/circonus.go @@ -5,6 +5,7 @@ package circonus import ( "strings" + "github.com/armon/go-metrics" cgm "github.com/circonus-labs/circonus-gometrics" ) @@ -61,6 +62,12 @@ func (s *CirconusSink) SetGauge(key []string, val float32) { s.metrics.SetGauge(flatKey, int64(val)) } +// SetGaugeWithLabels sets value for a gauge metric with the given labels +func (s *CirconusSink) SetGaugeWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.metrics.SetGauge(flatKey, int64(val)) +} + // EmitKey is not implemented in circonus func (s *CirconusSink) EmitKey(key []string, val float32) { // NOP @@ -72,12 +79,24 @@ func (s *CirconusSink) IncrCounter(key []string, val float32) { s.metrics.IncrementByValue(flatKey, uint64(val)) } +// IncrCounterWithLabels increments a counter metric with the given labels +func (s *CirconusSink) IncrCounterWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.metrics.IncrementByValue(flatKey, uint64(val)) +} + // AddSample adds a sample to a histogram metric func (s *CirconusSink) AddSample(key []string, val float32) { flatKey := s.flattenKey(key) s.metrics.RecordValue(flatKey, float64(val)) } +// AddSampleWithLabels adds a sample to a histogram metric with the given labels +func (s *CirconusSink) AddSampleWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.metrics.RecordValue(flatKey, float64(val)) +} + // Flattens key to Circonus metric name func (s *CirconusSink) flattenKey(parts []string) string { joined := strings.Join(parts, "`") @@ -90,3 +109,11 @@ func (s *CirconusSink) flattenKey(parts []string) string { } }, joined) } + +// Flattens the key along with labels for formatting, removes spaces +func (s *CirconusSink) flattenKeyLabels(parts []string, labels []metrics.Label) string { + for _, label := range labels { + parts = append(parts, label.Value) + } + return s.flattenKey(parts) +} diff --git a/vendor/github.com/armon/go-metrics/datadog/dogstatsd.go b/vendor/github.com/armon/go-metrics/datadog/dogstatsd.go index aaba9fe0e224..fe021d01c0fc 100644 --- a/vendor/github.com/armon/go-metrics/datadog/dogstatsd.go +++ b/vendor/github.com/armon/go-metrics/datadog/dogstatsd.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/DataDog/datadog-go/statsd" + "github.com/armon/go-metrics" ) // DogStatsdSink provides a MetricSink that can be used @@ -45,46 +46,49 @@ func (s *DogStatsdSink) EnableHostNamePropagation() { func (s *DogStatsdSink) flattenKey(parts []string) string { joined := strings.Join(parts, ".") - return strings.Map(func(r rune) rune { - switch r { - case ':': - fallthrough - case ' ': - return '_' - default: - return r - } - }, joined) + return strings.Map(sanitize, joined) +} + +func sanitize(r rune) rune { + switch r { + case ':': + fallthrough + case ' ': + return '_' + default: + return r + } } -func (s *DogStatsdSink) parseKey(key []string) ([]string, []string) { +func (s *DogStatsdSink) parseKey(key []string) ([]string, []metrics.Label) { // Since DogStatsd supports dimensionality via tags on metric keys, this sink's approach is to splice the hostname out of the key in favor of a `host` tag // The `host` tag is either forced here, or set downstream by the DogStatsd server - var tags []string + var labels []metrics.Label hostName := s.hostName - //Splice the hostname out of the key + // Splice the hostname out of the key for i, el := range key { if el == hostName { key = append(key[:i], key[i+1:]...) + break } } if s.propagateHostname { - tags = append(tags, fmt.Sprintf("host:%s", hostName)) + labels = append(labels, metrics.Label{"host", hostName}) } - return key, tags + return key, labels } // Implementation of methods in the MetricSink interface func (s *DogStatsdSink) SetGauge(key []string, val float32) { - s.SetGaugeWithTags(key, val, []string{}) + s.SetGaugeWithLabels(key, val, nil) } func (s *DogStatsdSink) IncrCounter(key []string, val float32) { - s.IncrCounterWithTags(key, val, []string{}) + s.IncrCounterWithLabels(key, val, nil) } // EmitKey is not implemented since DogStatsd does not provide a metric type that holds an @@ -93,33 +97,44 @@ func (s *DogStatsdSink) EmitKey(key []string, val float32) { } func (s *DogStatsdSink) AddSample(key []string, val float32) { - s.AddSampleWithTags(key, val, []string{}) + s.AddSampleWithLabels(key, val, nil) } -// The following ...WithTags methods correspond to Datadog's Tag extension to Statsd. +// The following ...WithLabels methods correspond to Datadog's Tag extension to Statsd. // http://docs.datadoghq.com/guides/dogstatsd/#tags - -func (s *DogStatsdSink) SetGaugeWithTags(key []string, val float32, tags []string) { - flatKey, tags := s.getFlatkeyAndCombinedTags(key, tags) +func (s *DogStatsdSink) SetGaugeWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) rate := 1.0 s.client.Gauge(flatKey, float64(val), tags, rate) } -func (s *DogStatsdSink) IncrCounterWithTags(key []string, val float32, tags []string) { - flatKey, tags := s.getFlatkeyAndCombinedTags(key, tags) +func (s *DogStatsdSink) IncrCounterWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) rate := 1.0 s.client.Count(flatKey, int64(val), tags, rate) } -func (s *DogStatsdSink) AddSampleWithTags(key []string, val float32, tags []string) { - flatKey, tags := s.getFlatkeyAndCombinedTags(key, tags) +func (s *DogStatsdSink) AddSampleWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) rate := 1.0 s.client.TimeInMilliseconds(flatKey, float64(val), tags, rate) } -func (s *DogStatsdSink) getFlatkeyAndCombinedTags(key []string, tags []string) (flattenedKey string, combinedTags []string) { - key, hostTags := s.parseKey(key) +func (s *DogStatsdSink) getFlatkeyAndCombinedLabels(key []string, labels []metrics.Label) (string, []string) { + key, parsedLabels := s.parseKey(key) flatKey := s.flattenKey(key) - tags = append(tags, hostTags...) + labels = append(labels, parsedLabels...) + + var tags []string + for _, label := range labels { + label.Name = strings.Map(sanitize, label.Name) + label.Value = strings.Map(sanitize, label.Value) + if label.Value != "" { + tags = append(tags, fmt.Sprintf("%s:%s", label.Name, label.Value)) + } else { + tags = append(tags, label.Name) + } + } + return flatKey, tags } diff --git a/vendor/github.com/armon/go-metrics/inmem.go b/vendor/github.com/armon/go-metrics/inmem.go index ac46443be9ad..cd1773042136 100644 --- a/vendor/github.com/armon/go-metrics/inmem.go +++ b/vendor/github.com/armon/go-metrics/inmem.go @@ -1,6 +1,7 @@ package metrics import ( + "bytes" "fmt" "math" "net/url" @@ -39,7 +40,7 @@ type IntervalMetrics struct { Interval time.Time // Gauges maps the key to the last set value - Gauges map[string]float32 + Gauges map[string]GaugeValue // Points maps the string to the list of emitted values // from EmitKey @@ -47,21 +48,21 @@ type IntervalMetrics struct { // Counters maps the string key to a sum of the counter // values - Counters map[string]*AggregateSample + Counters map[string]SampledValue // Samples maps the key to an AggregateSample, // which has the rolled up view of a sample - Samples map[string]*AggregateSample + Samples map[string]SampledValue } // NewIntervalMetrics creates a new IntervalMetrics for a given interval func NewIntervalMetrics(intv time.Time) *IntervalMetrics { return &IntervalMetrics{ Interval: intv, - Gauges: make(map[string]float32), + Gauges: make(map[string]GaugeValue), Points: make(map[string][]float32), - Counters: make(map[string]*AggregateSample), - Samples: make(map[string]*AggregateSample), + Counters: make(map[string]SampledValue), + Samples: make(map[string]SampledValue), } } @@ -69,12 +70,12 @@ func NewIntervalMetrics(intv time.Time) *IntervalMetrics { // about a sample type AggregateSample struct { Count int // The count of emitted pairs - Rate float64 // The count of emitted pairs per time unit (usually 1 second) + Rate float64 `json:"-"` // The count of emitted pairs per time unit (usually 1 second) Sum float64 // The sum of values - SumSq float64 // The sum of squared values + SumSq float64 `json:"-"` // The sum of squared values Min float64 // Minimum value Max float64 // Maximum value - LastUpdated time.Time // When value was last updated + LastUpdated time.Time `json:"-"` // When value was last updated } // Computes a Stddev of the values @@ -154,12 +155,16 @@ func NewInmemSink(interval, retain time.Duration) *InmemSink { } func (i *InmemSink) SetGauge(key []string, val float32) { - k := i.flattenKey(key) + i.SetGaugeWithLabels(key, val, nil) +} + +func (i *InmemSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { + k, name := i.flattenKeyLabels(key, labels) intv := i.getInterval() intv.Lock() defer intv.Unlock() - intv.Gauges[k] = val + intv.Gauges[k] = GaugeValue{Name: name, Value: val, Labels: labels} } func (i *InmemSink) EmitKey(key []string, val float32) { @@ -173,30 +178,46 @@ func (i *InmemSink) EmitKey(key []string, val float32) { } func (i *InmemSink) IncrCounter(key []string, val float32) { - k := i.flattenKey(key) + i.IncrCounterWithLabels(key, val, nil) +} + +func (i *InmemSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { + k, name := i.flattenKeyLabels(key, labels) intv := i.getInterval() intv.Lock() defer intv.Unlock() - agg := intv.Counters[k] - if agg == nil { - agg = &AggregateSample{} + agg, ok := intv.Counters[k] + if !ok { + agg = SampledValue{ + Name: name, + AggregateSample: &AggregateSample{}, + Labels: labels, + } intv.Counters[k] = agg } agg.Ingest(float64(val), i.rateDenom) } func (i *InmemSink) AddSample(key []string, val float32) { - k := i.flattenKey(key) + i.AddSampleWithLabels(key, val, nil) +} + +func (i *InmemSink) AddSampleWithLabels(key []string, val float32, labels []Label) { + k, name := i.flattenKeyLabels(key, labels) intv := i.getInterval() intv.Lock() defer intv.Unlock() - agg := intv.Samples[k] - if agg == nil { - agg = &AggregateSample{} + agg, ok := intv.Samples[k] + if !ok { + agg = SampledValue{ + Name: name, + AggregateSample: &AggregateSample{}, + Labels: labels, + } intv.Samples[k] = agg } agg.Ingest(float64(val), i.rateDenom) @@ -261,6 +282,38 @@ func (i *InmemSink) getInterval() *IntervalMetrics { // Flattens the key for formatting, removes spaces func (i *InmemSink) flattenKey(parts []string) string { - joined := strings.Join(parts, ".") - return strings.Replace(joined, " ", "_", -1) + buf := &bytes.Buffer{} + replacer := strings.NewReplacer(" ", "_") + + if len(parts) > 0 { + replacer.WriteString(buf, parts[0]) + } + for _, part := range parts[1:] { + replacer.WriteString(buf, ".") + replacer.WriteString(buf, part) + } + + return buf.String() +} + +// Flattens the key for formatting along with its labels, removes spaces +func (i *InmemSink) flattenKeyLabels(parts []string, labels []Label) (string, string) { + buf := &bytes.Buffer{} + replacer := strings.NewReplacer(" ", "_") + + if len(parts) > 0 { + replacer.WriteString(buf, parts[0]) + } + for _, part := range parts[1:] { + replacer.WriteString(buf, ".") + replacer.WriteString(buf, part) + } + + key := buf.String() + + for _, label := range labels { + replacer.WriteString(buf, fmt.Sprintf(";%s=%s", label.Name, label.Value)) + } + + return buf.String(), key } diff --git a/vendor/github.com/armon/go-metrics/inmem_endpoint.go b/vendor/github.com/armon/go-metrics/inmem_endpoint.go new file mode 100644 index 000000000000..504f1b374854 --- /dev/null +++ b/vendor/github.com/armon/go-metrics/inmem_endpoint.go @@ -0,0 +1,118 @@ +package metrics + +import ( + "fmt" + "net/http" + "sort" + "time" +) + +// MetricsSummary holds a roll-up of metrics info for a given interval +type MetricsSummary struct { + Timestamp string + Gauges []GaugeValue + Points []PointValue + Counters []SampledValue + Samples []SampledValue +} + +type GaugeValue struct { + Name string + Hash string `json:"-"` + Value float32 + + Labels []Label `json:"-"` + DisplayLabels map[string]string `json:"Labels"` +} + +type PointValue struct { + Name string + Points []float32 +} + +type SampledValue struct { + Name string + Hash string `json:"-"` + *AggregateSample + Mean float64 + Stddev float64 + + Labels []Label `json:"-"` + DisplayLabels map[string]string `json:"Labels"` +} + +// DisplayMetrics returns a summary of the metrics from the most recent finished interval. +func (i *InmemSink) DisplayMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + data := i.Data() + + var interval *IntervalMetrics + n := len(data) + switch { + case n == 0: + return nil, fmt.Errorf("no metric intervals have been initialized yet") + case n == 1: + // Show the current interval if it's all we have + interval = i.intervals[0] + default: + // Show the most recent finished interval if we have one + interval = i.intervals[n-2] + } + + summary := MetricsSummary{ + Timestamp: interval.Interval.Round(time.Second).UTC().String(), + Gauges: make([]GaugeValue, 0, len(interval.Gauges)), + Points: make([]PointValue, 0, len(interval.Points)), + } + + // Format and sort the output of each metric type, so it gets displayed in a + // deterministic order. + for name, points := range interval.Points { + summary.Points = append(summary.Points, PointValue{name, points}) + } + sort.Slice(summary.Points, func(i, j int) bool { + return summary.Points[i].Name < summary.Points[j].Name + }) + + for hash, value := range interval.Gauges { + value.Hash = hash + value.DisplayLabels = make(map[string]string) + for _, label := range value.Labels { + value.DisplayLabels[label.Name] = label.Value + } + value.Labels = nil + + summary.Gauges = append(summary.Gauges, value) + } + sort.Slice(summary.Gauges, func(i, j int) bool { + return summary.Gauges[i].Hash < summary.Gauges[j].Hash + }) + + summary.Counters = formatSamples(interval.Counters) + summary.Samples = formatSamples(interval.Samples) + + return summary, nil +} + +func formatSamples(source map[string]SampledValue) []SampledValue { + output := make([]SampledValue, 0, len(source)) + for hash, sample := range source { + displayLabels := make(map[string]string) + for _, label := range sample.Labels { + displayLabels[label.Name] = label.Value + } + + output = append(output, SampledValue{ + Name: sample.Name, + Hash: hash, + AggregateSample: sample.AggregateSample, + Mean: sample.AggregateSample.Mean(), + Stddev: sample.AggregateSample.Stddev(), + DisplayLabels: displayLabels, + }) + } + sort.Slice(output, func(i, j int) bool { + return output[i].Hash < output[j].Hash + }) + + return output +} diff --git a/vendor/github.com/armon/go-metrics/inmem_signal.go b/vendor/github.com/armon/go-metrics/inmem_signal.go index 95d08ee10f0b..76b2ab8a80c9 100644 --- a/vendor/github.com/armon/go-metrics/inmem_signal.go +++ b/vendor/github.com/armon/go-metrics/inmem_signal.go @@ -79,7 +79,7 @@ func (i *InmemSignal) dumpStats() { intv := data[i] intv.RLock() for name, val := range intv.Gauges { - fmt.Fprintf(buf, "[%v][G] '%s': %0.3f\n", intv.Interval, name, val) + fmt.Fprintf(buf, "[%v][G] '%s': %0.3f\n", intv.Interval, name, val.Value) } for name, vals := range intv.Points { for _, val := range vals { @@ -87,10 +87,10 @@ func (i *InmemSignal) dumpStats() { } } for name, agg := range intv.Counters { - fmt.Fprintf(buf, "[%v][C] '%s': %s\n", intv.Interval, name, agg) + fmt.Fprintf(buf, "[%v][C] '%s': %s\n", intv.Interval, name, agg.AggregateSample) } for name, agg := range intv.Samples { - fmt.Fprintf(buf, "[%v][S] '%s': %s\n", intv.Interval, name, agg) + fmt.Fprintf(buf, "[%v][S] '%s': %s\n", intv.Interval, name, agg.AggregateSample) } intv.RUnlock() } diff --git a/vendor/github.com/armon/go-metrics/metrics.go b/vendor/github.com/armon/go-metrics/metrics.go index b818e4182c0c..e51cb889b4d6 100644 --- a/vendor/github.com/armon/go-metrics/metrics.go +++ b/vendor/github.com/armon/go-metrics/metrics.go @@ -2,20 +2,41 @@ package metrics import ( "runtime" + "strings" "time" ) +type Label struct { + Name string + Value string +} + func (m *Metrics) SetGauge(key []string, val float32) { - if m.HostName != "" && m.EnableHostname { - key = insert(0, m.HostName, key) + m.SetGaugeWithLabels(key, val, nil) +} + +func (m *Metrics) SetGaugeWithLabels(key []string, val float32, labels []Label) { + if m.HostName != "" { + if m.EnableHostnameLabel { + labels = append(labels, Label{"host", m.HostName}) + } else if m.EnableHostname { + key = insert(0, m.HostName, key) + } } if m.EnableTypePrefix { key = insert(0, "gauge", key) } if m.ServiceName != "" { - key = insert(0, m.ServiceName, key) + if m.EnableServiceLabel { + labels = append(labels, Label{"service", m.ServiceName}) + } else { + key = insert(0, m.ServiceName, key) + } + } + if !m.allowMetric(key) { + return } - m.sink.SetGauge(key, val) + m.sink.SetGaugeWithLabels(key, val, labels) } func (m *Metrics) EmitKey(key []string, val float32) { @@ -25,40 +46,98 @@ func (m *Metrics) EmitKey(key []string, val float32) { if m.ServiceName != "" { key = insert(0, m.ServiceName, key) } + if !m.allowMetric(key) { + return + } m.sink.EmitKey(key, val) } func (m *Metrics) IncrCounter(key []string, val float32) { + m.IncrCounterWithLabels(key, val, nil) +} + +func (m *Metrics) IncrCounterWithLabels(key []string, val float32, labels []Label) { + if m.HostName != "" && m.EnableHostnameLabel { + labels = append(labels, Label{"host", m.HostName}) + } if m.EnableTypePrefix { key = insert(0, "counter", key) } if m.ServiceName != "" { - key = insert(0, m.ServiceName, key) + if m.EnableServiceLabel { + labels = append(labels, Label{"service", m.ServiceName}) + } else { + key = insert(0, m.ServiceName, key) + } } - m.sink.IncrCounter(key, val) + if !m.allowMetric(key) { + return + } + m.sink.IncrCounterWithLabels(key, val, labels) } func (m *Metrics) AddSample(key []string, val float32) { + m.AddSampleWithLabels(key, val, nil) +} + +func (m *Metrics) AddSampleWithLabels(key []string, val float32, labels []Label) { + if m.HostName != "" && m.EnableHostnameLabel { + labels = append(labels, Label{"host", m.HostName}) + } if m.EnableTypePrefix { key = insert(0, "sample", key) } if m.ServiceName != "" { - key = insert(0, m.ServiceName, key) + if m.EnableServiceLabel { + labels = append(labels, Label{"service", m.ServiceName}) + } else { + key = insert(0, m.ServiceName, key) + } } - m.sink.AddSample(key, val) + if !m.allowMetric(key) { + return + } + m.sink.AddSampleWithLabels(key, val, labels) } func (m *Metrics) MeasureSince(key []string, start time.Time) { + m.MeasureSinceWithLabels(key, start, nil) +} + +func (m *Metrics) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { + if m.HostName != "" && m.EnableHostnameLabel { + labels = append(labels, Label{"host", m.HostName}) + } if m.EnableTypePrefix { key = insert(0, "timer", key) } if m.ServiceName != "" { - key = insert(0, m.ServiceName, key) + if m.EnableServiceLabel { + labels = append(labels, Label{"service", m.ServiceName}) + } else { + key = insert(0, m.ServiceName, key) + } + } + if !m.allowMetric(key) { + return } now := time.Now() elapsed := now.Sub(start) msec := float32(elapsed.Nanoseconds()) / float32(m.TimerGranularity) - m.sink.AddSample(key, msec) + m.sink.AddSampleWithLabels(key, msec, labels) +} + +// Returns whether the metric should be allowed based on configured prefix filters +func (m *Metrics) allowMetric(key []string) bool { + if m.filter == nil || m.filter.Len() == 0 { + return m.Config.FilterDefault + } + + _, allowed, ok := m.filter.Root().LongestPrefix([]byte(strings.Join(key, "."))) + if !ok { + return m.Config.FilterDefault + } + return allowed.(bool) } // Periodically collects runtime stats to publish diff --git a/vendor/github.com/armon/go-metrics/sink.go b/vendor/github.com/armon/go-metrics/sink.go index 9f7e2f6a2467..0b7d6e4be43f 100644 --- a/vendor/github.com/armon/go-metrics/sink.go +++ b/vendor/github.com/armon/go-metrics/sink.go @@ -10,31 +10,41 @@ import ( type MetricSink interface { // A Gauge should retain the last value it is set to SetGauge(key []string, val float32) + SetGaugeWithLabels(key []string, val float32, labels []Label) // Should emit a Key/Value pair for each call EmitKey(key []string, val float32) // Counters should accumulate values IncrCounter(key []string, val float32) + IncrCounterWithLabels(key []string, val float32, labels []Label) // Samples are for timing information, where quantiles are used AddSample(key []string, val float32) + AddSampleWithLabels(key []string, val float32, labels []Label) } // BlackholeSink is used to just blackhole messages type BlackholeSink struct{} -func (*BlackholeSink) SetGauge(key []string, val float32) {} -func (*BlackholeSink) EmitKey(key []string, val float32) {} -func (*BlackholeSink) IncrCounter(key []string, val float32) {} -func (*BlackholeSink) AddSample(key []string, val float32) {} +func (*BlackholeSink) SetGauge(key []string, val float32) {} +func (*BlackholeSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {} +func (*BlackholeSink) EmitKey(key []string, val float32) {} +func (*BlackholeSink) IncrCounter(key []string, val float32) {} +func (*BlackholeSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {} +func (*BlackholeSink) AddSample(key []string, val float32) {} +func (*BlackholeSink) AddSampleWithLabels(key []string, val float32, labels []Label) {} // FanoutSink is used to sink to fanout values to multiple sinks type FanoutSink []MetricSink func (fh FanoutSink) SetGauge(key []string, val float32) { + fh.SetGaugeWithLabels(key, val, nil) +} + +func (fh FanoutSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { for _, s := range fh { - s.SetGauge(key, val) + s.SetGaugeWithLabels(key, val, labels) } } @@ -45,14 +55,22 @@ func (fh FanoutSink) EmitKey(key []string, val float32) { } func (fh FanoutSink) IncrCounter(key []string, val float32) { + fh.IncrCounterWithLabels(key, val, nil) +} + +func (fh FanoutSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { for _, s := range fh { - s.IncrCounter(key, val) + s.IncrCounterWithLabels(key, val, labels) } } func (fh FanoutSink) AddSample(key []string, val float32) { + fh.AddSampleWithLabels(key, val, nil) +} + +func (fh FanoutSink) AddSampleWithLabels(key []string, val float32, labels []Label) { for _, s := range fh { - s.AddSample(key, val) + s.AddSampleWithLabels(key, val, labels) } } diff --git a/vendor/github.com/armon/go-metrics/start.go b/vendor/github.com/armon/go-metrics/start.go index 40c8d68c15a5..f16fad0fd967 100644 --- a/vendor/github.com/armon/go-metrics/start.go +++ b/vendor/github.com/armon/go-metrics/start.go @@ -4,6 +4,8 @@ import ( "os" "sync/atomic" "time" + + "github.com/hashicorp/go-immutable-radix" ) // Config is used to configure metrics settings @@ -11,10 +13,15 @@ type Config struct { ServiceName string // Prefixed with keys to seperate services HostName string // Hostname to use. If not provided and EnableHostname, it will be os.Hostname EnableHostname bool // Enable prefixing gauge values with hostname + EnableHostnameLabel bool // Enable adding hostname to labels + EnableServiceLabel bool // Enable adding service to labels EnableRuntimeMetrics bool // Enables profiling of runtime metrics (GC, Goroutines, Memory) EnableTypePrefix bool // Prefixes key with a type ("counter", "gauge", "timer") TimerGranularity time.Duration // Granularity of timers. ProfileInterval time.Duration // Interval to profile runtime metrics + AllowedPrefixes []string // A list of metric prefixes to allow, with '.' as the separator + BlockedPrefixes []string // A list of metric prefixes to block, with '.' as the separator + FilterDefault bool // Whether to allow metrics by default } // Metrics represents an instance of a metrics sink that can @@ -23,6 +30,7 @@ type Metrics struct { Config lastNumGC uint32 sink MetricSink + filter *iradix.Tree } // Shared global metrics instance @@ -43,6 +51,7 @@ func DefaultConfig(serviceName string) *Config { EnableTypePrefix: false, // Disable type prefix TimerGranularity: time.Millisecond, // Timers are in milliseconds ProfileInterval: time.Second, // Poll runtime every second + FilterDefault: true, // Don't filter metrics by default } // Try to get the hostname @@ -56,6 +65,14 @@ func New(conf *Config, sink MetricSink) (*Metrics, error) { met := &Metrics{} met.Config = *conf met.sink = sink + met.filter = iradix.New() + + for _, prefix := range conf.AllowedPrefixes { + met.filter, _, _ = met.filter.Insert([]byte(prefix), true) + } + for _, prefix := range conf.BlockedPrefixes { + met.filter, _, _ = met.filter.Insert([]byte(prefix), false) + } // Start the runtime collector if conf.EnableRuntimeMetrics { @@ -79,6 +96,10 @@ func SetGauge(key []string, val float32) { globalMetrics.Load().(*Metrics).SetGauge(key, val) } +func SetGaugeWithLabels(key []string, val float32, labels []Label) { + globalMetrics.Load().(*Metrics).SetGaugeWithLabels(key, val, labels) +} + func EmitKey(key []string, val float32) { globalMetrics.Load().(*Metrics).EmitKey(key, val) } @@ -87,10 +108,22 @@ func IncrCounter(key []string, val float32) { globalMetrics.Load().(*Metrics).IncrCounter(key, val) } +func IncrCounterWithLabels(key []string, val float32, labels []Label) { + globalMetrics.Load().(*Metrics).IncrCounterWithLabels(key, val, labels) +} + func AddSample(key []string, val float32) { globalMetrics.Load().(*Metrics).AddSample(key, val) } +func AddSampleWithLabels(key []string, val float32, labels []Label) { + globalMetrics.Load().(*Metrics).AddSampleWithLabels(key, val, labels) +} + func MeasureSince(key []string, start time.Time) { globalMetrics.Load().(*Metrics).MeasureSince(key, start) } + +func MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { + globalMetrics.Load().(*Metrics).MeasureSinceWithLabels(key, start, labels) +} diff --git a/vendor/github.com/armon/go-metrics/statsd.go b/vendor/github.com/armon/go-metrics/statsd.go index 4241e880c573..382342816181 100644 --- a/vendor/github.com/armon/go-metrics/statsd.go +++ b/vendor/github.com/armon/go-metrics/statsd.go @@ -50,6 +50,11 @@ func (s *StatsdSink) SetGauge(key []string, val float32) { s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) } +func (s *StatsdSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) +} + func (s *StatsdSink) EmitKey(key []string, val float32) { flatKey := s.flattenKey(key) s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val)) @@ -60,11 +65,21 @@ func (s *StatsdSink) IncrCounter(key []string, val float32) { s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) } +func (s *StatsdSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) +} + func (s *StatsdSink) AddSample(key []string, val float32) { flatKey := s.flattenKey(key) s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) } +func (s *StatsdSink) AddSampleWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) +} + // Flattens the key for formatting, removes spaces func (s *StatsdSink) flattenKey(parts []string) string { joined := strings.Join(parts, ".") @@ -80,6 +95,15 @@ func (s *StatsdSink) flattenKey(parts []string) string { }, joined) } +// Flattens the key along with labels for formatting, removes spaces +func (s *StatsdSink) flattenKeyLabels(parts []string, labels []Label) string { + fullName := parts + for _, label := range labels { + fullName = append(parts, label.Value) + } + return s.flattenKey(fullName) +} + // Does a non-blocking push to the metrics queue func (s *StatsdSink) pushMetric(m string) { select { diff --git a/vendor/github.com/armon/go-metrics/statsite.go b/vendor/github.com/armon/go-metrics/statsite.go index 572fe0571522..6c0d284d2ddb 100644 --- a/vendor/github.com/armon/go-metrics/statsite.go +++ b/vendor/github.com/armon/go-metrics/statsite.go @@ -50,6 +50,11 @@ func (s *StatsiteSink) SetGauge(key []string, val float32) { s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) } +func (s *StatsiteSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) +} + func (s *StatsiteSink) EmitKey(key []string, val float32) { flatKey := s.flattenKey(key) s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val)) @@ -60,11 +65,21 @@ func (s *StatsiteSink) IncrCounter(key []string, val float32) { s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) } +func (s *StatsiteSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) +} + func (s *StatsiteSink) AddSample(key []string, val float32) { flatKey := s.flattenKey(key) s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) } +func (s *StatsiteSink) AddSampleWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) +} + // Flattens the key for formatting, removes spaces func (s *StatsiteSink) flattenKey(parts []string) string { joined := strings.Join(parts, ".") @@ -80,6 +95,14 @@ func (s *StatsiteSink) flattenKey(parts []string) string { }, joined) } +// Flattens the key along with labels for formatting, removes spaces +func (s *StatsiteSink) flattenKeyLabels(parts []string, labels []Label) string { + for _, label := range labels { + parts = append(parts, label.Value) + } + return s.flattenKey(parts) +} + // Does a non-blocking push to the metrics queue func (s *StatsiteSink) pushMetric(m string) { select { diff --git a/vendor/vendor.json b/vendor/vendor.json index 82b254f0e2ef..8eb3f32c4aa9 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -6,9 +6,9 @@ {"checksumSHA1":"AzjRkOQtVBTwIw4RJLTygFhJs3s=","path":"github.com/Microsoft/go-winio","revision":"c4dc1301f1dc0307acd38e611aa375a64dfe0642","revisionTime":"2017-07-12T04:46:15Z"}, {"checksumSHA1":"9NR0rrcAT5J76C5xMS4AVksS9o0=","path":"github.com/StackExchange/wmi","revision":"e54cbda6595d7293a7a468ccf9525f6bc8887f99","revisionTime":"2016-08-11T21:45:55Z"}, {"checksumSHA1":"l0iFqayYAaEip6Olaq3/LCOa/Sg=","path":"github.com/armon/circbuf","revision":"bbbad097214e2918d8543d5201d12bfd7bca254d","revisionTime":"2015-08-27T00:49:46Z"}, - {"checksumSHA1":"M+ZeYktTT2wak9ZvQ0OZBbIHAGo=","path":"github.com/armon/go-metrics","revision":"f036747b9d0e8590f175a5d654a2194a7d9df4b5","revisionTime":"2017-06-01T21:44:32Z"}, - {"checksumSHA1":"OmqT9Y1mAHvlAKeJh0jBHC9SH78=","path":"github.com/armon/go-metrics/circonus","revision":"3df31a1ada83e310c2e24b267c8e8b68836547b4","revisionTime":"2016-07-17T04:34:58Z"}, - {"checksumSHA1":"mAzNU3zeZGEwqjDT4ZkspFvx3TI=","path":"github.com/armon/go-metrics/datadog","revision":"3df31a1ada83e310c2e24b267c8e8b68836547b4","revisionTime":"2016-07-17T04:34:58Z"}, + {"checksumSHA1":"1fTBW3jW8+cYV5UNKrAMXsQhqnQ=","path":"github.com/armon/go-metrics","revision":"efce74234d0fcaac1bd5b2a32d6d3afd108c4498","revisionTime":"2017-08-08T03:49:35Z"}, + {"checksumSHA1":"xCsGGM9TKBogZDfSN536KtQdLko=","path":"github.com/armon/go-metrics/circonus","revision":"ded85ed431a7aee3f3af79f082b704d948058f64","revisionTime":"2017-08-07T19:17:41Z"}, + {"checksumSHA1":"Dt0n1sSivvvdZQdzc4Hu/yOG+T0=","path":"github.com/armon/go-metrics/datadog","revision":"ded85ed431a7aee3f3af79f082b704d948058f64","revisionTime":"2017-08-07T19:17:41Z"}, {"checksumSHA1":"gNO0JNpLzYOdInGeq7HqMZUzx9M=","path":"github.com/armon/go-radix","revision":"4239b77079c7b5d1243b7b4736304ce8ddb6f0f2","revisionTime":"2016-01-15T23:47:25Z"}, {"checksumSHA1":"dvd7Su+WNmHRP1+w1HezrPUCDsc=","path":"github.com/bgentry/speakeasy","revision":"e1439544d8ecd0f3e9373a636d447668096a8f81","revisionTime":"2016-05-20T23:26:10Z"}, {"checksumSHA1":"twtRfb6484vfr2qqjiFkLThTjcQ=","path":"github.com/bgentry/speakeasy/example","revision":"e1439544d8ecd0f3e9373a636d447668096a8f81","revisionTime":"2016-05-20T23:26:10Z"}, From d5634fe2a849d46283a7dbcbf6944b5153faf301 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 8 Aug 2017 01:31:38 -0700 Subject: [PATCH 2/6] Add support for labels/filters from go-metrics --- agent/agent.go | 4 + agent/config.go | 32 +++++++ agent/config_test.go | 12 +++ agent/consul/catalog_endpoint.go | 9 +- agent/consul/fsm.go | 15 ++- agent/consul/health_endpoint.go | 9 +- agent/consul/rpc.go | 3 +- agent/dns.go | 6 +- agent/http.go | 2 + api/agent.go | 26 ++++++ api/agent_test.go | 22 +++++ command/agent.go | 17 ++-- website/source/api/agent.html.md | 106 ++++++++++++++++++++++ website/source/docs/agent/options.html.md | 18 ++++ 14 files changed, 260 insertions(+), 21 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 98f02b6924bb..f59a55e4ec81 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -19,6 +19,7 @@ import ( "sync" "time" + "github.com/armon/go-metrics" "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/consul/structs" "github.com/hashicorp/consul/agent/systemd" @@ -94,6 +95,9 @@ type Agent struct { // Used for streaming logs to LogWriter *logger.LogWriter + // In-memory sink used for collecting metrics + MemSink *metrics.InmemSink + // delegate is either a *consul.Server or *consul.Client // depending on the configuration delegate delegate diff --git a/agent/config.go b/agent/config.go index e9e5b9966b3e..116835cf9a19 100644 --- a/agent/config.go +++ b/agent/config.go @@ -219,6 +219,16 @@ type Telemetry struct { // DisableHostname will disable hostname prefixing for all metrics DisableHostname bool `mapstructure:"disable_hostname"` + // PrefixFilter is a list of filter rules to apply for allowing/blocking metrics + // by prefix. + PrefixFilter []string `mapstructure:"prefix_filter"` + AllowedPrefixes []string `mapstructure:"-" json:"-"` + BlockedPrefixes []string `mapstructure:"-" json:"-"` + + // FilterDefault is the default for whether to allow a metric that's not + // covered by the filter. + FilterDefault *bool `mapstructure:"filter_default"` + // DogStatsdAddr is the address of a dogstatsd instance. If provided, // metrics will be sent to that instance DogStatsdAddr string `mapstructure:"dogstatsd_addr"` @@ -937,6 +947,7 @@ func DefaultConfig() *Config { }, Telemetry: Telemetry{ StatsitePrefix: "consul", + FilterDefault: Bool(true), }, Meta: make(map[string]string), SyslogFacility: "LOCAL0", @@ -1461,6 +1472,21 @@ func DecodeConfig(r io.Reader) (*Config, error) { result.EnableACLReplication = true } + // Parse the metric filters + for _, rule := range result.Telemetry.PrefixFilter { + if rule == "" { + return nil, fmt.Errorf("Cannot have empty filter rule in prefix_filter") + } + switch rule[0] { + case '+': + result.Telemetry.AllowedPrefixes = append(result.Telemetry.AllowedPrefixes, rule[1:]) + case '-': + result.Telemetry.BlockedPrefixes = append(result.Telemetry.BlockedPrefixes, rule[1:]) + default: + return nil, fmt.Errorf("Filter rule must begin with either '+' or '-': %s", rule) + } + } + return &result, nil } @@ -1755,6 +1781,12 @@ func MergeConfig(a, b *Config) *Config { if b.Telemetry.DisableHostname == true { result.Telemetry.DisableHostname = true } + if len(b.Telemetry.PrefixFilter) != 0 { + result.Telemetry.PrefixFilter = append(result.Telemetry.PrefixFilter, b.Telemetry.PrefixFilter...) + } + if b.Telemetry.FilterDefault != nil { + result.Telemetry.FilterDefault = b.Telemetry.FilterDefault + } if b.Telemetry.StatsdAddr != "" { result.Telemetry.StatsdAddr = b.Telemetry.StatsdAddr } diff --git a/agent/config_test.go b/agent/config_test.go index 42e4366a7904..5b83c677d6ed 100644 --- a/agent/config_test.go +++ b/agent/config_test.go @@ -719,6 +719,18 @@ func TestDecodeConfig(t *testing.T) { in: `{"telemetry":{"dogstatsd_tags":["a","b"]}}`, c: &Config{Telemetry: Telemetry{DogStatsdTags: []string{"a", "b"}}}, }, + { + in: `{"telemetry":{"filter_default":true}}`, + c: &Config{Telemetry: Telemetry{FilterDefault: Bool(true)}}, + }, + { + in: `{"telemetry":{"prefix_filter":["+consul.metric","-consul.othermetric"]}}`, + c: &Config{Telemetry: Telemetry{ + PrefixFilter: []string{"+consul.metric", "-consul.othermetric"}, + AllowedPrefixes: []string{"consul.metric"}, + BlockedPrefixes: []string{"consul.othermetric"}, + }}, + }, { in: `{"telemetry":{"statsd_address":"a"}}`, c: &Config{Telemetry: Telemetry{StatsdAddr: "a"}}, diff --git a/agent/consul/catalog_endpoint.go b/agent/consul/catalog_endpoint.go index db7b6c7bfe5e..06ad854948f1 100644 --- a/agent/consul/catalog_endpoint.go +++ b/agent/consul/catalog_endpoint.go @@ -268,12 +268,15 @@ func (c *Catalog) ServiceNodes(args *structs.ServiceSpecificRequest, reply *stru // Provide some metrics if err == nil { - metrics.IncrCounter([]string{"consul", "catalog", "service", "query", args.ServiceName}, 1) + metrics.IncrCounterWithLabels([]string{"consul", "catalog", "service", "query"}, 1, + []metrics.Label{{Name: "service", Value: args.ServiceName}}) if args.ServiceTag != "" { - metrics.IncrCounter([]string{"consul", "catalog", "service", "query-tag", args.ServiceName, args.ServiceTag}, 1) + metrics.IncrCounterWithLabels([]string{"consul", "catalog", "service", "query-tag"}, 1, + []metrics.Label{{Name: "service", Value: args.ServiceName}, {Name: "tag", Value: args.ServiceTag}}) } if len(reply.ServiceNodes) == 0 { - metrics.IncrCounter([]string{"consul", "catalog", "service", "not-found", args.ServiceName}, 1) + metrics.IncrCounterWithLabels([]string{"consul", "catalog", "service", "not-found"}, 1, + []metrics.Label{{Name: "service", Value: args.ServiceName}}) } } return err diff --git a/agent/consul/fsm.go b/agent/consul/fsm.go index b3b39a7f3f0e..9481ea35a1f8 100644 --- a/agent/consul/fsm.go +++ b/agent/consul/fsm.go @@ -172,7 +172,8 @@ func (c *consulFSM) applyKVSOperation(buf []byte, index uint64) interface{} { if err := structs.Decode(buf, &req); err != nil { panic(fmt.Errorf("failed to decode request: %v", err)) } - defer metrics.MeasureSince([]string{"consul", "fsm", "kvs", string(req.Op)}, time.Now()) + defer metrics.MeasureSinceWithLabels([]string{"consul", "fsm", "kvs"}, time.Now(), + []metrics.Label{{Name: "op", Value: string(req.Op)}}) switch req.Op { case api.KVSet: return c.state.KVSSet(index, &req.DirEnt) @@ -216,7 +217,8 @@ func (c *consulFSM) applySessionOperation(buf []byte, index uint64) interface{} if err := structs.Decode(buf, &req); err != nil { panic(fmt.Errorf("failed to decode request: %v", err)) } - defer metrics.MeasureSince([]string{"consul", "fsm", "session", string(req.Op)}, time.Now()) + defer metrics.MeasureSinceWithLabels([]string{"consul", "fsm", "session"}, time.Now(), + []metrics.Label{{Name: "op", Value: string(req.Op)}}) switch req.Op { case structs.SessionCreate: if err := c.state.SessionCreate(index, &req.Session); err != nil { @@ -236,7 +238,8 @@ func (c *consulFSM) applyACLOperation(buf []byte, index uint64) interface{} { if err := structs.Decode(buf, &req); err != nil { panic(fmt.Errorf("failed to decode request: %v", err)) } - defer metrics.MeasureSince([]string{"consul", "fsm", "acl", string(req.Op)}, time.Now()) + defer metrics.MeasureSinceWithLabels([]string{"consul", "fsm", "acl"}, time.Now(), + []metrics.Label{{Name: "op", Value: string(req.Op)}}) switch req.Op { case structs.ACLBootstrapInit: enabled, err := c.state.ACLBootstrapInit(index) @@ -267,7 +270,8 @@ func (c *consulFSM) applyTombstoneOperation(buf []byte, index uint64) interface{ if err := structs.Decode(buf, &req); err != nil { panic(fmt.Errorf("failed to decode request: %v", err)) } - defer metrics.MeasureSince([]string{"consul", "fsm", "tombstone", string(req.Op)}, time.Now()) + defer metrics.MeasureSinceWithLabels([]string{"consul", "fsm", "tombstone"}, time.Now(), + []metrics.Label{{Name: "op", Value: string(req.Op)}}) switch req.Op { case structs.TombstoneReap: return c.state.ReapTombstones(req.ReapIndex) @@ -301,7 +305,8 @@ func (c *consulFSM) applyPreparedQueryOperation(buf []byte, index uint64) interf panic(fmt.Errorf("failed to decode request: %v", err)) } - defer metrics.MeasureSince([]string{"consul", "fsm", "prepared-query", string(req.Op)}, time.Now()) + defer metrics.MeasureSinceWithLabels([]string{"consul", "fsm", "prepared-query"}, time.Now(), + []metrics.Label{{Name: "op", Value: string(req.Op)}}) switch req.Op { case structs.PreparedQueryCreate, structs.PreparedQueryUpdate: return c.state.PreparedQuerySet(index, req.Query) diff --git a/agent/consul/health_endpoint.go b/agent/consul/health_endpoint.go index 8d9d9ac46a81..b930d3032eee 100644 --- a/agent/consul/health_endpoint.go +++ b/agent/consul/health_endpoint.go @@ -139,12 +139,15 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc // Provide some metrics if err == nil { - metrics.IncrCounter([]string{"consul", "health", "service", "query", args.ServiceName}, 1) + metrics.IncrCounterWithLabels([]string{"consul", "health", "service", "query"}, 1, + []metrics.Label{{Name: "service", Value: args.ServiceName}}) if args.ServiceTag != "" { - metrics.IncrCounter([]string{"consul", "health", "service", "query-tag", args.ServiceName, args.ServiceTag}, 1) + metrics.IncrCounterWithLabels([]string{"consul", "health", "service", "query-tag"}, 1, + []metrics.Label{{Name: "service", Value: args.ServiceName}, {Name: "tag", Value: args.ServiceTag}}) } if len(reply.Nodes) == 0 { - metrics.IncrCounter([]string{"consul", "health", "service", "not-found", args.ServiceName}, 1) + metrics.IncrCounterWithLabels([]string{"consul", "health", "service", "not-found"}, 1, + []metrics.Label{{Name: "service", Value: args.ServiceName}}) } } return err diff --git a/agent/consul/rpc.go b/agent/consul/rpc.go index 9f63166c600c..3fd544b41ad1 100644 --- a/agent/consul/rpc.go +++ b/agent/consul/rpc.go @@ -263,7 +263,8 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ return structs.ErrNoDCPath } - metrics.IncrCounter([]string{"consul", "rpc", "cross-dc", dc}, 1) + metrics.IncrCounterWithLabels([]string{"consul", "rpc", "cross-dc"}, 1, + []metrics.Label{{Name: "datacenter", Value: dc}}) if err := s.connPool.RPC(dc, server.Addr, server.Version, method, server.UseTLS, args, reply); err != nil { manager.NotifyFailedServer(server) s.logger.Printf("[ERR] consul: RPC failed to server %s in DC %q: %v", server.Addr, dc, err) diff --git a/agent/dns.go b/agent/dns.go index bf01bd1e8570..808c02cca49f 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -118,7 +118,8 @@ START: func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { q := req.Question[0] defer func(s time.Time) { - metrics.MeasureSince([]string{"consul", "dns", "ptr_query", d.agent.config.NodeName}, s) + metrics.MeasureSinceWithLabels([]string{"consul", "dns", "ptr_query"}, s, + []metrics.Label{{Name: "node", Value: d.agent.config.NodeName}}) d.logger.Printf("[DEBUG] dns: request for %v (%v) from client %s (%s)", q, time.Now().Sub(s), resp.RemoteAddr().String(), resp.RemoteAddr().Network()) @@ -187,7 +188,8 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { func (d *DNSServer) handleQuery(resp dns.ResponseWriter, req *dns.Msg) { q := req.Question[0] defer func(s time.Time) { - metrics.MeasureSince([]string{"consul", "dns", "domain_query", d.agent.config.NodeName}, s) + metrics.MeasureSinceWithLabels([]string{"consul", "dns", "domain_query"}, s, + []metrics.Label{{Name: "node", Value: d.agent.config.NodeName}}) d.logger.Printf("[DEBUG] dns: request for %v (%v) from client %s (%s)", q, time.Now().Sub(s), resp.RemoteAddr().String(), resp.RemoteAddr().Network()) diff --git a/agent/http.go b/agent/http.go index a42ce15e1b1d..2d43209b1869 100644 --- a/agent/http.go +++ b/agent/http.go @@ -60,6 +60,7 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { // Register the wrapper, which will close over the expensive-to-compute // parts from above. + // TODO (kyhavlov): Convert this to utilize metric labels in a major release wrapper := func(resp http.ResponseWriter, req *http.Request) { start := time.Now() handler(resp, req) @@ -97,6 +98,7 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance)) handleFuncMetrics("/v1/agent/reload", s.wrap(s.AgentReload)) handleFuncMetrics("/v1/agent/monitor", s.wrap(s.AgentMonitor)) + handleFuncMetrics("/v1/agent/metrics", s.wrap(s.agent.MemSink.DisplayMetrics)) handleFuncMetrics("/v1/agent/services", s.wrap(s.AgentServices)) handleFuncMetrics("/v1/agent/checks", s.wrap(s.AgentChecks)) handleFuncMetrics("/v1/agent/members", s.wrap(s.AgentMembers)) diff --git a/api/agent.go b/api/agent.go index 383d2816ed59..2f43d8eda255 100644 --- a/api/agent.go +++ b/api/agent.go @@ -96,6 +96,15 @@ type AgentToken struct { Token string } +// Metrics info is used to store different types of metric values from the agent. +type MetricsInfo struct { + Timestamp string + Gauges []map[string]interface{} + Points []map[string]interface{} + Counters []map[string]interface{} + Samples []map[string]interface{} +} + // Agent can be used to query the Agent endpoints type Agent struct { c *Client @@ -126,6 +135,23 @@ func (a *Agent) Self() (map[string]map[string]interface{}, error) { return out, nil } +// Metrics is used to query the agent we are speaking to for +// its current internal metric data +func (a *Agent) Metrics() (*MetricsInfo, error) { + r := a.c.newRequest("GET", "/v1/agent/metrics") + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out *MetricsInfo + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return out, nil +} + // Reload triggers a configuration reload for the agent we are connected to. func (a *Agent) Reload() error { r := a.c.newRequest("PUT", "/v1/agent/reload") diff --git a/api/agent_test.go b/api/agent_test.go index 6e6292eee9fe..1b2cf2cba662 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -28,6 +28,28 @@ func TestAPI_AgentSelf(t *testing.T) { } } +func TestAPI_AgentMetrics(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + agent := c.Agent() + + metrics, err := agent.Metrics() + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(metrics.Gauges) < 0 { + t.Fatalf("bad: %v", metrics) + } + + name := metrics.Gauges[0]["Name"] + if name != "consul.runtime.alloc_bytes" { + t.Fatalf("bad: %v", metrics.Gauges[0]) + } +} + func TestAPI_AgentReload(t *testing.T) { t.Parallel() diff --git a/command/agent.go b/command/agent.go index 66c5ef30825b..fc31a53b7c0a 100644 --- a/command/agent.go +++ b/command/agent.go @@ -601,7 +601,7 @@ func circonusSink(config *agent.Config, hostname string) (metrics.MetricSink, er return sink, nil } -func startupTelemetry(config *agent.Config) error { +func startupTelemetry(config *agent.Config) (*metrics.InmemSink, error) { // Setup telemetry // Aggregate on 10 second intervals for 1 minute. Expose the // metrics over stderr when there is a SIGUSR1 received. @@ -609,6 +609,7 @@ func startupTelemetry(config *agent.Config) error { metrics.DefaultInmemSignal(memSink) metricsConf := metrics.DefaultConfig(config.Telemetry.StatsitePrefix) metricsConf.EnableHostname = !config.Telemetry.DisableHostname + metricsConf.FilterDefault = *config.Telemetry.FilterDefault var sinks metrics.FanoutSink addSink := func(name string, fn func(*agent.Config, string) (metrics.MetricSink, error)) error { @@ -623,16 +624,16 @@ func startupTelemetry(config *agent.Config) error { } if err := addSink("statsite", statsiteSink); err != nil { - return err + return nil, err } if err := addSink("statsd", statsdSink); err != nil { - return err + return nil, err } if err := addSink("dogstatd", dogstatdSink); err != nil { - return err + return nil, err } if err := addSink("circonus", circonusSink); err != nil { - return err + return nil, err } if len(sinks) > 0 { @@ -642,7 +643,7 @@ func startupTelemetry(config *agent.Config) error { metricsConf.EnableHostname = false metrics.NewGlobal(metricsConf, memSink) } - return nil + return memSink, nil } func (cmd *AgentCommand) Run(args []string) int { @@ -682,7 +683,8 @@ func (cmd *AgentCommand) run(args []string) int { cmd.logOutput = logOutput cmd.logger = log.New(logOutput, "", log.LstdFlags) - if err := startupTelemetry(config); err != nil { + memSink, err := startupTelemetry(config) + if err != nil { cmd.UI.Error(err.Error()) return 1 } @@ -696,6 +698,7 @@ func (cmd *AgentCommand) run(args []string) int { } agent.LogOutput = logOutput agent.LogWriter = logWriter + agent.MemSink = memSink if err := agent.Start(); err != nil { cmd.UI.Error(fmt.Sprintf("Error starting agent: %s", err)) diff --git a/website/source/api/agent.html.md b/website/source/api/agent.html.md index 5adee9766e30..78876c84fa41 100644 --- a/website/source/api/agent.html.md +++ b/website/source/api/agent.html.md @@ -249,6 +249,112 @@ $ curl \ https://consul.rocks/v1/agent/maintenance?enable=true&reason=For+API+docs ``` +## View Metrics + +This endpoint returns the configuration and member information of the local +agent. + +| Method | Path | Produces | +| ------ | ---------------------------- | -------------------------- | +| `GET` | `/agent/metrics` | `application/json` | + +This endpoint will dump the metrics for the most recent finished interval. +For more information about metrics, see the [telemetry](/docs/agent/telemetry.html) +page. + +| Blocking Queries | Consistency Modes | ACL Required | +| ---------------- | ----------------- | ------------ | +| `NO` | `none` | `agent:read` | + +### Sample Request + +```text +$ curl \ + https://consul.rocks/v1/agent/metrics +``` + +### Sample Response + +```json +{ + "Timestamp": "2017-08-08 02:55:10 +0000 UTC", + "Gauges": [ + { + "Name": "consul.consul.session_ttl.active", + "Value": 0, + "Labels": {} + }, + { + "Name": "consul.runtime.alloc_bytes", + "Value": 4704344, + "Labels": {} + }, + { + "Name": "consul.runtime.free_count", + "Value": 74063, + "Labels": {} + } + ], + "Points": [], + "Counters": [ + { + "Name": "consul.consul.catalog.service.query", + "Count": 1, + "Sum": 1, + "Min": 1, + "Max": 1, + "Mean": 1, + "Stddev": 0, + "Labels": { + "service": "consul" + } + }, + { + "Name": "consul.raft.apply", + "Count": 1, + "Sum": 1, + "Min": 1, + "Max": 1, + "Mean": 1, + "Stddev": 0, + "Labels": {} + } + ], + "Samples": [ + { + "Name": "consul.consul.http.GET.v1.agent.metrics", + "Count": 1, + "Sum": 0.1817069947719574, + "Min": 0.1817069947719574, + "Max": 0.1817069947719574, + "Mean": 0.1817069947719574, + "Stddev": 0, + "Labels": {} + }, + { + "Name": "consul.consul.http.GET.v1.catalog.service._", + "Count": 1, + "Sum": 0.23342099785804749, + "Min": 0.23342099785804749, + "Max": 0.23342099785804749, + "Mean": 0.23342099785804749, + "Stddev": 0, + "Labels": {} + }, + { + "Name": "consul.serf.queue.Query", + "Count": 20, + "Sum": 0, + "Min": 0, + "Max": 0, + "Mean": 0, + "Stddev": 0, + "Labels": {} + } + ] +} +``` + ## Stream Logs This endpoint streams logs from the local agent until the connection is closed. diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md index 4dfb2616c471..fde27bdbf010 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -1130,6 +1130,24 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass * `disable_hostname` This controls whether or not to prepend runtime telemetry with the machine's hostname, defaults to false. + * `prefix_filter` + This is a list of filter rules to apply for allowing/blocking metrics by prefix in the following format: + + ```javascript + [ + "+consul.raft.apply", + "-consul.http", + "+consul.http.GET" + ] + ``` + A leading "+" will enable any metrics with the given prefix, and a leading "-" will block them. If there + is overlap between two rules, the more specific rule will take precedence. Blocking will take priority if the same + prefix is listed multiple times. + + * `filter_default` + This controls whether to allow metrics that have not been specified by the filter. Defaults to `true`, which will + allow all metrics when no filters are provided. When set to `false` with no filters, no metrics will be sent. + * `circonus_api_token` A valid API Token used to create/manage check. If provided, metric management is enabled. From 0428e9fe9e951c739a2fd4fbd18565daf4e80c9a Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 8 Aug 2017 12:33:30 -0700 Subject: [PATCH 3/6] Update docs for metrics endpoint --- agent/agent.go | 3 +++ agent/config.go | 2 +- agent/http.go | 22 +++++++++++++++++++++- website/source/api/agent.html.md | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index f59a55e4ec81..1b3bc959ac0e 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -2248,5 +2248,8 @@ func (a *Agent) ReloadConfig(newCfg *Config) error { return fmt.Errorf("Failed reloading watches: %v", err) } + // Update filtered metrics + metrics.UpdateFilter(newCfg.Telemetry.AllowedPrefixes, newCfg.Telemetry.BlockedPrefixes) + return nil } diff --git a/agent/config.go b/agent/config.go index 116835cf9a19..ee57b47796e7 100644 --- a/agent/config.go +++ b/agent/config.go @@ -1483,7 +1483,7 @@ func DecodeConfig(r io.Reader) (*Config, error) { case '-': result.Telemetry.BlockedPrefixes = append(result.Telemetry.BlockedPrefixes, rule[1:]) default: - return nil, fmt.Errorf("Filter rule must begin with either '+' or '-': %s", rule) + return nil, fmt.Errorf("Filter rule must begin with either '+' or '-': %q", rule) } } diff --git a/agent/http.go b/agent/http.go index 2d43209b1869..968f2100d3f2 100644 --- a/agent/http.go +++ b/agent/http.go @@ -98,7 +98,7 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance)) handleFuncMetrics("/v1/agent/reload", s.wrap(s.AgentReload)) handleFuncMetrics("/v1/agent/monitor", s.wrap(s.AgentMonitor)) - handleFuncMetrics("/v1/agent/metrics", s.wrap(s.agent.MemSink.DisplayMetrics)) + handleFuncMetrics("/v1/agent/metrics", s.wrap(s.requireAgentRead(s.agent.MemSink.DisplayMetrics))) handleFuncMetrics("/v1/agent/services", s.wrap(s.AgentServices)) handleFuncMetrics("/v1/agent/checks", s.wrap(s.AgentChecks)) handleFuncMetrics("/v1/agent/members", s.wrap(s.AgentMembers)) @@ -264,6 +264,26 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque } } +type handlerFunc func(resp http.ResponseWriter, req *http.Request) (interface{}, error) + +// requireAgentRead wraps the given function, requiring a token with agent read permissions +func (s *HTTPServer) requireAgentRead(handler handlerFunc) handlerFunc { + return func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + // Fetch the ACL token, if any, and enforce agent policy. + var token string + s.parseToken(req, &token) + acl, err := s.agent.resolveToken(token) + if err != nil { + return nil, err + } + if acl != nil && !acl.AgentRead(s.agent.config.NodeName) { + return nil, errPermissionDenied + } + + return handler(resp, req) + } +} + // marshalJSON marshals the object into JSON, respecting the user's pretty-ness // configuration. func (s *HTTPServer) marshalJSON(req *http.Request, obj interface{}) ([]byte, error) { diff --git a/website/source/api/agent.html.md b/website/source/api/agent.html.md index 78876c84fa41..ea09e231ca46 100644 --- a/website/source/api/agent.html.md +++ b/website/source/api/agent.html.md @@ -355,6 +355,21 @@ $ curl \ } ``` +- `Timestamp` is the timestamp of the interval for the displayed metrics. Metrics are +aggregated on a ten second interval, so this value (along with the displayed metrics) +will change every ten seconds. + +- `Gauges` is a list of gauges which store one value that is updated as time goes on, +such as the amount of memory allocated. + +- `Points` is a list of point metrics, which each store a series of points under a given name. + +- `Counters` is a list of counters, which store info about a metric that is incremented +over time such as the number of requests to an HTTP endpoint. + +- `Samples` is a list of samples, which store info about the amount of time spent on an +operation, such as the time taken to serve a request to a specific http endpoint. + ## Stream Logs This endpoint streams logs from the local agent until the connection is closed. From ecaf069590726441ec88be8e59214b5ab212f367 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 8 Aug 2017 12:33:47 -0700 Subject: [PATCH 4/6] vendor: update github.com/armon/go-metrics --- vendor/github.com/armon/go-metrics/metrics.go | 22 +++++++++++++++ vendor/github.com/armon/go-metrics/start.go | 28 +++++++++---------- vendor/vendor.json | 2 +- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/vendor/github.com/armon/go-metrics/metrics.go b/vendor/github.com/armon/go-metrics/metrics.go index e51cb889b4d6..d260bd4b29ec 100644 --- a/vendor/github.com/armon/go-metrics/metrics.go +++ b/vendor/github.com/armon/go-metrics/metrics.go @@ -4,6 +4,8 @@ import ( "runtime" "strings" "time" + + "github.com/hashicorp/go-immutable-radix" ) type Label struct { @@ -127,8 +129,28 @@ func (m *Metrics) MeasureSinceWithLabels(key []string, start time.Time, labels [ m.sink.AddSampleWithLabels(key, msec, labels) } +// UpdateFilter overwrites the existing filter with the given rules. +func (m *Metrics) UpdateFilter(allow, block []string) { + m.filterLock.Lock() + defer m.filterLock.Unlock() + + m.AllowedPrefixes = allow + m.BlockedPrefixes = block + + m.filter = iradix.New() + for _, prefix := range m.AllowedPrefixes { + m.filter, _, _ = m.filter.Insert([]byte(prefix), true) + } + for _, prefix := range m.BlockedPrefixes { + m.filter, _, _ = m.filter.Insert([]byte(prefix), false) + } +} + // Returns whether the metric should be allowed based on configured prefix filters func (m *Metrics) allowMetric(key []string) bool { + m.filterLock.RLock() + defer m.filterLock.RUnlock() + if m.filter == nil || m.filter.Len() == 0 { return m.Config.FilterDefault } diff --git a/vendor/github.com/armon/go-metrics/start.go b/vendor/github.com/armon/go-metrics/start.go index f16fad0fd967..46f0c2eb2db4 100644 --- a/vendor/github.com/armon/go-metrics/start.go +++ b/vendor/github.com/armon/go-metrics/start.go @@ -2,6 +2,7 @@ package metrics import ( "os" + "sync" "sync/atomic" "time" @@ -19,18 +20,20 @@ type Config struct { EnableTypePrefix bool // Prefixes key with a type ("counter", "gauge", "timer") TimerGranularity time.Duration // Granularity of timers. ProfileInterval time.Duration // Interval to profile runtime metrics - AllowedPrefixes []string // A list of metric prefixes to allow, with '.' as the separator - BlockedPrefixes []string // A list of metric prefixes to block, with '.' as the separator - FilterDefault bool // Whether to allow metrics by default + + AllowedPrefixes []string // A list of metric prefixes to allow, with '.' as the separator + BlockedPrefixes []string // A list of metric prefixes to block, with '.' as the separator + FilterDefault bool // Whether to allow metrics by default } // Metrics represents an instance of a metrics sink that can // be used to emit type Metrics struct { Config - lastNumGC uint32 - sink MetricSink - filter *iradix.Tree + lastNumGC uint32 + sink MetricSink + filter *iradix.Tree + filterLock sync.RWMutex } // Shared global metrics instance @@ -65,14 +68,7 @@ func New(conf *Config, sink MetricSink) (*Metrics, error) { met := &Metrics{} met.Config = *conf met.sink = sink - met.filter = iradix.New() - - for _, prefix := range conf.AllowedPrefixes { - met.filter, _, _ = met.filter.Insert([]byte(prefix), true) - } - for _, prefix := range conf.BlockedPrefixes { - met.filter, _, _ = met.filter.Insert([]byte(prefix), false) - } + met.UpdateFilter(conf.AllowedPrefixes, conf.BlockedPrefixes) // Start the runtime collector if conf.EnableRuntimeMetrics { @@ -127,3 +123,7 @@ func MeasureSince(key []string, start time.Time) { func MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { globalMetrics.Load().(*Metrics).MeasureSinceWithLabels(key, start, labels) } + +func UpdateFilter(allow, block []string) { + globalMetrics.Load().(*Metrics).UpdateFilter(allow, block) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 8eb3f32c4aa9..92dfc1350603 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -6,7 +6,7 @@ {"checksumSHA1":"AzjRkOQtVBTwIw4RJLTygFhJs3s=","path":"github.com/Microsoft/go-winio","revision":"c4dc1301f1dc0307acd38e611aa375a64dfe0642","revisionTime":"2017-07-12T04:46:15Z"}, {"checksumSHA1":"9NR0rrcAT5J76C5xMS4AVksS9o0=","path":"github.com/StackExchange/wmi","revision":"e54cbda6595d7293a7a468ccf9525f6bc8887f99","revisionTime":"2016-08-11T21:45:55Z"}, {"checksumSHA1":"l0iFqayYAaEip6Olaq3/LCOa/Sg=","path":"github.com/armon/circbuf","revision":"bbbad097214e2918d8543d5201d12bfd7bca254d","revisionTime":"2015-08-27T00:49:46Z"}, - {"checksumSHA1":"1fTBW3jW8+cYV5UNKrAMXsQhqnQ=","path":"github.com/armon/go-metrics","revision":"efce74234d0fcaac1bd5b2a32d6d3afd108c4498","revisionTime":"2017-08-08T03:49:35Z"}, + {"checksumSHA1":"gXAf+SOZfdKaXaO03vL5LH12g8s=","path":"github.com/armon/go-metrics","revision":"2e4f2be0fe4f6b7096471aa85f2c342bff3b8f4f","revisionTime":"2017-08-08T19:31:08Z"}, {"checksumSHA1":"xCsGGM9TKBogZDfSN536KtQdLko=","path":"github.com/armon/go-metrics/circonus","revision":"ded85ed431a7aee3f3af79f082b704d948058f64","revisionTime":"2017-08-07T19:17:41Z"}, {"checksumSHA1":"Dt0n1sSivvvdZQdzc4Hu/yOG+T0=","path":"github.com/armon/go-metrics/datadog","revision":"ded85ed431a7aee3f3af79f082b704d948058f64","revisionTime":"2017-08-07T19:17:41Z"}, {"checksumSHA1":"gNO0JNpLzYOdInGeq7HqMZUzx9M=","path":"github.com/armon/go-radix","revision":"4239b77079c7b5d1243b7b4736304ce8ddb6f0f2","revisionTime":"2016-01-15T23:47:25Z"}, From 5b998cacb134666d7101a1d82356e34d93ac87db Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 8 Aug 2017 12:33:59 -0700 Subject: [PATCH 5/6] Update api structs for metrics endpoint --- api/agent.go | 35 +++++++++++++++++++++++++++++++---- api/agent_test.go | 3 +-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/api/agent.go b/api/agent.go index 2f43d8eda255..fe82ab2b3821 100644 --- a/api/agent.go +++ b/api/agent.go @@ -99,10 +99,37 @@ type AgentToken struct { // Metrics info is used to store different types of metric values from the agent. type MetricsInfo struct { Timestamp string - Gauges []map[string]interface{} - Points []map[string]interface{} - Counters []map[string]interface{} - Samples []map[string]interface{} + Gauges []GaugeValue + Points []PointValue + Counters []SampledValue + Samples []SampledValue +} + +// GaugeValue stores one value that is updated as time goes on, such as +// the amount of memory allocated. +type GaugeValue struct { + Name string + Value float32 + Labels map[string]string +} + +// PointValue holds a series of points for a metric. +type PointValue struct { + Name string + Points []float32 +} + +// SampledValue stores info about a metric that is incremented over time, +// such as the number of requests to an HTTP endpoint. +type SampledValue struct { + Name string + Count int + Sum float64 + Min float64 + Max float64 + Mean float64 + Stddev float64 + Labels map[string]string } // Agent can be used to query the Agent endpoints diff --git a/api/agent_test.go b/api/agent_test.go index 1b2cf2cba662..7cdc2f255234 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -44,8 +44,7 @@ func TestAPI_AgentMetrics(t *testing.T) { t.Fatalf("bad: %v", metrics) } - name := metrics.Gauges[0]["Name"] - if name != "consul.runtime.alloc_bytes" { + if metrics.Gauges[0].Name != "consul.runtime.alloc_bytes" { t.Fatalf("bad: %v", metrics.Gauges[0]) } } From c1c883f441d5a436e3f97e84412f56fd8c215088 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 8 Aug 2017 13:05:38 -0700 Subject: [PATCH 6/6] Add doc links for metrics endpoint --- agent/agent_endpoint.go | 15 +++++++++++ agent/agent_endpoint_test.go | 28 +++++++++++++++++++++ agent/http.go | 22 +--------------- website/source/docs/agent/options.html.md | 1 + website/source/docs/agent/telemetry.html.md | 3 ++- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 0296c3e1cccc..1ce49faec02d 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -54,6 +54,21 @@ func (s *HTTPServer) AgentSelf(resp http.ResponseWriter, req *http.Request) (int }, nil } +func (s *HTTPServer) AgentMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + // Fetch the ACL token, if any, and enforce agent policy. + var token string + s.parseToken(req, &token) + acl, err := s.agent.resolveToken(token) + if err != nil { + return nil, err + } + if acl != nil && !acl.AgentRead(s.agent.config.NodeName) { + return nil, errPermissionDenied + } + + return s.agent.MemSink.DisplayMetrics(resp, req) +} + func (s *HTTPServer) AgentReload(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "PUT" { resp.WriteHeader(http.StatusMethodNotAllowed) diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 1d9fe5ff7ebd..0b2dd2f0eb13 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -239,6 +239,34 @@ func TestAgent_Self_ACLDeny(t *testing.T) { }) } +func TestAgent_Metrics_ACLDeny(t *testing.T) { + t.Parallel() + a := NewTestAgent(t.Name(), TestACLConfig()) + defer a.Shutdown() + + t.Run("no token", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/agent/metrics", nil) + if _, err := a.srv.AgentSelf(nil, req); !isPermissionDenied(err) { + t.Fatalf("err: %v", err) + } + }) + + t.Run("agent master token", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/agent/metrics?token=towel", nil) + if _, err := a.srv.AgentSelf(nil, req); err != nil { + t.Fatalf("err: %v", err) + } + }) + + t.Run("read-only token", func(t *testing.T) { + ro := makeReadOnlyAgentACL(t, a.srv) + req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/agent/metrics?token=%s", ro), nil) + if _, err := a.srv.AgentSelf(nil, req); err != nil { + t.Fatalf("err: %v", err) + } + }) +} + func TestAgent_Reload(t *testing.T) { t.Parallel() cfg := TestConfig() diff --git a/agent/http.go b/agent/http.go index 968f2100d3f2..8806ee2d2c06 100644 --- a/agent/http.go +++ b/agent/http.go @@ -98,7 +98,7 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance)) handleFuncMetrics("/v1/agent/reload", s.wrap(s.AgentReload)) handleFuncMetrics("/v1/agent/monitor", s.wrap(s.AgentMonitor)) - handleFuncMetrics("/v1/agent/metrics", s.wrap(s.requireAgentRead(s.agent.MemSink.DisplayMetrics))) + handleFuncMetrics("/v1/agent/metrics", s.wrap(s.AgentMetrics)) handleFuncMetrics("/v1/agent/services", s.wrap(s.AgentServices)) handleFuncMetrics("/v1/agent/checks", s.wrap(s.AgentChecks)) handleFuncMetrics("/v1/agent/members", s.wrap(s.AgentMembers)) @@ -264,26 +264,6 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque } } -type handlerFunc func(resp http.ResponseWriter, req *http.Request) (interface{}, error) - -// requireAgentRead wraps the given function, requiring a token with agent read permissions -func (s *HTTPServer) requireAgentRead(handler handlerFunc) handlerFunc { - return func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Fetch the ACL token, if any, and enforce agent policy. - var token string - s.parseToken(req, &token) - acl, err := s.agent.resolveToken(token) - if err != nil { - return nil, err - } - if acl != nil && !acl.AgentRead(s.agent.config.NodeName) { - return nil, errPermissionDenied - } - - return handler(resp, req) - } -} - // marshalJSON marshals the object into JSON, respecting the user's pretty-ness // configuration. func (s *HTTPServer) marshalJSON(req *http.Request, obj interface{}) ([]byte, error) { diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md index fde27bdbf010..89dffbc89e23 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -1345,3 +1345,4 @@ items which are reloaded include: * Watches * HTTP Client Address * Node Metadata +* Metric Prefix Filter diff --git a/website/source/docs/agent/telemetry.html.md b/website/source/docs/agent/telemetry.html.md index b63f68686814..02bb3767ca96 100644 --- a/website/source/docs/agent/telemetry.html.md +++ b/website/source/docs/agent/telemetry.html.md @@ -22,7 +22,8 @@ getting a better view of what Consul is doing. Additionally, if the [`telemetry` configuration options](/docs/agent/options.html#telemetry) are provided, the telemetry information will be streamed to a [statsite](http://github.com/armon/statsite) or [statsd](http://github.com/etsy/statsd) server where -it can be aggregated and flushed to Graphite or any other metrics store. +it can be aggregated and flushed to Graphite or any other metrics store. This +information can also be viewed with the [metrics endpoint](/api/agent.html#view-metrics) Below is sample output of a telemetry dump: