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:
Cian Johnston
2023-05-31 06:55:57 -07:00
committed by GitHub
parent 00a30775bc
commit 784696dfa5
10 changed files with 1641 additions and 8 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -97,6 +97,10 @@ type fakeQuerier struct {
*data
}
func (*fakeQuerier) Wrappers() []string {
return []string{}
}
type fakeTx struct {
*fakeQuerier
locks map[int64]struct{}

File diff suppressed because it is too large Load Diff

View File

@ -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))
}