mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 19:09:19 +02:00
148 lines
3.6 KiB
Go
148 lines
3.6 KiB
Go
package sqlutil
|
|
|
|
import (
|
|
"context"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/oxynote/oxynote/server/core/pkg/metricutil"
|
|
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
type contextKey int
|
|
|
|
const timeKey contextKey = iota + 1
|
|
|
|
const (
|
|
// _queryLabel specifies a constant raw query label.
|
|
_queryLabel = "query"
|
|
)
|
|
|
|
// Hooks specifies custom database querying hooks and implements
|
|
// sqlhook.hooks interface.
|
|
type Hooks struct {
|
|
// dbName specifies a string that is used in all
|
|
// metrics as a subsystem.
|
|
dbName string
|
|
|
|
// durationThreshold specifies a duration threshold which must be
|
|
// exceeded in order to create a metric log.
|
|
durationThreshold time.Duration
|
|
|
|
// errorHandler specifies a function for handling errors.
|
|
errorHandler func(error) error
|
|
|
|
queriesCounter metricutil.CounterVec
|
|
queriesDuration metricutil.HistogramVec
|
|
}
|
|
|
|
// HookOption specifies a hook option.
|
|
type HookOption func(*Hooks)
|
|
|
|
// WithHookDatabaseName specifies a database prefix.
|
|
func WithHookDatabaseName(prefix string) HookOption {
|
|
return func(h *Hooks) {
|
|
h.dbName = prefix
|
|
}
|
|
}
|
|
|
|
// WithHookDurationThreshold specifies a duration threshold option.
|
|
func WithHookDurationThreshold(durationThreshold time.Duration) HookOption {
|
|
return func(h *Hooks) {
|
|
h.durationThreshold = durationThreshold
|
|
}
|
|
}
|
|
|
|
// WithHookErrorHandler specifies an error handling function.
|
|
func WithHookErrorHandler(hdl func(error) error) HookOption {
|
|
return func(h *Hooks) {
|
|
h.errorHandler = hdl
|
|
}
|
|
}
|
|
|
|
// NewHooks creates a new hooks instance.
|
|
func NewHooks(
|
|
fc metricutil.Factory,
|
|
opts ...HookOption,
|
|
) *Hooks {
|
|
h := &Hooks{}
|
|
|
|
for _, opt := range opts {
|
|
opt(h)
|
|
}
|
|
|
|
h.queriesCounter = fc.NewCounterVec(
|
|
metricutil.Options{
|
|
Subsystem: h.dbName, //nolint:promlinter // cannot validate a variable
|
|
Name: "database_queries_total",
|
|
Help: "Tracks the number of database queries.",
|
|
},
|
|
[]string{
|
|
_queryLabel,
|
|
},
|
|
)
|
|
|
|
h.queriesDuration = fc.NewHistogramVec(
|
|
metricutil.Options{
|
|
Subsystem: h.dbName, //nolint:promlinter // cannot validate a variable
|
|
Name: "database_queries_duration_seconds",
|
|
Help: "Tracks the durations of database queries.",
|
|
},
|
|
[]string{
|
|
_queryLabel,
|
|
},
|
|
)
|
|
|
|
return h
|
|
}
|
|
|
|
// Before executes before the query.
|
|
func (h *Hooks) Before(
|
|
ctx context.Context,
|
|
query string,
|
|
_ ...any,
|
|
) (context.Context, error) {
|
|
h.queriesCounter.With(prometheus.Labels{
|
|
_queryLabel: normalizeQuery(query),
|
|
}).Inc()
|
|
|
|
return context.WithValue(ctx, timeKey, timeutil.Now()), nil
|
|
}
|
|
|
|
// OnError executes on error.
|
|
func (h *Hooks) OnError(_ context.Context, err error, _ string, _ ...any) error {
|
|
if h.errorHandler != nil {
|
|
return h.errorHandler(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// After executes after the query.
|
|
func (h *Hooks) After(
|
|
ctx context.Context,
|
|
query string,
|
|
_ ...any,
|
|
) (context.Context, error) {
|
|
startedAt, ok := ctx.Value(timeKey).(time.Time)
|
|
if ok && time.Since(startedAt) > h.durationThreshold {
|
|
h.queriesDuration.With(prometheus.Labels{
|
|
_queryLabel: normalizeQuery(query),
|
|
}).Observe(time.Since(startedAt).Seconds())
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|
|
|
|
// _placeholderListRe matches a run of two or more positional placeholders,
|
|
// which is the one part of a squirrel query that varies with the data.
|
|
var _placeholderListRe = regexp.MustCompile(`\$\d+(\s*,\s*\$\d+)+`)
|
|
|
|
// normalizeQuery collapses the parts of a query that vary per call, so an IN
|
|
// clause over a slice mints one label value instead of one per slice length.
|
|
// Every squirrel query is otherwise constant-shaped.
|
|
func normalizeQuery(query string) string {
|
|
return _placeholderListRe.ReplaceAllString(query, "$$?")
|
|
}
|