Skip to content

[FSSDK-8707] feat(metrics): Adds support for prometheus #348

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 6, 2023
Merged
3 changes: 2 additions & 1 deletion cmd/optimizely/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ func main() {

setRuntimeEnvironment(conf.Runtime)

agentMetricsRegistry := metrics.NewRegistry()
// Set metrics type to be used
agentMetricsRegistry := metrics.NewRegistry(conf.Admin.MetricsType)
sdkMetricsRegistry := optimizely.NewRegistry(agentMetricsRegistry)

ctx, cancel := context.WithCancel(context.Background()) // Create default service context
Expand Down
3 changes: 3 additions & 0 deletions cmd/optimizely/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func assertLog(t *testing.T, actual config.LogConfig) {

func assertAdmin(t *testing.T, actual config.AdminConfig) {
assert.Equal(t, "3002", actual.Port)
assert.Equal(t, "prometheus", actual.MetricsType)
}

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

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

_ = os.Setenv("OPTIMIZELY_ADMIN_PORT", "3002")
_ = os.Setenv("OPTIMIZELY_ADMIN_METRICSTYPE", "prometheus")

_ = os.Setenv("OPTIMIZELY_API_MAXCONNS", "100")
_ = os.Setenv("OPTIMIZELY_API_PORT", "3000")
Expand Down
1 change: 1 addition & 0 deletions cmd/optimizely/testdata/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ client:

admin:
port: "3002"
metricsType: "prometheus"
auth:
ttl: 30m
hmacSecrets:
Expand Down
4 changes: 4 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ api:
admin:
## http listener port
port: "8088"
## metrics package to use
## supported packages are expvar and prometheus
## default is expvar
metricsType: ""
##
## webhook service receives update notifications to your Optimizely project. Receipt of the webhook will
## trigger an immediate download of the datafile from the CDN
Expand Down
8 changes: 5 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ func NewDefaultConfig() *AgentConfig {
JwksURL: "",
JwksUpdateInterval: 0,
},
Port: "8088",
Port: "8088",
MetricsType: "expvar",
},
API: APIConfig{
Auth: ServiceAuthConfig{
Expand Down Expand Up @@ -255,8 +256,9 @@ type CORSConfig struct {

// AdminConfig holds the configuration for the admin web interface
type AdminConfig struct {
Auth ServiceAuthConfig `json:"-"`
Port string `json:"port"`
Auth ServiceAuthConfig `json:"-"`
Port string `json:"port"`
MetricsType string `json:"metricsType"`
}

// WebhookConfig holds configuration for Optimizely Webhooks
Expand Down
1 change: 1 addition & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func TestDefaultConfig(t *testing.T) {
assert.Equal(t, "info", conf.Log.Level)

assert.Equal(t, "8088", conf.Admin.Port)
assert.Equal(t, "expvar", conf.Admin.MetricsType)
assert.Equal(t, make([]OAuthClientCredentials, 0), conf.Admin.Auth.Clients)
assert.Equal(t, make([]string, 0), conf.Admin.Auth.HMACSecrets)
assert.Equal(t, time.Duration(0), conf.Admin.Auth.TTL)
Expand Down
13 changes: 12 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.0
github.com/lestrrat-go/jwx v0.9.0
github.com/optimizely/go-sdk v1.8.4-0.20230411182937-99d0bcfccf75
github.com/optimizely/go-sdk v1.8.4-0.20230515121609-7ffed835c991
github.com/orcaman/concurrent-map v1.0.0
github.com/prometheus/client_golang v1.11.0
github.com/rakyll/statik v0.1.7
github.com/rs/zerolog v1.29.0
github.com/spf13/viper v1.15.0
Expand All @@ -23,6 +24,16 @@ require (
gopkg.in/yaml.v2 v2.4.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)

require (
github.com/VividCortex/gohistogram v1.0.0 // indirect
github.com/ajg/form v1.5.1 // indirect
Expand Down
94 changes: 91 additions & 3 deletions go.sum

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions pkg/handlers/admin_entities.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2019-2020, Optimizely, Inc. and contributors *
* Copyright 2019-2020,2023 Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand All @@ -18,14 +18,13 @@
package handlers

import (
"expvar"
"net/http"
"os"
"time"

"github.com/go-chi/render"

"github.com/optimizely/agent/config"
"github.com/optimizely/agent/pkg/metrics"
)

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

// Metrics returns expvar info
func (a Admin) Metrics(w http.ResponseWriter, r *http.Request) {
expvar.Handler().ServeHTTP(w, r)
metrics.GetHandler(a.Config.Admin.MetricsType).ServeHTTP(w, r)
}
161 changes: 117 additions & 44 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@
package metrics

import (
"expvar"
"net/http"
"regexp"
"strings"
"sync"

go_kit_metrics "github.com/go-kit/kit/metrics"

go_kit_expvar "github.com/go-kit/kit/metrics/expvar"
go_kit_prometheus "github.com/go-kit/kit/metrics/prometheus"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog/log"
)

Expand All @@ -33,37 +39,49 @@ const (
TimerPrefix = "timer"
)

const (
prometheusPackage = "prometheus"
)

// GetHandler returns request handler for provided metrics package type
func GetHandler(packageType string) http.Handler {
switch packageType {
case prometheusPackage:
return promhttp.Handler()
default:
// expvar
return expvar.Handler()
}
}

// Timer is the collection of some timers
type Timer struct {
hits go_kit_metrics.Counter
totalTime go_kit_metrics.Counter
histogram go_kit_metrics.Histogram
}

// Update timer components
func (t *Timer) Update(delta float64) {
t.hits.Add(1)
t.totalTime.Add(delta)
t.histogram.Observe(delta)
}

// Registry initializes expvar metrics registry
type Registry struct {
metricsCounterVars map[string]go_kit_metrics.Counter
metricsGaugeVars map[string]go_kit_metrics.Gauge
metricsHistogramVars map[string]go_kit_metrics.Histogram
metricsTimerVars map[string]*Timer
metricsType string

gaugeLock sync.RWMutex
counterLock sync.RWMutex
histogramLock sync.RWMutex
timerLock sync.RWMutex
}

// NewRegistry initializes metrics registry
func NewRegistry() *Registry {

return &Registry{
metricsCounterVars: map[string]go_kit_metrics.Counter{},
metricsGaugeVars: map[string]go_kit_metrics.Gauge{},
metricsHistogramVars: map[string]go_kit_metrics.Histogram{},
metricsTimerVars: map[string]*Timer{},
}
}

// Timer is the collection of some timers
type Timer struct {
hits go_kit_metrics.Counter
totalTime go_kit_metrics.Counter
histogram go_kit_metrics.Histogram
}

// NewTimer constructs Timer
func (m *Registry) NewTimer(key string) *Timer {
if key == "" {
Expand All @@ -81,16 +99,22 @@ func (m *Registry) NewTimer(key string) *Timer {
return m.createTimer(combinedKey)
}

// Update timer components
func (t *Timer) Update(delta float64) {
t.hits.Add(1)
t.totalTime.Add(delta)
t.histogram.Observe(delta)
// GetCounter gets go-kit expvar Counter
// NewRegistry initializes metrics registry
func NewRegistry(metricsType string) *Registry {

registry := &Registry{
metricsCounterVars: map[string]go_kit_metrics.Counter{},
metricsGaugeVars: map[string]go_kit_metrics.Gauge{},
metricsHistogramVars: map[string]go_kit_metrics.Histogram{},
metricsTimerVars: map[string]*Timer{},
metricsType: metricsType,
}
return registry
}

// GetCounter gets go-kit expvar Counter
// GetCounter gets go-kit Counter
func (m *Registry) GetCounter(key string) go_kit_metrics.Counter {

if key == "" {
log.Warn().Msg("metrics counter key is empty")
return nil
Expand All @@ -103,13 +127,11 @@ func (m *Registry) GetCounter(key string) go_kit_metrics.Counter {
if val, ok := m.metricsCounterVars[combinedKey]; ok {
return val
}

return m.createCounter(combinedKey)
}

// GetGauge gets go-kit expvar Gauge
// GetGauge gets go-kit Gauge
func (m *Registry) GetGauge(key string) go_kit_metrics.Gauge {

if key == "" {
log.Warn().Msg("metrics gauge key is empty")
return nil
Expand All @@ -127,9 +149,8 @@ func (m *Registry) GetGauge(key string) go_kit_metrics.Gauge {

// GetHistogram gets go-kit Histogram
func (m *Registry) GetHistogram(key string) go_kit_metrics.Histogram {

if key == "" {
log.Warn().Msg("metrics gauge key is empty")
log.Warn().Msg("metrics histogram key is empty")
return nil
}

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

func (m *Registry) createGauge(key string) *go_kit_expvar.Gauge {
gaugeVar := go_kit_expvar.NewGauge(key)
func (m *Registry) createGauge(key string) (gaugeVar go_kit_metrics.Gauge) {
// This is required since naming convention for every package differs
name := m.getPackageSupportedName(key)
switch m.metricsType {
case prometheusPackage:
gaugeVar = go_kit_prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{
Name: name,
}, []string{})
default:
// Default expvar
gaugeVar = go_kit_expvar.NewGauge(name)
}
m.metricsGaugeVars[key] = gaugeVar
return gaugeVar

return
}

func (m *Registry) createCounter(key string) *go_kit_expvar.Counter {
counterVar := go_kit_expvar.NewCounter(key)
func (m *Registry) createCounter(key string) (counterVar go_kit_metrics.Counter) {
// This is required since naming convention for every package differs
name := m.getPackageSupportedName(key)
switch m.metricsType {
case prometheusPackage:
counterVar = go_kit_prometheus.NewCounterFrom(stdprometheus.CounterOpts{
Name: name,
}, []string{})
default:
// Default expvar
counterVar = go_kit_expvar.NewCounter(name)
}
m.metricsCounterVars[key] = counterVar
return counterVar

return
}

func (m *Registry) createHistogram(key string) *go_kit_expvar.Histogram {
histogramVar := go_kit_expvar.NewHistogram(key, 50)
func (m *Registry) createHistogram(key string) (histogramVar go_kit_metrics.Histogram) {
// This is required since naming convention for every package differs
name := m.getPackageSupportedName(key)
switch m.metricsType {
case prometheusPackage:
histogramVar = go_kit_prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
Name: name,
}, []string{})
default:
// Default expvar
histogramVar = go_kit_expvar.NewHistogram(name, 50)
}
m.metricsHistogramVars[key] = histogramVar
return histogramVar

return
}

func (m *Registry) createTimer(key string) *Timer {
Expand All @@ -168,8 +216,33 @@ func (m *Registry) createTimer(key string) *Timer {
totalTime: m.createCounter(key + ".responseTime"),
histogram: m.createHistogram(key + ".responseTimeHist"),
}

m.metricsTimerVars[key] = timerVar
return timerVar
}

// getPackageSupportedName converts name to package supported type
func (m *Registry) getPackageSupportedName(name string) string {
switch m.metricsType {
case prometheusPackage:
// https://prometheus.io/docs/practices/naming/
return toSnakeCase(name)
default:
// Default expvar
return name
}
}

func toSnakeCase(name string) string {
v := strings.Replace(name, "-", "_", -1)
strArray := strings.Split(v, ".")
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
convertedArray := []string{}

for _, v := range strArray {
snake := matchFirstCap.ReplaceAllString(v, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
convertedArray = append(convertedArray, strings.ToLower(snake))
}
return strings.Join(convertedArray, "_")
}
Loading