1package metrics
2
3import (
4	"runtime"
5	"strings"
6	"time"
7
8	"github.com/hashicorp/go-immutable-radix"
9)
10
11type Label struct {
12	Name  string
13	Value string
14}
15
16func (m *Metrics) SetGauge(key []string, val float32) {
17	m.SetGaugeWithLabels(key, val, nil)
18}
19
20func (m *Metrics) SetGaugeWithLabels(key []string, val float32, labels []Label) {
21	if m.HostName != "" {
22		if m.EnableHostnameLabel {
23			labels = append(labels, Label{"host", m.HostName})
24		} else if m.EnableHostname {
25			key = insert(0, m.HostName, key)
26		}
27	}
28	if m.EnableTypePrefix {
29		key = insert(0, "gauge", key)
30	}
31	if m.ServiceName != "" {
32		if m.EnableServiceLabel {
33			labels = append(labels, Label{"service", m.ServiceName})
34		} else {
35			key = insert(0, m.ServiceName, key)
36		}
37	}
38	allowed, labelsFiltered := m.allowMetric(key, labels)
39	if !allowed {
40		return
41	}
42	m.sink.SetGaugeWithLabels(key, val, labelsFiltered)
43}
44
45func (m *Metrics) EmitKey(key []string, val float32) {
46	if m.EnableTypePrefix {
47		key = insert(0, "kv", key)
48	}
49	if m.ServiceName != "" {
50		key = insert(0, m.ServiceName, key)
51	}
52	allowed, _ := m.allowMetric(key, nil)
53	if !allowed {
54		return
55	}
56	m.sink.EmitKey(key, val)
57}
58
59func (m *Metrics) IncrCounter(key []string, val float32) {
60	m.IncrCounterWithLabels(key, val, nil)
61}
62
63func (m *Metrics) IncrCounterWithLabels(key []string, val float32, labels []Label) {
64	if m.HostName != "" && m.EnableHostnameLabel {
65		labels = append(labels, Label{"host", m.HostName})
66	}
67	if m.EnableTypePrefix {
68		key = insert(0, "counter", key)
69	}
70	if m.ServiceName != "" {
71		if m.EnableServiceLabel {
72			labels = append(labels, Label{"service", m.ServiceName})
73		} else {
74			key = insert(0, m.ServiceName, key)
75		}
76	}
77	allowed, labelsFiltered := m.allowMetric(key, labels)
78	if !allowed {
79		return
80	}
81	m.sink.IncrCounterWithLabels(key, val, labelsFiltered)
82}
83
84func (m *Metrics) AddSample(key []string, val float32) {
85	m.AddSampleWithLabels(key, val, nil)
86}
87
88func (m *Metrics) AddSampleWithLabels(key []string, val float32, labels []Label) {
89	if m.HostName != "" && m.EnableHostnameLabel {
90		labels = append(labels, Label{"host", m.HostName})
91	}
92	if m.EnableTypePrefix {
93		key = insert(0, "sample", key)
94	}
95	if m.ServiceName != "" {
96		if m.EnableServiceLabel {
97			labels = append(labels, Label{"service", m.ServiceName})
98		} else {
99			key = insert(0, m.ServiceName, key)
100		}
101	}
102	allowed, labelsFiltered := m.allowMetric(key, labels)
103	if !allowed {
104		return
105	}
106	m.sink.AddSampleWithLabels(key, val, labelsFiltered)
107}
108
109func (m *Metrics) MeasureSince(key []string, start time.Time) {
110	m.MeasureSinceWithLabels(key, start, nil)
111}
112
113func (m *Metrics) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) {
114	if m.HostName != "" && m.EnableHostnameLabel {
115		labels = append(labels, Label{"host", m.HostName})
116	}
117	if m.EnableTypePrefix {
118		key = insert(0, "timer", key)
119	}
120	if m.ServiceName != "" {
121		if m.EnableServiceLabel {
122			labels = append(labels, Label{"service", m.ServiceName})
123		} else {
124			key = insert(0, m.ServiceName, key)
125		}
126	}
127	allowed, labelsFiltered := m.allowMetric(key, labels)
128	if !allowed {
129		return
130	}
131	now := time.Now()
132	elapsed := now.Sub(start)
133	msec := float32(elapsed.Nanoseconds()) / float32(m.TimerGranularity)
134	m.sink.AddSampleWithLabels(key, msec, labelsFiltered)
135}
136
137// UpdateFilter overwrites the existing filter with the given rules.
138func (m *Metrics) UpdateFilter(allow, block []string) {
139	m.UpdateFilterAndLabels(allow, block, m.AllowedLabels, m.BlockedLabels)
140}
141
142// UpdateFilterAndLabels overwrites the existing filter with the given rules.
143func (m *Metrics) UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) {
144	m.filterLock.Lock()
145	defer m.filterLock.Unlock()
146
147	m.AllowedPrefixes = allow
148	m.BlockedPrefixes = block
149
150	if allowedLabels == nil {
151		// Having a white list means we take only elements from it
152		m.allowedLabels = nil
153	} else {
154		m.allowedLabels = make(map[string]bool)
155		for _, v := range allowedLabels {
156			m.allowedLabels[v] = true
157		}
158	}
159	m.blockedLabels = make(map[string]bool)
160	for _, v := range blockedLabels {
161		m.blockedLabels[v] = true
162	}
163	m.AllowedLabels = allowedLabels
164	m.BlockedLabels = blockedLabels
165
166	m.filter = iradix.New()
167	for _, prefix := range m.AllowedPrefixes {
168		m.filter, _, _ = m.filter.Insert([]byte(prefix), true)
169	}
170	for _, prefix := range m.BlockedPrefixes {
171		m.filter, _, _ = m.filter.Insert([]byte(prefix), false)
172	}
173}
174
175// labelIsAllowed return true if a should be included in metric
176// the caller should lock m.filterLock while calling this method
177func (m *Metrics) labelIsAllowed(label *Label) bool {
178	labelName := (*label).Name
179	if m.blockedLabels != nil {
180		_, ok := m.blockedLabels[labelName]
181		if ok {
182			// If present, let's remove this label
183			return false
184		}
185	}
186	if m.allowedLabels != nil {
187		_, ok := m.allowedLabels[labelName]
188		return ok
189	}
190	// Allow by default
191	return true
192}
193
194// filterLabels return only allowed labels
195// the caller should lock m.filterLock while calling this method
196func (m *Metrics) filterLabels(labels []Label) []Label {
197	if labels == nil {
198		return nil
199	}
200	toReturn := []Label{}
201	for _, label := range labels {
202		if m.labelIsAllowed(&label) {
203			toReturn = append(toReturn, label)
204		}
205	}
206	return toReturn
207}
208
209// Returns whether the metric should be allowed based on configured prefix filters
210// Also return the applicable labels
211func (m *Metrics) allowMetric(key []string, labels []Label) (bool, []Label) {
212	m.filterLock.RLock()
213	defer m.filterLock.RUnlock()
214
215	if m.filter == nil || m.filter.Len() == 0 {
216		return m.Config.FilterDefault, m.filterLabels(labels)
217	}
218
219	_, allowed, ok := m.filter.Root().LongestPrefix([]byte(strings.Join(key, ".")))
220	if !ok {
221		return m.Config.FilterDefault, m.filterLabels(labels)
222	}
223
224	return allowed.(bool), m.filterLabels(labels)
225}
226
227// Periodically collects runtime stats to publish
228func (m *Metrics) collectStats() {
229	for {
230		time.Sleep(m.ProfileInterval)
231		m.emitRuntimeStats()
232	}
233}
234
235// Emits various runtime statsitics
236func (m *Metrics) emitRuntimeStats() {
237	// Export number of Goroutines
238	numRoutines := runtime.NumGoroutine()
239	m.SetGauge([]string{"runtime", "num_goroutines"}, float32(numRoutines))
240
241	// Export memory stats
242	var stats runtime.MemStats
243	runtime.ReadMemStats(&stats)
244	m.SetGauge([]string{"runtime", "alloc_bytes"}, float32(stats.Alloc))
245	m.SetGauge([]string{"runtime", "sys_bytes"}, float32(stats.Sys))
246	m.SetGauge([]string{"runtime", "malloc_count"}, float32(stats.Mallocs))
247	m.SetGauge([]string{"runtime", "free_count"}, float32(stats.Frees))
248	m.SetGauge([]string{"runtime", "heap_objects"}, float32(stats.HeapObjects))
249	m.SetGauge([]string{"runtime", "total_gc_pause_ns"}, float32(stats.PauseTotalNs))
250	m.SetGauge([]string{"runtime", "total_gc_runs"}, float32(stats.NumGC))
251
252	// Export info about the last few GC runs
253	num := stats.NumGC
254
255	// Handle wrap around
256	if num < m.lastNumGC {
257		m.lastNumGC = 0
258	}
259
260	// Ensure we don't scan more than 256
261	if num-m.lastNumGC >= 256 {
262		m.lastNumGC = num - 255
263	}
264
265	for i := m.lastNumGC; i < num; i++ {
266		pause := stats.PauseNs[i%256]
267		m.AddSample([]string{"runtime", "gc_pause_ns"}, float32(pause))
268	}
269	m.lastNumGC = num
270}
271
272// Inserts a string value at an index into the slice
273func insert(i int, v string, s []string) []string {
274	s = append(s, "")
275	copy(s[i+1:], s[i:])
276	s[i] = v
277	return s
278}
279