Skip to content

Commit bc7bea7

Browse files
theshadowRaphaël Simon
authored and
Raphaël Simon
committed
Modify metrics so that names are normalized to legal chars (goadesign#771)
* Add normalizeKeys() function for normalizing metric key names * Properly synchronize updates to metrics object
1 parent 61b3b6e commit bc7bea7

File tree

2 files changed

+221
-18
lines changed

2 files changed

+221
-18
lines changed

metrics.go

+89-18
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,104 @@
33
package goa
44

55
import (
6+
"regexp"
7+
"strings"
8+
"sync/atomic"
69
"time"
710

811
"github.com/armon/go-metrics"
912
)
1013

11-
// metriks is the local instance of metrics.Metrics
12-
var metriks *metrics.Metrics
14+
const (
15+
allMatcher string = "*/*"
16+
allReplacement string = "all"
17+
normalizedToken string = "_"
18+
)
19+
20+
var (
21+
// metriks atomic value storage
22+
metriks atomic.Value
23+
24+
// invalidCharactersRE is the invert match of validCharactersRE
25+
invalidCharactersRE = regexp.MustCompile(`[\*/]`)
26+
27+
// Taken from https://github.com/prometheus/client_golang/blob/66058aac3a83021948e5fb12f1f408ff556b9037/prometheus/desc.go
28+
validCharactersRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_:]*$`)
29+
)
30+
31+
func init() {
32+
m, err := metrics.New(metrics.DefaultConfig("service"), NewNoOpSink())
33+
if err != nil {
34+
panic("Unable to instantiate default metrics sink")
35+
}
36+
37+
SetMetrics(m)
38+
}
39+
40+
// NewNoOpSink returns a NOOP sink.
41+
func NewNoOpSink() metrics.MetricSink {
42+
return &NoOpSink{}
43+
}
44+
45+
// NoOpSink default NOOP metrics recorder
46+
type NoOpSink struct{}
47+
48+
// SetGauge method
49+
func (md *NoOpSink) SetGauge(key []string, val float32) {}
50+
51+
// EmitKey method
52+
func (md *NoOpSink) EmitKey(key []string, val float32) {}
53+
54+
// IncrCounter method
55+
func (md *NoOpSink) IncrCounter(key []string, val float32) {}
56+
57+
// AddSample method
58+
func (md *NoOpSink) AddSample(key []string, val float32) {}
59+
60+
// MeasureSince method
61+
func (md *NoOpSink) MeasureSince(key []string, start time.Time) {}
1362

1463
// NewMetrics initializes goa's metrics instance with the supplied
1564
// configuration and metrics sink
65+
// This method is deprecated and SetMetrics should be used instead.
1666
func NewMetrics(conf *metrics.Config, sink metrics.MetricSink) (err error) {
17-
metriks, err = metrics.NewGlobal(conf, sink)
18-
return
67+
m, err := metrics.NewGlobal(conf, sink)
68+
SetMetrics(m)
69+
70+
return nil
71+
}
72+
73+
// SetMetrics initializes goa's metrics instance with the supplied metrics adapter interface.
74+
func SetMetrics(m *metrics.Metrics) {
75+
metriks.Store(m)
1976
}
2077

2178
// AddSample adds a sample to an aggregated metric
2279
// reporting count, min, max, mean, and std deviation
2380
// Usage:
2481
// AddSample([]string{"my","namespace","key"}, 15.0)
2582
func AddSample(key []string, val float32) {
26-
if metriks != nil {
27-
metriks.AddSample(key, val)
28-
}
83+
normalizeKeys(key)
84+
85+
metriks.Load().(*metrics.Metrics).AddSample(key, val)
2986
}
3087

3188
// EmitKey emits a key/value pair
3289
// Usage:
3390
// EmitKey([]string{"my","namespace","key"}, 15.0)
3491
func EmitKey(key []string, val float32) {
35-
if metriks != nil {
36-
metriks.EmitKey(key, val)
37-
}
92+
normalizeKeys(key)
93+
94+
metriks.Load().(*metrics.Metrics).EmitKey(key, val)
3895
}
3996

4097
// IncrCounter increments the counter named by `key`
4198
// Usage:
4299
// IncrCounter([]key{"my","namespace","counter"}, 1.0)
43100
func IncrCounter(key []string, val float32) {
44-
if metriks != nil {
45-
metriks.IncrCounter(key, val)
46-
}
101+
normalizeKeys(key)
102+
103+
metriks.Load().(*metrics.Metrics).IncrCounter(key, val)
47104
}
48105

49106
// MeasureSince creates a timing metric that records
@@ -53,16 +110,30 @@ func IncrCounter(key []string, val float32) {
53110
// Frequently used in a defer:
54111
// defer MeasureSince([]string{"my","namespace","action}, time.Now())
55112
func MeasureSince(key []string, start time.Time) {
56-
if metriks != nil {
57-
metriks.MeasureSince(key, start)
58-
}
113+
normalizeKeys(key)
114+
115+
metriks.Load().(*metrics.Metrics).MeasureSince(key, start)
59116
}
60117

61118
// SetGauge sets the named gauge to the specified value
62119
// Usage:
63120
// SetGauge([]string{"my","namespace"}, 2.0)
64121
func SetGauge(key []string, val float32) {
65-
if metriks != nil {
66-
metriks.SetGauge(key, val)
122+
normalizeKeys(key)
123+
124+
metriks.Load().(*metrics.Metrics).SetGauge(key, val)
125+
}
126+
127+
// This function is used to make metric names safe for all metric services. Specifically, prometheus does
128+
// not support * or / in metric names.
129+
func normalizeKeys(key []string) {
130+
for i, k := range key {
131+
if !validCharactersRE.MatchString(k) {
132+
// first replace */* with all
133+
k = strings.Replace(k, allMatcher, allReplacement, -1)
134+
135+
// now replace all other invalid characters with a safe one.
136+
key[i] = invalidCharactersRE.ReplaceAllString(k, normalizedToken)
137+
}
67138
}
68139
}

metrics_test.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package goa_test
2+
3+
import (
4+
"time"
5+
6+
"github.com/goadesign/goa"
7+
8+
. "github.com/onsi/ginkgo"
9+
. "github.com/onsi/gomega"
10+
11+
"github.com/armon/go-metrics"
12+
)
13+
14+
// mock metrics
15+
type mockSink struct{}
16+
17+
func (m *mockSink) SetGauge(key []string, val float32) {}
18+
func (m *mockSink) EmitKey(key []string, val float32) {}
19+
func (m *mockSink) IncrCounter(key []string, val float32) {}
20+
func (m *mockSink) AddSample(key []string, val float32) {}
21+
func (m *mockSink) MeasureSince(key []string, start time.Time) {}
22+
23+
var _ = Describe("Metrics", func() {
24+
var keys = [6]string{}
25+
var metriks *metrics.Metrics
26+
var sink *mockSink
27+
28+
BeforeEach(func() {
29+
sink = &mockSink{}
30+
31+
var err error
32+
metriks, err = metrics.New(metrics.DefaultConfig("UnitTest Service"), sink)
33+
34+
if err != nil {
35+
panic("Unable to create test instance of metrics")
36+
}
37+
38+
keys = [6]string{
39+
"foo_bar_*/*",
40+
"foo_*_baz",
41+
"foo/baz",
42+
"foo/bar/baz",
43+
"foo/bar*_*/*",
44+
"//foo/bar*",
45+
}
46+
})
47+
48+
Describe("Add sample", func() {
49+
Context("With invalid characters in key", func() {
50+
It("should replace invalid characters with normalized characters", func() {
51+
goa.SetMetrics(metriks)
52+
goa.AddSample(keys[:], 3.14)
53+
Ω(keys).Should(ConsistOf([]string{
54+
"foo_bar_all",
55+
"foo___baz",
56+
"foo_baz",
57+
"foo_bar_baz",
58+
"foo_bar__all",
59+
"__foo_bar_",
60+
}))
61+
})
62+
})
63+
})
64+
65+
Describe("Emit key", func() {
66+
Context("With invalid characters in key", func() {
67+
It("should replace invalid characters with normalized characters", func() {
68+
goa.SetMetrics(metriks)
69+
goa.EmitKey(keys[:], 3.14)
70+
Ω(keys).Should(ConsistOf([]string{
71+
"foo_bar_all",
72+
"foo___baz",
73+
"foo_baz",
74+
"foo_bar_baz",
75+
"foo_bar__all",
76+
"__foo_bar_",
77+
}))
78+
})
79+
})
80+
})
81+
82+
Describe("Increment Counter", func() {
83+
Context("With invalid characters in key", func() {
84+
It("should replace invalid characters with normalized characters", func() {
85+
goa.SetMetrics(metriks)
86+
goa.IncrCounter(keys[:], 3.14)
87+
Ω(keys).Should(ConsistOf([]string{
88+
"foo_bar_all",
89+
"foo___baz",
90+
"foo_baz",
91+
"foo_bar_baz",
92+
"foo_bar__all",
93+
"__foo_bar_",
94+
}))
95+
})
96+
})
97+
})
98+
99+
Describe("Measure since", func() {
100+
Context("With invalid characters in key", func() {
101+
It("should replace invalid characters with normalized characters", func() {
102+
goa.SetMetrics(metriks)
103+
goa.MeasureSince(keys[:], time.Time{})
104+
Ω(keys).Should(ConsistOf([]string{
105+
"foo_bar_all",
106+
"foo___baz",
107+
"foo_baz",
108+
"foo_bar_baz",
109+
"foo_bar__all",
110+
"__foo_bar_",
111+
}))
112+
})
113+
})
114+
})
115+
116+
Describe("Set gauge", func() {
117+
Context("With invalid characters in key", func() {
118+
It("should replace invalid characters with normalized characters", func() {
119+
goa.SetMetrics(metriks)
120+
goa.SetGauge(keys[:], 3.14)
121+
Ω(keys).Should(ConsistOf([]string{
122+
"foo_bar_all",
123+
"foo___baz",
124+
"foo_baz",
125+
"foo_bar_baz",
126+
"foo_bar__all",
127+
"__foo_bar_",
128+
}))
129+
})
130+
})
131+
})
132+
})

0 commit comments

Comments
 (0)