/
metrics.go
196 lines (165 loc) · 5.21 KB
/
metrics.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/*
Package metrics is part of the Tarmac suite of Host Callback packages. This package provides users with the ability
to provide WASM functions with a host callback interface that provides metrics tracking capabilities.
import (
"github.com/madflojo/tarmac/pkg/callbacks"
"github.com/madflojo/tarmac/pkg/callbacks/metrics"
)
func main() {
// Create instance of metrics to register for callback execution
metrics := metrics.New(metrics.Config{})
// Create Callback router and register metrics
router := callbacks.New()
router.RegisterCallback("metrics", "Counter", metrics.Counter)
}
*/
package metrics
import (
"fmt"
"github.com/madflojo/tarmac"
"github.com/pquerna/ffjson/ffjson"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"regexp"
"sync"
)
// Metrics stores and manages the user-defined metrics created via
// WASM function callbacks.
type Metrics struct {
sync.Mutex
// all contains a map of all custom defined metrics
all map[string]string
// counters holds a map of existing custom counters
counters map[string]prometheus.Counter
// gauges holds a map of existing custom gauges
gauges map[string]prometheus.Gauge
// histograms holds a map of existing custom summaries
histograms map[string]prometheus.Summary
}
// ErrInvalidMetricName is an error returned when the user supplies an
// invalid formatted metric name.
var ErrInvalidMetricName = fmt.Errorf("invalid metric name")
// isMetricNameValid is a regex used to validate metric names.
var isMetricNameValid = regexp.MustCompile(`^[a-zA-Z0-9_:][a-zA-Z0-9_:]*$`)
// Config is provided to users to configure the Host Callback. All Tarmac Callbacks follow the same configuration
// format; each Config struct gives the specific Host Callback unique functionality.
type Config struct{}
// New will create a new instance of metrics enabling users to
// collect custom metrics.
func New(cfg Config) (*Metrics, error) {
m := &Metrics{}
m.all = make(map[string]string)
m.counters = make(map[string]prometheus.Counter)
m.gauges = make(map[string]prometheus.Gauge)
m.histograms = make(map[string]prometheus.Summary)
return m, nil
}
// Counter will create and increment a counter metric. The expected input for
// this function is a MetricsCounter JSON.
func (m *Metrics) Counter(b []byte) ([]byte, error) {
// Parse incoming Request
var rq tarmac.MetricsCounter
err := ffjson.Unmarshal(b, &rq)
if err != nil {
return []byte(""), fmt.Errorf("unable to parse input JSON - %s", err)
}
// Verify Name Value
if !isMetricNameValid.MatchString(rq.Name) {
return []byte(""), ErrInvalidMetricName
}
// Map Safety
m.Lock()
defer m.Unlock()
// Check if counter already exists, if not create one
_, ok := m.counters[rq.Name]
if !ok {
// Check if name is already used
_, ok2 := m.all[rq.Name]
if ok2 {
return []byte(""), fmt.Errorf("metric name in use")
}
m.counters[rq.Name] = promauto.NewCounter(prometheus.CounterOpts{
Name: rq.Name,
})
m.all[rq.Name] = "counter"
}
// Perform action
m.counters[rq.Name].Inc()
return []byte(""), nil
}
// Gauge will create a gauge metric and either increment or decrement the value
// based on the provided input. The expected input for this function is a
// MetricsGauge JSON.
func (m *Metrics) Gauge(b []byte) ([]byte, error) {
// Parse incoming Request
var rq tarmac.MetricsGauge
err := ffjson.Unmarshal(b, &rq)
if err != nil {
return []byte(""), fmt.Errorf("unable to parse input JSON - %s", err)
}
// Verify Name Value
if !isMetricNameValid.MatchString(rq.Name) {
return []byte(""), ErrInvalidMetricName
}
// Map Safety
m.Lock()
defer m.Unlock()
// Check if gauge already exists, if not create one
_, ok := m.gauges[rq.Name]
if !ok {
// Check if name is already used
_, ok2 := m.all[rq.Name]
if ok2 {
return []byte(""), fmt.Errorf("metric name in use")
}
m.gauges[rq.Name] = promauto.NewGauge(prometheus.GaugeOpts{
Name: rq.Name,
})
m.all[rq.Name] = "gauge"
}
// Perform action
switch rq.Action {
case "inc":
m.gauges[rq.Name].Inc()
case "dec":
m.gauges[rq.Name].Dec()
default:
return []byte(""), fmt.Errorf("invalid action")
}
return []byte(""), nil
}
// Histogram will create a histogram or summary metric and observe the
// provided values. The expected input for this function is a
// MetricsHistogram JSON.
func (m *Metrics) Histogram(b []byte) ([]byte, error) {
// Parse incoming Request
var rq tarmac.MetricsHistogram
err := ffjson.Unmarshal(b, &rq)
if err != nil {
return []byte(""), fmt.Errorf("unable to parse input JSON - %s", err)
}
// Verify Name Value
if !isMetricNameValid.MatchString(rq.Name) {
return []byte(""), ErrInvalidMetricName
}
// Map Safety
m.Lock()
defer m.Unlock()
// Check if histogram already exists, if not create one
_, ok := m.histograms[rq.Name]
if !ok {
// Check if name is already used
_, ok2 := m.all[rq.Name]
if ok2 {
return []byte(""), fmt.Errorf("metric name in use")
}
m.histograms[rq.Name] = promauto.NewSummary(prometheus.SummaryOpts{
Name: rq.Name,
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
})
m.all[rq.Name] = "histogram"
}
// Perform action
m.histograms[rq.Name].Observe(rq.Value)
return []byte(""), nil
}