Skip to content

Commit bd0039f

Browse files
yasirfolio3msohailhussainpulak-opti
authored
[FSSDK-8707] feat(metrics): Adds support for prometheus (#348)
* Added support for prometheus. * Added unit tests. * Added unit tests. * updated headers. * fixes. * cleanup. * Adding support for labels in prometheus and expvar. * Refactor code * fix readme * Fix copyright year --------- Co-authored-by: Mirza Sohail Hussain <[email protected]> Co-authored-by: Pulak Bhowmick <[email protected]>
1 parent 819dd62 commit bd0039f

File tree

16 files changed

+482
-74
lines changed

16 files changed

+482
-74
lines changed

cmd/optimizely/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ func main() {
144144

145145
setRuntimeEnvironment(conf.Runtime)
146146

147-
agentMetricsRegistry := metrics.NewRegistry()
147+
// Set metrics type to be used
148+
agentMetricsRegistry := metrics.NewRegistry(conf.Admin.MetricsType)
148149
sdkMetricsRegistry := optimizely.NewRegistry(agentMetricsRegistry)
149150

150151
ctx, cancel := context.WithCancel(context.Background()) // Create default service context

cmd/optimizely/main_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func assertLog(t *testing.T, actual config.LogConfig) {
121121

122122
func assertAdmin(t *testing.T, actual config.AdminConfig) {
123123
assert.Equal(t, "3002", actual.Port)
124+
assert.Equal(t, "prometheus", actual.MetricsType)
124125
}
125126

126127
func assertAdminAuth(t *testing.T, actual config.ServiceAuthConfig) {
@@ -282,6 +283,7 @@ func TestViperProps(t *testing.T) {
282283
v.Set("log.level", "debug")
283284

284285
v.Set("admin.port", "3002")
286+
v.Set("admin.metricsType", "prometheus")
285287
v.Set("admin.auth.ttl", "30m")
286288
v.Set("admin.auth.hmacSecrets", "efgh,ijkl")
287289
v.Set("admin.auth.jwksURL", "admin_jwks_url")
@@ -375,6 +377,7 @@ func TestViperEnv(t *testing.T) {
375377
_ = os.Setenv("OPTIMIZELY_LOG_LEVEL", "debug")
376378

377379
_ = os.Setenv("OPTIMIZELY_ADMIN_PORT", "3002")
380+
_ = os.Setenv("OPTIMIZELY_ADMIN_METRICSTYPE", "prometheus")
378381

379382
_ = os.Setenv("OPTIMIZELY_API_MAXCONNS", "100")
380383
_ = os.Setenv("OPTIMIZELY_API_PORT", "3000")

cmd/optimizely/testdata/default.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ client:
7070

7171
admin:
7272
port: "3002"
73+
metricsType: "prometheus"
7374
auth:
7475
ttl: 30m
7576
hmacSecrets:

config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ api:
9595
admin:
9696
## http listener port
9797
port: "8088"
98+
## metrics package to use
99+
## supported packages are expvar and prometheus
100+
## default is expvar
101+
metricsType: ""
98102
##
99103
## webhook service receives update notifications to your Optimizely project. Receipt of the webhook will
100104
## trigger an immediate download of the datafile from the CDN

config/config.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ func NewDefaultConfig() *AgentConfig {
3939
JwksURL: "",
4040
JwksUpdateInterval: 0,
4141
},
42-
Port: "8088",
42+
Port: "8088",
43+
MetricsType: "expvar",
4344
},
4445
API: APIConfig{
4546
Auth: ServiceAuthConfig{
@@ -255,8 +256,9 @@ type CORSConfig struct {
255256

256257
// AdminConfig holds the configuration for the admin web interface
257258
type AdminConfig struct {
258-
Auth ServiceAuthConfig `json:"-"`
259-
Port string `json:"port"`
259+
Auth ServiceAuthConfig `json:"-"`
260+
Port string `json:"port"`
261+
MetricsType string `json:"metricsType"`
260262
}
261263

262264
// WebhookConfig holds configuration for Optimizely Webhooks

config/config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func TestDefaultConfig(t *testing.T) {
5050
assert.Equal(t, "info", conf.Log.Level)
5151

5252
assert.Equal(t, "8088", conf.Admin.Port)
53+
assert.Equal(t, "expvar", conf.Admin.MetricsType)
5354
assert.Equal(t, make([]OAuthClientCredentials, 0), conf.Admin.Auth.Clients)
5455
assert.Equal(t, make([]string, 0), conf.Admin.Auth.HMACSecrets)
5556
assert.Equal(t, time.Duration(0), conf.Admin.Auth.TTL)

go.mod

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ require (
1212
github.com/golang-jwt/jwt/v4 v4.5.0
1313
github.com/google/uuid v1.3.0
1414
github.com/lestrrat-go/jwx v0.9.0
15-
github.com/optimizely/go-sdk v1.8.4-0.20230411182937-99d0bcfccf75
15+
github.com/optimizely/go-sdk v1.8.4-0.20230515121609-7ffed835c991
1616
github.com/orcaman/concurrent-map v1.0.0
17+
github.com/prometheus/client_golang v1.11.0
1718
github.com/rakyll/statik v0.1.7
1819
github.com/rs/zerolog v1.29.0
1920
github.com/spf13/viper v1.15.0
@@ -23,6 +24,16 @@ require (
2324
gopkg.in/yaml.v2 v2.4.0
2425
)
2526

27+
require (
28+
github.com/beorn7/perks v1.0.1 // indirect
29+
github.com/golang/protobuf v1.5.2 // indirect
30+
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
31+
github.com/prometheus/client_model v0.2.0 // indirect
32+
github.com/prometheus/common v0.30.0 // indirect
33+
github.com/prometheus/procfs v0.7.3 // indirect
34+
google.golang.org/protobuf v1.28.1 // indirect
35+
)
36+
2637
require (
2738
github.com/VividCortex/gohistogram v1.0.0 // indirect
2839
github.com/ajg/form v1.5.1 // indirect

go.sum

Lines changed: 91 additions & 3 deletions
Large diffs are not rendered by default.

pkg/handlers/admin_entities.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019-2020, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020,2023 Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -18,14 +18,13 @@
1818
package handlers
1919

2020
import (
21-
"expvar"
2221
"net/http"
2322
"os"
2423
"time"
2524

2625
"github.com/go-chi/render"
27-
2826
"github.com/optimizely/agent/config"
27+
"github.com/optimizely/agent/pkg/metrics"
2928
)
3029

3130
var startTime = time.Now()
@@ -87,5 +86,5 @@ func (a Admin) AppInfoHeader(next http.Handler) http.Handler {
8786

8887
// Metrics returns expvar info
8988
func (a Admin) Metrics(w http.ResponseWriter, r *http.Request) {
90-
expvar.Handler().ServeHTTP(w, r)
89+
metrics.GetHandler(a.Config.Admin.MetricsType).ServeHTTP(w, r)
9190
}

pkg/metrics/metrics.go

Lines changed: 117 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818
package metrics
1919

2020
import (
21+
"expvar"
22+
"net/http"
23+
"regexp"
24+
"strings"
2125
"sync"
2226

2327
go_kit_metrics "github.com/go-kit/kit/metrics"
24-
2528
go_kit_expvar "github.com/go-kit/kit/metrics/expvar"
29+
go_kit_prometheus "github.com/go-kit/kit/metrics/prometheus"
30+
stdprometheus "github.com/prometheus/client_golang/prometheus"
31+
"github.com/prometheus/client_golang/prometheus/promhttp"
2632
"github.com/rs/zerolog/log"
2733
)
2834

@@ -33,37 +39,49 @@ const (
3339
TimerPrefix = "timer"
3440
)
3541

42+
const (
43+
prometheusPackage = "prometheus"
44+
)
45+
46+
// GetHandler returns request handler for provided metrics package type
47+
func GetHandler(packageType string) http.Handler {
48+
switch packageType {
49+
case prometheusPackage:
50+
return promhttp.Handler()
51+
default:
52+
// expvar
53+
return expvar.Handler()
54+
}
55+
}
56+
57+
// Timer is the collection of some timers
58+
type Timer struct {
59+
hits go_kit_metrics.Counter
60+
totalTime go_kit_metrics.Counter
61+
histogram go_kit_metrics.Histogram
62+
}
63+
64+
// Update timer components
65+
func (t *Timer) Update(delta float64) {
66+
t.hits.Add(1)
67+
t.totalTime.Add(delta)
68+
t.histogram.Observe(delta)
69+
}
70+
3671
// Registry initializes expvar metrics registry
3772
type Registry struct {
3873
metricsCounterVars map[string]go_kit_metrics.Counter
3974
metricsGaugeVars map[string]go_kit_metrics.Gauge
4075
metricsHistogramVars map[string]go_kit_metrics.Histogram
4176
metricsTimerVars map[string]*Timer
77+
metricsType string
4278

4379
gaugeLock sync.RWMutex
4480
counterLock sync.RWMutex
4581
histogramLock sync.RWMutex
4682
timerLock sync.RWMutex
4783
}
4884

49-
// NewRegistry initializes metrics registry
50-
func NewRegistry() *Registry {
51-
52-
return &Registry{
53-
metricsCounterVars: map[string]go_kit_metrics.Counter{},
54-
metricsGaugeVars: map[string]go_kit_metrics.Gauge{},
55-
metricsHistogramVars: map[string]go_kit_metrics.Histogram{},
56-
metricsTimerVars: map[string]*Timer{},
57-
}
58-
}
59-
60-
// Timer is the collection of some timers
61-
type Timer struct {
62-
hits go_kit_metrics.Counter
63-
totalTime go_kit_metrics.Counter
64-
histogram go_kit_metrics.Histogram
65-
}
66-
6785
// NewTimer constructs Timer
6886
func (m *Registry) NewTimer(key string) *Timer {
6987
if key == "" {
@@ -81,16 +99,22 @@ func (m *Registry) NewTimer(key string) *Timer {
8199
return m.createTimer(combinedKey)
82100
}
83101

84-
// Update timer components
85-
func (t *Timer) Update(delta float64) {
86-
t.hits.Add(1)
87-
t.totalTime.Add(delta)
88-
t.histogram.Observe(delta)
102+
// GetCounter gets go-kit expvar Counter
103+
// NewRegistry initializes metrics registry
104+
func NewRegistry(metricsType string) *Registry {
105+
106+
registry := &Registry{
107+
metricsCounterVars: map[string]go_kit_metrics.Counter{},
108+
metricsGaugeVars: map[string]go_kit_metrics.Gauge{},
109+
metricsHistogramVars: map[string]go_kit_metrics.Histogram{},
110+
metricsTimerVars: map[string]*Timer{},
111+
metricsType: metricsType,
112+
}
113+
return registry
89114
}
90115

91-
// GetCounter gets go-kit expvar Counter
116+
// GetCounter gets go-kit Counter
92117
func (m *Registry) GetCounter(key string) go_kit_metrics.Counter {
93-
94118
if key == "" {
95119
log.Warn().Msg("metrics counter key is empty")
96120
return nil
@@ -103,13 +127,11 @@ func (m *Registry) GetCounter(key string) go_kit_metrics.Counter {
103127
if val, ok := m.metricsCounterVars[combinedKey]; ok {
104128
return val
105129
}
106-
107130
return m.createCounter(combinedKey)
108131
}
109132

110-
// GetGauge gets go-kit expvar Gauge
133+
// GetGauge gets go-kit Gauge
111134
func (m *Registry) GetGauge(key string) go_kit_metrics.Gauge {
112-
113135
if key == "" {
114136
log.Warn().Msg("metrics gauge key is empty")
115137
return nil
@@ -127,9 +149,8 @@ func (m *Registry) GetGauge(key string) go_kit_metrics.Gauge {
127149

128150
// GetHistogram gets go-kit Histogram
129151
func (m *Registry) GetHistogram(key string) go_kit_metrics.Histogram {
130-
131152
if key == "" {
132-
log.Warn().Msg("metrics gauge key is empty")
153+
log.Warn().Msg("metrics histogram key is empty")
133154
return nil
134155
}
135156

@@ -141,25 +162,52 @@ func (m *Registry) GetHistogram(key string) go_kit_metrics.Histogram {
141162
return m.createHistogram(key)
142163
}
143164

144-
func (m *Registry) createGauge(key string) *go_kit_expvar.Gauge {
145-
gaugeVar := go_kit_expvar.NewGauge(key)
165+
func (m *Registry) createGauge(key string) (gaugeVar go_kit_metrics.Gauge) {
166+
// This is required since naming convention for every package differs
167+
name := m.getPackageSupportedName(key)
168+
switch m.metricsType {
169+
case prometheusPackage:
170+
gaugeVar = go_kit_prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{
171+
Name: name,
172+
}, []string{})
173+
default:
174+
// Default expvar
175+
gaugeVar = go_kit_expvar.NewGauge(name)
176+
}
146177
m.metricsGaugeVars[key] = gaugeVar
147-
return gaugeVar
148-
178+
return
149179
}
150180

151-
func (m *Registry) createCounter(key string) *go_kit_expvar.Counter {
152-
counterVar := go_kit_expvar.NewCounter(key)
181+
func (m *Registry) createCounter(key string) (counterVar go_kit_metrics.Counter) {
182+
// This is required since naming convention for every package differs
183+
name := m.getPackageSupportedName(key)
184+
switch m.metricsType {
185+
case prometheusPackage:
186+
counterVar = go_kit_prometheus.NewCounterFrom(stdprometheus.CounterOpts{
187+
Name: name,
188+
}, []string{})
189+
default:
190+
// Default expvar
191+
counterVar = go_kit_expvar.NewCounter(name)
192+
}
153193
m.metricsCounterVars[key] = counterVar
154-
return counterVar
155-
194+
return
156195
}
157196

158-
func (m *Registry) createHistogram(key string) *go_kit_expvar.Histogram {
159-
histogramVar := go_kit_expvar.NewHistogram(key, 50)
197+
func (m *Registry) createHistogram(key string) (histogramVar go_kit_metrics.Histogram) {
198+
// This is required since naming convention for every package differs
199+
name := m.getPackageSupportedName(key)
200+
switch m.metricsType {
201+
case prometheusPackage:
202+
histogramVar = go_kit_prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
203+
Name: name,
204+
}, []string{})
205+
default:
206+
// Default expvar
207+
histogramVar = go_kit_expvar.NewHistogram(name, 50)
208+
}
160209
m.metricsHistogramVars[key] = histogramVar
161-
return histogramVar
162-
210+
return
163211
}
164212

165213
func (m *Registry) createTimer(key string) *Timer {
@@ -168,8 +216,33 @@ func (m *Registry) createTimer(key string) *Timer {
168216
totalTime: m.createCounter(key + ".responseTime"),
169217
histogram: m.createHistogram(key + ".responseTimeHist"),
170218
}
171-
172219
m.metricsTimerVars[key] = timerVar
173220
return timerVar
221+
}
174222

223+
// getPackageSupportedName converts name to package supported type
224+
func (m *Registry) getPackageSupportedName(name string) string {
225+
switch m.metricsType {
226+
case prometheusPackage:
227+
// https://prometheus.io/docs/practices/naming/
228+
return toSnakeCase(name)
229+
default:
230+
// Default expvar
231+
return name
232+
}
233+
}
234+
235+
func toSnakeCase(name string) string {
236+
v := strings.Replace(name, "-", "_", -1)
237+
strArray := strings.Split(v, ".")
238+
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
239+
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
240+
convertedArray := []string{}
241+
242+
for _, v := range strArray {
243+
snake := matchFirstCap.ReplaceAllString(v, "${1}_${2}")
244+
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
245+
convertedArray = append(convertedArray, strings.ToLower(snake))
246+
}
247+
return strings.Join(convertedArray, "_")
175248
}

0 commit comments

Comments
 (0)