Skip to content

Commit b88c729

Browse files
authored
Add framework metrics (#255)
1 parent 2bd1ad8 commit b88c729

File tree

21 files changed

+323
-43
lines changed

21 files changed

+323
-43
lines changed

pkg/gofr/container/container.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ func NewContainer(conf config.Config) *Container {
3838

3939
c.Debug("Container is being created")
4040

41-
c.Redis = redis.NewClient(conf, c.Logger)
41+
c.metricsManager = metrics.NewMetricsManager(exporters.Prometheus(c.appName, c.appVersion), c.Logger)
4242

43-
c.DB = sql.NewSQL(conf, c.Logger)
43+
// Register framework metrics
44+
c.registerFrameworkMetrics()
4445

45-
c.metricsManager = metrics.NewMetricManager(exporters.Prometheus(c.appName, c.appVersion), c.Logger)
46+
c.Redis = redis.NewClient(conf, c.Logger, c.metricsManager)
47+
48+
c.DB = sql.NewSQL(conf, c.Logger, c.metricsManager)
4649

4750
return c
4851
}
@@ -57,6 +60,29 @@ func (c *Container) Metrics() metrics.Manager {
5760
return c.metricsManager
5861
}
5962

63+
func (c *Container) registerFrameworkMetrics() {
64+
// system info metrics
65+
c.Metrics().NewGauge("app_go_routines", "Number of Go routines running.")
66+
c.Metrics().NewGauge("app_sys_memory_alloc", "Number of bytes allocated for heap objects.")
67+
c.Metrics().NewGauge("app_sys_total_alloc", "Number of cumulative bytes allocated for heap objects.")
68+
c.Metrics().NewGauge("app_go_numGC", "Number of completed Garbage Collector cycles.")
69+
c.Metrics().NewGauge("app_go_sys", "Number of total bytes of memory.")
70+
71+
histogramBuckets := []float64{.001, .003, .005, .01, .02, .03, .05, .1, .2, .3, .5, .75, 1, 2, 3, 5, 10, 30}
72+
73+
// http metrics
74+
c.Metrics().NewHistogram("app_http_response", "Response time of http requests in seconds.", histogramBuckets...)
75+
c.Metrics().NewHistogram("app_http_service_response", "Response time of http service requests in seconds.", histogramBuckets...)
76+
77+
// redis metrics
78+
c.Metrics().NewHistogram("app_redis_stats", "Observes the response time for Redis commands.", histogramBuckets...)
79+
80+
// sql metrics
81+
c.Metrics().NewHistogram("app_sql_stats", "Observes the response time for SQL queries.", histogramBuckets...)
82+
c.Metrics().NewGauge("app_sql_open_connections", "Number of open SQL connections.")
83+
c.Metrics().NewGauge("app_sql_inUse_connections", "Number of inUse SQL connections.")
84+
}
85+
6086
func (c *Container) GetAppName() string {
6187
return c.appName
6288
}

pkg/gofr/datasource/redis/hook.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import (
1313

1414
// redisHook is a custom Redis hook for logging queries and their durations.
1515
type redisHook struct {
16-
logger datasource.Logger
16+
logger datasource.Logger
17+
metrics Metrics
1718
}
1819

1920
// QueryLog represents a logged Redis query.
@@ -43,11 +44,16 @@ func (ql QueryLog) String() string {
4344

4445
// logQuery logs the Redis query information.
4546
func (r *redisHook) logQuery(start time.Time, query string, args ...interface{}) {
47+
duration := time.Since(start)
48+
4649
r.logger.Debug(QueryLog{
4750
Query: query,
48-
Duration: time.Since(start).Microseconds(),
51+
Duration: duration.Microseconds(),
4952
Args: args,
5053
})
54+
55+
r.metrics.RecordHistogram(context.Background(), "app_redis_stats",
56+
duration.Seconds(), "type", query)
5157
}
5258

5359
// DialHook implements the redis.DialHook interface.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package redis
2+
3+
import "context"
4+
5+
type Metrics interface {
6+
IncrementCounter(ctx context.Context, name string, labels ...string)
7+
DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string)
8+
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
9+
SetGauge(name string, value float64)
10+
}

pkg/gofr/datasource/redis/metrics_interface.go

Lines changed: 98 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/gofr/datasource/redis/redis.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type Redis struct {
3232

3333
// NewClient return a redis client if connection is successful based on Config.
3434
// In case of error, it returns an error as second parameter.
35-
func NewClient(c config.Config, logger datasource.Logger) *Redis {
35+
func NewClient(c config.Config, logger datasource.Logger, metrics Metrics) *Redis {
3636
var redisConfig = &Config{}
3737

3838
if redisConfig.HostName = c.Get("REDIS_HOST"); redisConfig.HostName == "" {
@@ -55,7 +55,7 @@ func NewClient(c config.Config, logger datasource.Logger) *Redis {
5555
redisConfig.Options = options
5656

5757
rc := redis.NewClient(redisConfig.Options)
58-
rc.AddHook(&redisHook{logger: logger})
58+
rc.AddHook(&redisHook{logger: logger, metrics: metrics})
5959

6060
ctx, cancel := context.WithTimeout(context.TODO(), redisPingTimeout)
6161
defer cancel()

pkg/gofr/datasource/redis/redis_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@ func TestRedis_QueryLogging(t *testing.T) {
2323

2424
defer s.Close()
2525

26+
mockMetric := NewMockMetrics(ctrl)
27+
mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "ping")
28+
mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "set")
29+
2630
result := testutil.StdoutOutputForFunc(func() {
2731
mockLogger := testutil.NewMockLogger(testutil.DEBUGLOG)
2832
client := NewClient(testutil.NewMockConfig(map[string]string{
2933
"REDIS_HOST": s.Host(),
3034
"REDIS_PORT": s.Port(),
31-
}), mockLogger)
35+
}), mockLogger, mockMetric)
3236
assert.Nil(t, err)
3337

3438
result, err := client.Set(context.TODO(), "key", "value", 1*time.Minute).Result()
@@ -51,13 +55,17 @@ func TestRedis_PipelineQueryLogging(t *testing.T) {
5155

5256
defer s.Close()
5357

58+
mockMetric := NewMockMetrics(ctrl)
59+
mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "ping")
60+
mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "pipeline")
61+
5462
// Execute Redis pipeline
5563
result := testutil.StdoutOutputForFunc(func() {
5664
mockLogger := testutil.NewMockLogger(testutil.DEBUGLOG)
5765
client := NewClient(testutil.NewMockConfig(map[string]string{
5866
"REDIS_HOST": s.Host(),
5967
"REDIS_PORT": s.Port(),
60-
}), mockLogger)
68+
}), mockLogger, mockMetric)
6169
assert.Nil(t, err)
6270

6371
// Pipeline execution

pkg/gofr/datasource/sql/db.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import (
1515
type DB struct {
1616
// contains unexported or private fields
1717
*sql.DB
18-
logger datasource.Logger
19-
config *DBConfig
18+
logger datasource.Logger
19+
config *DBConfig
20+
metrics Metrics
2021
}
2122

2223
type Log struct {
@@ -27,12 +28,24 @@ type Log struct {
2728
}
2829

2930
func (d *DB) logQuery(start time.Time, queryType, query string, args ...interface{}) {
31+
duration := time.Since(start)
32+
3033
d.logger.Debug(Log{
3134
Type: queryType,
3235
Query: query,
33-
Duration: time.Since(start).Microseconds(),
36+
Duration: duration.Microseconds(),
3437
Args: args,
3538
})
39+
40+
d.metrics.RecordHistogram(context.Background(), "app_sql_stats",
41+
duration.Seconds(), "type", getOperationType(query))
42+
}
43+
44+
func getOperationType(query string) string {
45+
query = strings.TrimSpace(query)
46+
words := strings.Split(query, " ")
47+
48+
return words[0]
3649
}
3750

3851
func (d *DB) Query(query string, args ...interface{}) (*sql.Rows, error) {
@@ -66,21 +79,26 @@ func (d *DB) Begin() (*Tx, error) {
6679
return nil, err
6780
}
6881

69-
return &Tx{Tx: tx, logger: d.logger}, nil
82+
return &Tx{Tx: tx, logger: d.logger, metrics: d.metrics}, nil
7083
}
7184

7285
type Tx struct {
7386
*sql.Tx
74-
logger datasource.Logger
87+
logger datasource.Logger
88+
metrics Metrics
7589
}
7690

7791
func (t *Tx) logQuery(start time.Time, queryType, query string, args ...interface{}) {
92+
duration := time.Since(start)
93+
7894
t.logger.Debug(Log{
7995
Type: queryType,
8096
Query: query,
81-
Duration: time.Since(start).Microseconds(),
97+
Duration: duration.Microseconds(),
8298
Args: args,
8399
})
100+
101+
t.metrics.RecordHistogram(context.Background(), "app_sql_stats", duration.Seconds())
84102
}
85103

86104
func (t *Tx) Query(query string, args ...interface{}) (*sql.Rows, error) {

pkg/gofr/datasource/sql/db_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func getDB(t *testing.T, logLevel int) (*DB, sqlmock.Sqlmock) {
2222
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
2323
}
2424

25-
return &DB{mockDB, testutil.NewMockLogger(logLevel), nil}, mock
25+
return &DB{mockDB, testutil.NewMockLogger(logLevel), nil, nil}, mock
2626
}
2727

2828
func TestDB_SelectSingleColumnFromIntToString(t *testing.T) {

pkg/gofr/datasource/sql/metrics.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package sql
2+
3+
import "context"
4+
5+
type Metrics interface {
6+
IncrementCounter(ctx context.Context, name string, labels ...string)
7+
DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string)
8+
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
9+
SetGauge(name string, value float64)
10+
}

pkg/gofr/datasource/sql/sql.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"database/sql"
55
"fmt"
66
"strconv"
7+
"time"
78

89
"gofr.dev/pkg/gofr/config"
910
"gofr.dev/pkg/gofr/datasource"
@@ -20,7 +21,7 @@ type DBConfig struct {
2021
Database string
2122
}
2223

23-
func NewSQL(configs config.Config, logger datasource.Logger) *DB {
24+
func NewSQL(configs config.Config, logger datasource.Logger, metrics Metrics) *DB {
2425
dbConfig := getDBConfig(configs)
2526

2627
// if Hostname is not provided, we won't try to connect to DB
@@ -41,19 +42,21 @@ func NewSQL(configs config.Config, logger datasource.Logger) *DB {
4142
logger.Errorf("could not connect with '%s' user to database '%s:%s' error: %v",
4243
dbConfig.User, dbConfig.HostName, dbConfig.Port, err)
4344

44-
return &DB{config: dbConfig}
45+
return &DB{config: dbConfig, metrics: metrics}
4546
}
4647

4748
if err := db.Ping(); err != nil {
4849
logger.Errorf("could not connect with '%s' user to database '%s:%s' error: %v",
4950
dbConfig.User, dbConfig.HostName, dbConfig.Port, err)
5051

51-
return &DB{config: dbConfig}
52+
return &DB{config: dbConfig, metrics: metrics}
5253
}
5354

5455
logger.Logf("connected to '%s' database at %s:%s", dbConfig.Database, dbConfig.HostName, dbConfig.Port)
5556

56-
return &DB{DB: db, config: dbConfig, logger: logger}
57+
go pushDBMetrics(db, metrics)
58+
59+
return &DB{DB: db, config: dbConfig, logger: logger, metrics: metrics}
5760
}
5861

5962
func getDBConfig(configs config.Config) *DBConfig {
@@ -65,3 +68,16 @@ func getDBConfig(configs config.Config) *DBConfig {
6568
Database: configs.Get("DB_NAME"),
6669
}
6770
}
71+
72+
func pushDBMetrics(db *sql.DB, metrics Metrics) {
73+
const frequency = 10
74+
75+
for {
76+
stats := db.Stats()
77+
78+
metrics.SetGauge("app_sql_open_connections", float64(stats.OpenConnections))
79+
metrics.SetGauge("app_sql_inUse_connections", float64(stats.InUse))
80+
81+
time.Sleep(frequency * time.Second)
82+
}
83+
}

0 commit comments

Comments
 (0)