Skip to content

Commit cfe57ed

Browse files
authored
Implementing database metrics, configuring DB connection settings and integrating fullPath rate limiter (#43)
* Register and sync db metrics * fix mock * Add RateLimiterByFullPath * TestRateLimitByFullPath
1 parent c56dcbb commit cfe57ed

File tree

8 files changed

+155
-1
lines changed

8 files changed

+155
-1
lines changed

pkg/zdb/methods.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ func (z *zDatabase) Scopes(funcs ...func(ZDatabase) ZDatabase) ZDatabase {
132132
return wrap(z.db.Scopes(gormFuncs...))
133133
}
134134

135+
func (z *zDatabase) GetDBStats() (sql.DBStats, error) {
136+
sqlDB, err := z.db.DB()
137+
return sqlDB.Stats(), err
138+
}
139+
135140
func (z *zDatabase) Error() error {
136141
return z.db.Error
137142
}

pkg/zdb/metrics.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package zdb
2+
3+
import (
4+
"fmt"
5+
"github.com/zondax/golem/pkg/metrics"
6+
"github.com/zondax/golem/pkg/metrics/collectors"
7+
"go.uber.org/zap"
8+
"time"
9+
)
10+
11+
const (
12+
defaultInterval = time.Minute
13+
dbOpenConnectionsMetricName = "db_open_connections"
14+
dbIdleConnectionsMetricName = "db_idle_connections"
15+
dbMaxOpenConnectionsMetricName = "db_max_open_connections"
16+
dbWaitDurationMetricName = "db_wait_duration"
17+
dbInUseConnectionsMetricName = "db_in_use_connections"
18+
)
19+
20+
func SetupAndMonitorDBMetrics(appName string, metricsServer metrics.TaskMetrics, db ZDatabase, updateInterval time.Duration) []error {
21+
if updateInterval <= 0 {
22+
updateInterval = defaultInterval
23+
}
24+
25+
var errs []error
26+
register := func(name, help string, labels []string, handler metrics.MetricHandler) {
27+
if err := metricsServer.RegisterMetric(name, help, labels, handler); err != nil {
28+
errs = append(errs, err)
29+
}
30+
}
31+
32+
register(getMetricName(appName, dbOpenConnectionsMetricName), "Number of open database connections.", nil, &collectors.Gauge{})
33+
register(getMetricName(appName, dbIdleConnectionsMetricName), "Number of idle database connections in the pool.", nil, &collectors.Gauge{})
34+
register(getMetricName(appName, dbMaxOpenConnectionsMetricName), "Maximum number of open database connections.", nil, &collectors.Gauge{})
35+
register(getMetricName(appName, dbWaitDurationMetricName), "Total time waited for new database connections.", nil, &collectors.Histogram{})
36+
register(getMetricName(appName, dbInUseConnectionsMetricName), "Number of database connections currently in use.", nil, &collectors.Gauge{})
37+
38+
if len(errs) > 0 {
39+
return errs
40+
}
41+
42+
go func() {
43+
ticker := time.NewTicker(updateInterval)
44+
for range ticker.C {
45+
stats, err := db.GetDBStats()
46+
if err != nil {
47+
zap.S().Errorf("Error while getting db stats: %v", err)
48+
continue
49+
}
50+
51+
_ = metricsServer.UpdateMetric(getMetricName(appName, dbOpenConnectionsMetricName), float64(stats.OpenConnections))
52+
_ = metricsServer.UpdateMetric(getMetricName(appName, dbIdleConnectionsMetricName), float64(stats.Idle))
53+
_ = metricsServer.UpdateMetric(getMetricName(appName, dbMaxOpenConnectionsMetricName), float64(stats.MaxOpenConnections))
54+
_ = metricsServer.UpdateMetric(getMetricName(appName, dbWaitDurationMetricName), float64(stats.WaitDuration.Milliseconds()))
55+
_ = metricsServer.UpdateMetric(getMetricName(appName, dbInUseConnectionsMetricName), float64(stats.InUse))
56+
}
57+
}()
58+
59+
return nil
60+
}
61+
62+
func getMetricName(appName, metricType string) string {
63+
return fmt.Sprintf("zdatabase_%s_%s", appName, metricType)
64+
}

pkg/zdb/mock_zdb.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ func (m *MockZDatabase) Group(name string) ZDatabase {
142142
return args.Get(0).(ZDatabase)
143143
}
144144

145+
func (m *MockZDatabase) GetDBStats() (sql.DBStats, error) {
146+
args := m.Called()
147+
return args.Get(0).(sql.DBStats), args.Error(1)
148+
}
149+
145150
// MockDBConnector
146151

147152
type MockDBConnector struct {

pkg/zdb/zdb.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type ZDatabase interface {
4444
Scopes(funcs ...func(ZDatabase) ZDatabase) ZDatabase
4545
RowsAffected() int64
4646
GetDbConnection() *gorm.DB
47+
GetDBStats() (sql.DBStats, error)
4748
}
4849

4950
type zDatabase struct {

pkg/zdb/zdbconfig/zdbconfig.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package zdbconfig
22

3-
import "gorm.io/gorm"
3+
import (
4+
"gorm.io/gorm"
5+
"time"
6+
)
47

58
type ConnectionParams struct {
69
User string
@@ -17,6 +20,9 @@ type Config struct {
1720
MaxAttempts int
1821
ConnectionParams ConnectionParams
1922
LogConfig LogConfig
23+
MaxIdleConns int
24+
MaxOpenConns int
25+
ConnMaxLifetime time.Duration
2026
}
2127

2228
type LogConfig struct {

pkg/zdb/zdbconnector/clickhouse.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ func (c *ClickHouseConnector) Connect(config *zdbconfig.Config) (*gorm.DB, error
2424
if err != nil {
2525
return nil, err
2626
}
27+
db, err := dbConn.DB()
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
if config.MaxIdleConns != 0 {
33+
db.SetMaxIdleConns(config.MaxIdleConns)
34+
}
35+
36+
if config.MaxOpenConns != 0 {
37+
db.SetMaxOpenConns(config.MaxOpenConns)
38+
}
39+
40+
if config.ConnMaxLifetime != 0 {
41+
db.SetConnMaxLifetime(config.ConnMaxLifetime)
42+
}
43+
2744
return dbConn, nil
2845
}
2946

pkg/zrouter/zmiddlewares/rate_limit.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,44 @@ package zmiddlewares
33
import (
44
"golang.org/x/time/rate"
55
"net/http"
6+
"sync"
67
"time"
78
)
89

10+
var (
11+
limiters = make(map[string]*rate.Limiter)
12+
mu sync.Mutex
13+
)
14+
15+
func getLimiter(key string, maxRPM int) *rate.Limiter {
16+
mu.Lock()
17+
defer mu.Unlock()
18+
19+
limiter, exists := limiters[key]
20+
if !exists {
21+
limiter = rate.NewLimiter(rate.Every(time.Minute/time.Duration(maxRPM)), maxRPM)
22+
limiters[key] = limiter
23+
}
24+
25+
return limiter
26+
}
27+
28+
func RateLimitByFullPath(maxRPM int) Middleware {
29+
return func(next http.Handler) http.Handler {
30+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31+
key := r.URL.Path
32+
limiter := getLimiter(key, maxRPM)
33+
34+
if !limiter.Allow() {
35+
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
36+
return
37+
}
38+
39+
next.ServeHTTP(w, r)
40+
})
41+
}
42+
}
43+
944
func RateLimit(maxRPM int) Middleware {
1045
limiter := rate.NewLimiter(rate.Every(time.Minute/time.Duration(maxRPM)), maxRPM)
1146

pkg/zrouter/zmiddlewares/rate_limit_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,24 @@ func TestRateLimit(t *testing.T) {
2828
r.ServeHTTP(rec, req)
2929
assert.Equal(t, http.StatusTooManyRequests, rec.Code)
3030
}
31+
32+
func TestRateLimitByFullPath(t *testing.T) {
33+
r := chi.NewRouter()
34+
35+
r.Use(RateLimitByFullPath(1))
36+
37+
r.Get("/tests/{id}", func(w http.ResponseWriter, r *http.Request) {
38+
_, _ = w.Write([]byte("OK"))
39+
})
40+
41+
req := httptest.NewRequest("GET", "/tests/1", nil)
42+
43+
rec := httptest.NewRecorder()
44+
r.ServeHTTP(rec, req)
45+
assert.Equal(t, http.StatusOK, rec.Code)
46+
assert.Equal(t, "OK", rec.Body.String())
47+
48+
rec = httptest.NewRecorder()
49+
r.ServeHTTP(rec, req)
50+
assert.Equal(t, http.StatusTooManyRequests, rec.Code)
51+
}

0 commit comments

Comments
 (0)