mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add prometheus metrics to database.Store (#7713)
* Adds dbmetrics package and wraps database.Store with a Prometheus HistogramVec of timings. * Adds Wrappers method to database.Store to avoid double-wrapping interfaces * Fixes test flake in TestLicensesListFake
This commit is contained in:
@ -24,11 +24,20 @@ type Store interface {
|
||||
querier
|
||||
// customQuerier contains custom queries that are not generated.
|
||||
customQuerier
|
||||
// wrapper allows us to detect if the interface has been wrapped.
|
||||
wrapper
|
||||
|
||||
Ping(ctx context.Context) (time.Duration, error)
|
||||
InTx(func(Store) error, *sql.TxOptions) error
|
||||
}
|
||||
|
||||
type wrapper interface {
|
||||
// Wrappers returns a list of wrappers that have been applied to the store.
|
||||
// This is used to detect if the store has already wrapped, and avoid
|
||||
// double-wrapping.
|
||||
Wrappers() []string
|
||||
}
|
||||
|
||||
// DBTX represents a database connection or transaction.
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
@ -60,6 +69,10 @@ type sqlQuerier struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (*sqlQuerier) Wrappers() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Ping returns the time it takes to ping the database.
|
||||
func (q *sqlQuerier) Ping(ctx context.Context) (time.Duration, error) {
|
||||
start := time.Now()
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/open-policy-agent/opa/topdown"
|
||||
@ -17,6 +18,8 @@ import (
|
||||
|
||||
var _ database.Store = (*querier)(nil)
|
||||
|
||||
const wrapname = "dbauthz.querier"
|
||||
|
||||
// NoActorError wraps ErrNoRows for the api to return a 404. This is the correct
|
||||
// response when the user is not authorized.
|
||||
var NoActorError = xerrors.Errorf("no authorization actor in context: %w", sql.ErrNoRows)
|
||||
@ -89,7 +92,7 @@ type querier struct {
|
||||
func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger) database.Store {
|
||||
// If the underlying db store is already a querier, return it.
|
||||
// Do not double wrap.
|
||||
if _, ok := db.(*querier); ok {
|
||||
if slices.Contains(db.Wrappers(), wrapname) {
|
||||
return db
|
||||
}
|
||||
return &querier{
|
||||
@ -99,6 +102,10 @@ func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger) data
|
||||
}
|
||||
}
|
||||
|
||||
func (q *querier) Wrappers() []string {
|
||||
return append(q.db.Wrappers(), wrapname)
|
||||
}
|
||||
|
||||
// authorizeContext is a helper function to authorize an action on an object.
|
||||
func (q *querier) authorizeContext(ctx context.Context, action rbac.Action, object rbac.Objecter) error {
|
||||
act, ok := ActorFromContext(ctx)
|
||||
|
@ -138,7 +138,7 @@ func TestDBAuthzRecursive(t *testing.T) {
|
||||
for i := 2; i < method.Type.NumIn(); i++ {
|
||||
ins = append(ins, reflect.New(method.Type.In(i)).Elem())
|
||||
}
|
||||
if method.Name == "InTx" || method.Name == "Ping" {
|
||||
if method.Name == "InTx" || method.Name == "Ping" || method.Name == "Wrappers" {
|
||||
continue
|
||||
}
|
||||
// Log the name of the last method, so if there is a panic, it is
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-policy-agent/opa/topdown"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -19,14 +20,16 @@ import (
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/coderd/database/dbfake"
|
||||
"github.com/coder/coder/coderd/database/dbmock"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/rbac/regosql"
|
||||
"github.com/coder/coder/coderd/util/slice"
|
||||
)
|
||||
|
||||
var skipMethods = map[string]string{
|
||||
"InTx": "Not relevant",
|
||||
"Ping": "Not relevant",
|
||||
"InTx": "Not relevant",
|
||||
"Ping": "Not relevant",
|
||||
"Wrappers": "Not relevant",
|
||||
}
|
||||
|
||||
// TestMethodTestSuite runs MethodTestSuite.
|
||||
@ -52,7 +55,11 @@ type MethodTestSuite struct {
|
||||
// SetupSuite sets up the suite by creating a map of all methods on querier
|
||||
// and setting their count to 0.
|
||||
func (s *MethodTestSuite) SetupSuite() {
|
||||
az := dbauthz.New(nil, nil, slog.Make())
|
||||
ctrl := gomock.NewController(s.T())
|
||||
mockStore := dbmock.NewMockStore(ctrl)
|
||||
// We intentionally set no expectations apart from this.
|
||||
mockStore.EXPECT().Wrappers().Return([]string{}).AnyTimes()
|
||||
az := dbauthz.New(mockStore, nil, slog.Make())
|
||||
// Take the underlying type of the interface.
|
||||
azt := reflect.TypeOf(az).Elem()
|
||||
s.methodAccounting = make(map[string]int)
|
||||
|
@ -97,6 +97,10 @@ type fakeQuerier struct {
|
||||
*data
|
||||
}
|
||||
|
||||
func (*fakeQuerier) Wrappers() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
type fakeTx struct {
|
||||
*fakeQuerier
|
||||
locks map[int64]struct{}
|
||||
|
1569
coderd/database/dbmetrics/dbmetrics.go
Normal file
1569
coderd/database/dbmetrics/dbmetrics.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -3233,3 +3233,17 @@ func (mr *MockStoreMockRecorder) UpsertServiceBanner(arg0, arg1 interface{}) *go
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertServiceBanner", reflect.TypeOf((*MockStore)(nil).UpsertServiceBanner), arg0, arg1)
|
||||
}
|
||||
|
||||
// Wrappers mocks base method.
|
||||
func (m *MockStore) Wrappers() []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Wrappers")
|
||||
ret0, _ := ret[0].([]string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Wrappers indicates an expected call of Wrappers.
|
||||
func (mr *MockStoreMockRecorder) Wrappers() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wrappers", reflect.TypeOf((*MockStore)(nil).Wrappers))
|
||||
}
|
||||
|
Reference in New Issue
Block a user