Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions pkg/sqlutil/beholder_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package sqlutil

import (
"context"
"sync"

"go.opentelemetry.io/otel/metric"

"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
)

// sqlQueryTimeMetric records SQL query time as a percentage of timeout, duplicating [PromSQLQueryTime]
// as an OTel histogram via the Beholder meter.
type sqlQueryTimeMetric interface {
Record(ctx context.Context, pct float64)
}

type beholderSQLQueryTimeMetric struct {
histogram metric.Float64Histogram
}

func newSQLQueryTimeMetric(lggr logger.Logger) sqlQueryTimeMetric {
histogram, err := beholder.GetMeter().Float64Histogram(
"sql_query_timeout_percent",
metric.WithDescription("SQL query time as a percentage of timeout."),
metric.WithUnit("1"),
metric.WithExplicitBucketBoundaries(sqlQueryTimeBuckets...),
)
Comment thread
Copilot marked this conversation as resolved.
if err != nil {
lggr.Errorw("Failed to create sql_query_timeout_percent beholder histogram; disabling beholder SQL query time metric", "err", err)
return noopSQLQueryTimeMetric{}
}
return &beholderSQLQueryTimeMetric{histogram: histogram}
}

func (m *beholderSQLQueryTimeMetric) Record(ctx context.Context, pct float64) {
m.histogram.Record(ctx, pct)
}

type noopSQLQueryTimeMetric struct{}

func (noopSQLQueryTimeMetric) Record(context.Context, float64) {}

var (
sqlQueryTimeMetricOnce sync.Once
globalSQLQueryTimeMetric sqlQueryTimeMetric
Comment on lines +46 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we strictly need a global instance? or could we inject something to use or construct from instead? We don't typically need to suppress errors for metric creation.

)

func getSQLQueryTimeMetric(lggr logger.Logger) sqlQueryTimeMetric {
sqlQueryTimeMetricOnce.Do(func() {
globalSQLQueryTimeMetric = newSQLQueryTimeMetric(lggr)
})
return globalSQLQueryTimeMetric
}
5 changes: 4 additions & 1 deletion pkg/sqlutil/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import (

const slowMsg = "SLOW SQL QUERY"

var sqlQueryTimeBuckets = []float64{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120}

// PromSQLQueryTime is exported temporarily while transitioning the core ORMs.
var PromSQLQueryTime = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "sql_query_timeout_percent",
Help: "SQL query time as a percentage of timeout.",
Buckets: []float64{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120},
Buckets: sqlQueryTimeBuckets,
})

// MonitorHook returns a [QueryHook] that measures the timing of each query and logs about slow queries at increasing levels of severity.
Expand Down Expand Up @@ -172,6 +174,7 @@ func (q *queryLogger) logTiming(ctx context.Context, start time.Time) {
}

PromSQLQueryTime.Observe(pct)
getSQLQueryTimeMetric(q.lggr).Record(ctx, pct)
}

// LogThresholds holds funcs for computing thresholds for timeout usage.
Expand Down
77 changes: 77 additions & 0 deletions pkg/sqlutil/pg/beholder_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package pg

import (
"context"
"database/sql"

"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/noop"

"github.com/smartcontractkit/chainlink-common/pkg/beholder"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this package should import beholder. We already provide a means of setting a custom hook via StatsCustomReporterFn. Why don't we use that?

"github.com/smartcontractkit/chainlink-common/pkg/logger"
)

type dbStatsBeholderMetrics struct {
connsMax metric.Int64Gauge
connsOpen metric.Int64Gauge
connsInUse metric.Int64Gauge
waitCount metric.Int64Gauge
waitDuration metric.Float64Gauge
}

func newDBStatsBeholderMetrics(lggr logger.Logger) *dbStatsBeholderMetrics {
meter := beholder.GetMeter()
m := &dbStatsBeholderMetrics{
connsMax: noop.Int64Gauge{},
connsOpen: noop.Int64Gauge{},
connsInUse: noop.Int64Gauge{},
waitCount: noop.Int64Gauge{},
waitDuration: noop.Float64Gauge{},
}

if g, err := meter.Int64Gauge("db_conns_max",
metric.WithDescription("Maximum number of open connections to the database.")); err != nil {
lggr.Errorw("Failed to create db_conns_max beholder gauge", "err", err)
} else {
m.connsMax = g
}

if g, err := meter.Int64Gauge("db_conns_open",
metric.WithDescription("The number of established connections both in use and idle.")); err != nil {
lggr.Errorw("Failed to create db_conns_open beholder gauge", "err", err)
} else {
m.connsOpen = g
}

if g, err := meter.Int64Gauge("db_conns_used",
metric.WithDescription("The number of connections currently in use.")); err != nil {
lggr.Errorw("Failed to create db_conns_used beholder gauge", "err", err)
} else {
m.connsInUse = g
}

if g, err := meter.Int64Gauge("db_wait_count",
metric.WithDescription("The total number of connections waited for.")); err != nil {
lggr.Errorw("Failed to create db_wait_count beholder gauge", "err", err)
} else {
m.waitCount = g
}

if g, err := meter.Float64Gauge("db_wait_time_seconds",
metric.WithDescription("The total time blocked waiting for a new connection."),
metric.WithUnit("s")); err != nil {
lggr.Errorw("Failed to create db_wait_time_seconds beholder gauge", "err", err)
} else {
m.waitDuration = g
}

return m
}

func (m *dbStatsBeholderMetrics) record(ctx context.Context, stats sql.DBStats) {
m.connsMax.Record(ctx, int64(stats.MaxOpenConnections))
m.connsOpen.Record(ctx, int64(stats.OpenConnections))
m.connsInUse.Record(ctx, int64(stats.InUse))
m.waitCount.Record(ctx, stats.WaitCount)
m.waitDuration.Record(ctx, stats.WaitDuration.Seconds())
}
35 changes: 22 additions & 13 deletions pkg/sqlutil/pg/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,24 @@ type (
)

type StatsReporter struct {
statFn StatFn
reportFn ReportFn
interval time.Duration
cancel context.CancelFunc
lggr logger.Logger
once sync.Once
wg sync.WaitGroup
statFn StatFn
reportFn ReportFn
beholderMetrics *dbStatsBeholderMetrics
interval time.Duration
cancel context.CancelFunc
lggr logger.Logger
once sync.Once
wg sync.WaitGroup
}

func NewStatsReporter(fn StatFn, lggr logger.Logger, opts ...StatsReporterOpt) *StatsReporter {
namedLggr := logger.Named(lggr, "StatsReporter")
r := &StatsReporter{
statFn: fn,
reportFn: publishStats,
interval: dbStatsInternal,
lggr: logger.Named(lggr, "StatsReporter"),
statFn: fn,
reportFn: publishStats,
beholderMetrics: newDBStatsBeholderMetrics(namedLggr),
interval: dbStatsInternal,
lggr: namedLggr,
}

for _, opt := range opts {
Expand Down Expand Up @@ -119,14 +122,20 @@ func (r *StatsReporter) loop(ctx context.Context) {
ticker := time.NewTicker(r.interval)
defer ticker.Stop()

r.reportFn(r.statFn())
r.report(ctx)
for {
select {
case <-ticker.C:
r.reportFn(r.statFn())
r.report(ctx)
case <-ctx.Done():
r.lggr.Debug("stat reporter loop received done. stopping...")
return
}
}
}

func (r *StatsReporter) report(ctx context.Context) {
stats := r.statFn()
r.reportFn(stats)
r.beholderMetrics.record(ctx, stats)
}
Loading