forked from grafana/grafana
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathdatabase_wrapper.go
169 lines (139 loc) · 5.45 KB
/
database_wrapper.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package sqlstore
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"time"
"github.com/gchaincl/sqlhooks"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
"github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"xorm.io/core"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
var (
databaseQueryHistogram *prometheus.HistogramVec
)
func init() {
databaseQueryHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "grafana",
Name: "database_queries_duration_seconds",
Help: "Database query histogram",
Buckets: prometheus.ExponentialBuckets(0.00001, 4, 10),
}, []string{"status"})
prometheus.MustRegister(databaseQueryHistogram)
}
// WrapDatabaseDriverWithHooks creates a fake database driver that
// executes pre and post functions which we use to gather metrics about
// database queries. It also registers the metrics.
func WrapDatabaseDriverWithHooks(dbType string, tracer tracing.Tracer) string {
drivers := map[string]driver.Driver{
migrator.SQLite: &sqlite3.SQLiteDriver{},
migrator.MySQL: &mysql.MySQLDriver{},
migrator.Postgres: &pq.Driver{},
}
d, exist := drivers[dbType]
if !exist {
return dbType
}
driverWithHooks := dbType + "WithHooks"
sql.Register(driverWithHooks, sqlhooks.Wrap(d, &databaseQueryWrapper{log: log.New("sqlstore.metrics"), tracer: tracer}))
core.RegisterDriver(driverWithHooks, &databaseQueryWrapperDriver{dbType: dbType})
return driverWithHooks
}
// WrapDatabaseDriverWithHooks creates a fake database driver that
// executes pre and post functions which we use to gather metrics about
// database queries. It also registers the metrics.
func WrapDatabaseReplDriverWithHooks(dbType string, index uint, tracer tracing.Tracer) string {
drivers := map[string]driver.Driver{
migrator.SQLite: &sqlite3.SQLiteDriver{},
migrator.MySQL: &mysql.MySQLDriver{},
migrator.Postgres: &pq.Driver{},
}
d, exist := drivers[dbType]
if !exist {
return dbType
}
driverWithHooks := dbType + fmt.Sprintf("ReplicaWithHooks%d", index)
sql.Register(driverWithHooks, sqlhooks.Wrap(d, &databaseQueryWrapper{log: log.New("sqlstore.metrics"), tracer: tracer}))
core.RegisterDriver(driverWithHooks, &databaseQueryWrapperDriver{dbType: dbType})
return driverWithHooks
}
// databaseQueryWrapper satisfies the sqlhook.databaseQueryWrapper interface
// which allow us to wrap all SQL queries with a `Before` & `After` hook.
type databaseQueryWrapper struct {
log log.Logger
tracer tracing.Tracer
}
// databaseQueryWrapperKey is used as key to save values in `context.Context`
type databaseQueryWrapperKey struct{}
// Before hook will print the query with its args and return the context with the timestamp
func (h *databaseQueryWrapper) Before(ctx context.Context, query string, args ...any) (context.Context, error) {
return context.WithValue(ctx, databaseQueryWrapperKey{}, time.Now()), nil
}
// After hook will get the timestamp registered on the Before hook and print the elapsed time
func (h *databaseQueryWrapper) After(ctx context.Context, query string, args ...any) (context.Context, error) {
h.instrument(ctx, "success", query, nil)
return ctx, nil
}
func (h *databaseQueryWrapper) instrument(ctx context.Context, status string, query string, err error) {
begin := ctx.Value(databaseQueryWrapperKey{}).(time.Time)
elapsed := time.Since(begin)
histogram := databaseQueryHistogram.WithLabelValues(status)
if traceID := tracing.TraceIDFromContext(ctx, true); traceID != "" {
// Need to type-convert the Observer to an
// ExemplarObserver. This will always work for a
// HistogramVec.
histogram.(prometheus.ExemplarObserver).ObserveWithExemplar(
elapsed.Seconds(), prometheus.Labels{"traceID": traceID},
)
} else {
histogram.Observe(elapsed.Seconds())
}
ctx = log.IncDBCallCounter(ctx)
// timestamp overridden and recorded AFTER query is run
_, span := h.tracer.Start(ctx, "database query", trace.WithTimestamp(begin))
defer span.End()
span.AddEvent("query", trace.WithAttributes(attribute.String("query", query)))
span.AddEvent("status", trace.WithAttributes(attribute.String("status", status)))
if err != nil {
span.RecordError(err)
}
ctxLogger := h.log.FromContext(ctx)
ctxLogger.Debug("query finished", "status", status, "elapsed time", elapsed, "sql", query, "error", err)
}
// OnError will be called if any error happens
func (h *databaseQueryWrapper) OnError(ctx context.Context, err error, query string, args ...any) error {
// Not a user error: driver is telling sql package that an
// optional interface method is not implemented. There is
// nothing to instrument here.
// https://golang.org/pkg/database/sql/driver/#ErrSkip
// https://github.com/DataDog/dd-trace-go/issues/270
if errors.Is(err, driver.ErrSkip) {
return nil
}
status := "error"
if err == nil {
status = "success"
}
h.instrument(ctx, status, query, err)
return err
}
// databaseQueryWrapperDriver satisfies the xorm.io/core.Driver interface
type databaseQueryWrapperDriver struct {
dbType string
}
func (hp *databaseQueryWrapperDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) {
driver := core.QueryDriver(hp.dbType)
if driver == nil {
return nil, fmt.Errorf("could not find driver with name %s", hp.dbType)
}
return driver.Parse(driverName, dataSourceName)
}