Skip to content

Commit

Permalink
feat(bunotel): report DBStats metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
vmihailenco committed Oct 7, 2021
1 parent 9a16018 commit b9b1575
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 29 deletions.
7 changes: 7 additions & 0 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,14 @@ func (db *DB) ScanRow(ctx context.Context, rows *sql.Rows, dest ...interface{})
return rs.ScanRow(ctx, rows)
}

type queryHookIniter interface {
Init(db *DB)
}

func (db *DB) AddQueryHook(hook QueryHook) {
if initer, ok := hook.(queryHookIniter); ok {
initer.Init(db)
}
db.queryHooks = append(db.queryHooks, hook)
}

Expand Down
22 changes: 22 additions & 0 deletions extra/bunotel/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package bunotel

import (
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)

type Option func(h *QueryHook)

// WithAttributes configures attributes that are used to create a span.
func WithAttributes(attrs ...attribute.KeyValue) Option {
return func(h *QueryHook) {
h.attrs = append(h.attrs, attrs...)
}
}

// WithDBName configures a db.name attribute.
func WithDBName(name string) Option {
return func(h *QueryHook) {
h.attrs = append(h.attrs, semconv.DBNameKey.String(name))
}
}
122 changes: 93 additions & 29 deletions extra/bunotel/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/global"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"

"github.com/uptrace/bun"
Expand All @@ -20,43 +21,51 @@ import (
)

var (
tracer = otel.Tracer("github.com/uptrace/bun")
meter = metric.Must(global.Meter("github.com/uptrace/bun"))
tracer = otel.Tracer("github.com/uptrace/bun")
meter = metric.Must(global.Meter("github.com/uptrace/bun"))

queryHistogram = meter.NewInt64Histogram(
"bun.query.timing",
"go.sql.query_timing",
metric.WithDescription("Timing of processed queries"),
metric.WithUnit("milliseconds"),
)
)

type ConfigOption func(*QueryHook)

type QueryHook struct{}
type QueryHook struct {
attrs []attribute.KeyValue
}

var _ bun.QueryHook = (*QueryHook)(nil)

func NewQueryHook(opts ...ConfigOption) *QueryHook {
func NewQueryHook(opts ...Option) *QueryHook {
h := new(QueryHook)
for _, opt := range opts {
opt(h)
}
return h
}

func (h *QueryHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) context.Context {
if !trace.SpanFromContext(ctx).IsRecording() {
return ctx
func (h *QueryHook) Init(db *bun.DB) {
labels := make([]attribute.KeyValue, 0, len(h.attrs)+1)
labels = append(labels, h.attrs...)
if sys := dbSystem(db); sys.Valid() {
labels = append(labels, sys)
}

reportDBStats(db.DB, labels)
}

func (h *QueryHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) context.Context {
ctx, _ = tracer.Start(ctx, "")
return ctx
}

func (h *QueryHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {
operation := event.Operation()
dbOperation := attribute.String("db.operation", operation)
dbOperation := semconv.DBOperationKey.String(operation)

labels := []attribute.KeyValue{dbOperation}
labels := make([]attribute.KeyValue, 2)
labels = append(labels, dbOperation)
if event.IQuery != nil {
if tableName := event.IQuery.GetTableName(); tableName != "" {
labels = append(labels, attribute.String("db.table", tableName))
Expand All @@ -79,14 +88,14 @@ func (h *QueryHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {
attrs := make([]attribute.KeyValue, 0, 10)
attrs = append(attrs,
dbOperation,
attribute.String("db.statement", query),
attribute.String("code.function", fn),
attribute.String("code.filepath", file),
attribute.Int("code.lineno", line),
semconv.DBStatementKey.String(query),
semconv.CodeFunctionKey.String(fn),
semconv.CodeFilepathKey.String(file),
semconv.CodeLineNumberKey.Int(line),
)

if s := dbSystem(event.DB); s != "" {
attrs = append(attrs, attribute.String("db.system", s))
if sys := dbSystem(event.DB); sys.Valid() {
attrs = append(attrs, sys)
}
if event.Result != nil {
if n, _ := event.Result.RowsAffected(); n > 0 {
Expand All @@ -95,12 +104,8 @@ func (h *QueryHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {
}

if event.Err != nil {
switch event.Err {
case sql.ErrNoRows:
default:
span.RecordError(event.Err)
span.SetStatus(codes.Error, event.Err.Error())
}
span.RecordError(event.Err)
span.SetStatus(codes.Error, event.Err.Error())
}

span.SetAttributes(attrs...)
Expand Down Expand Up @@ -158,15 +163,74 @@ func unformattedQuery(event *bun.QueryEvent) string {
return string(event.Query)
}

func dbSystem(db *bun.DB) string {
func dbSystem(db *bun.DB) attribute.KeyValue {
switch db.Dialect().Name() {
case dialect.PG:
return "postgresql"
return semconv.DBSystemPostgreSQL
case dialect.MySQL:
return "mysql"
return semconv.DBSystemMySQL
case dialect.SQLite:
return "sqlite"
return semconv.DBSystemSqlite
default:
return ""
return attribute.KeyValue{}
}
}

func reportDBStats(db *sql.DB, labels []attribute.KeyValue) {
var maxOpenConns metric.Int64GaugeObserver
var openConns metric.Int64GaugeObserver
var inUseConns metric.Int64GaugeObserver
var idleConns metric.Int64GaugeObserver
var connsWaitCount metric.Int64CounterObserver
var connsWaitDuration metric.Int64CounterObserver
var connsClosedMaxIdle metric.Int64CounterObserver
var connsClosedMaxIdleTime metric.Int64CounterObserver
var connsClosedMaxLifetime metric.Int64CounterObserver

batch := meter.NewBatchObserver(func(ctx context.Context, result metric.BatchObserverResult) {
stats := db.Stats()

result.Observe(labels,
maxOpenConns.Observation(int64(stats.MaxOpenConnections)),

openConns.Observation(int64(stats.OpenConnections)),
inUseConns.Observation(int64(stats.InUse)),
idleConns.Observation(int64(stats.Idle)),

connsWaitCount.Observation(stats.WaitCount),
connsWaitDuration.Observation(int64(stats.WaitDuration)),
connsClosedMaxIdle.Observation(stats.MaxIdleClosed),
connsClosedMaxIdleTime.Observation(stats.MaxIdleTimeClosed),
connsClosedMaxLifetime.Observation(stats.MaxLifetimeClosed),
)
})

maxOpenConns = batch.NewInt64GaugeObserver("go.sql.connections_max_open",
metric.WithDescription("Maximum number of open connections to the database"),
)
openConns = batch.NewInt64GaugeObserver("go.sql.connections_open",
metric.WithDescription("The number of established connections both in use and idle"),
)
inUseConns = batch.NewInt64GaugeObserver("go.sql.connections_in_use",
metric.WithDescription("The number of connections currently in use"),
)
idleConns = batch.NewInt64GaugeObserver("go.sql.connections_idle",
metric.WithDescription("The number of idle connections"),
)
connsWaitCount = batch.NewInt64CounterObserver("go.sql.connections_wait_count",
metric.WithDescription("The total number of connections waited for"),
)
connsWaitDuration = batch.NewInt64CounterObserver("go.sql.connections_wait_duration",
metric.WithDescription("The total time blocked waiting for a new connection"),
metric.WithUnit("nanoseconds"),
)
connsClosedMaxIdle = batch.NewInt64CounterObserver("go.sql.connections_closed_max_idle",
metric.WithDescription("The total number of connections closed due to SetMaxIdleConns"),
)
connsClosedMaxIdleTime = batch.NewInt64CounterObserver("go.sql.connections_closed_max_idle_time",
metric.WithDescription("The total number of connections closed due to SetConnMaxIdleTime"),
)
connsClosedMaxLifetime = batch.NewInt64CounterObserver("go.sql.connections_closed_max_lifetime",
metric.WithDescription("The total number of connections closed due to SetConnMaxLifetime"),
)
}

0 comments on commit b9b1575

Please sign in to comment.