diff --git a/Makefile b/Makefile index d58b5ac..f500507 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ get: fmt: get go fmt ./... +tidy: + go mod tidy + # assert that there is no difference after running format no-diff: git diff --exit-code @@ -25,7 +28,7 @@ test: vet get-ginkgo # test target which includes the no-diff fail condition -ci-test: fmt no-diff test +ci-test: fmt tidy no-diff test test-docker: docker build -f Dockerfile.test . diff --git a/conn.go b/conn.go index be4bfc5..cea8935 100644 --- a/conn.go +++ b/conn.go @@ -16,6 +16,9 @@ type managedConn struct { reset bool killed bool mu sync.RWMutex + + execStmtsCounter int // count the number of exec calls in a transaction + queryStmtsCounter int // count the number of query calls in a transaction } // BeginTx calls the underlying BeginTx method unless the supervising context @@ -34,7 +37,12 @@ func (c *managedConn) BeginTx(ctx context.Context, opts driver.TxOptions) (drive } if conn, ok := c.conn.(driver.ConnBeginTx); ok { - return conn.BeginTx(ctx, opts) + tx, err := conn.BeginTx(ctx, opts) + if err != nil { + return nil, err + } + + return &managedTx{tx: tx, conn: c, ctx: ctx}, nil } // same as is defined in go sql package to call Begin method if the TxOptions are default @@ -71,6 +79,7 @@ func (c *managedConn) Exec(query string, args []driver.Value) (driver.Result, er if !ok { return nil, driver.ErrSkip } + c.incExecStmtsCounter() //increment the exec counter to keep track of the number of exec calls return conn.Exec(query, args) } @@ -79,6 +88,7 @@ func (c *managedConn) ExecContext(ctx context.Context, query string, args []driv if !ok { return nil, driver.ErrSkip } + c.incExecStmtsCounter() //increment the exec counter to keep track of the number of exec calls return conn.ExecContext(ctx, query, args) } @@ -95,6 +105,7 @@ func (c *managedConn) Query(query string, args []driver.Value) (driver.Rows, err if !ok { return nil, driver.ErrSkip } + c.incQueryStmtsCounter() //increment the query counter to keep track of the number of query calls return conn.Query(query, args) } @@ -103,6 +114,7 @@ func (c *managedConn) QueryContext(ctx context.Context, query string, args []dri if !ok { return nil, driver.ErrSkip } + c.incQueryStmtsCounter() //increment the query counter to keep track of the number of query calls return conn.QueryContext(ctx, query, args) } @@ -184,3 +196,27 @@ func (c *managedConn) GetKill() bool { defer c.mu.RUnlock() return c.killed } + +func (c *managedConn) incExecStmtsCounter() { + c.mu.Lock() + defer c.mu.Unlock() + c.execStmtsCounter++ +} + +func (c *managedConn) resetExecStmtsCounter() { + c.mu.Lock() + defer c.mu.Unlock() + c.execStmtsCounter = 0 +} + +func (c *managedConn) incQueryStmtsCounter() { + c.mu.Lock() + defer c.mu.Unlock() + c.queryStmtsCounter++ +} + +func (c *managedConn) resetQueryStmtsCounter() { + c.mu.Lock() + defer c.mu.Unlock() + c.queryStmtsCounter = 0 +} diff --git a/conn_test.go b/conn_test.go index d17a278..39463ab 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1,9 +1,15 @@ package hotload import ( + "context" + "database/sql/driver" + "io" + "strings" + "sync" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "sync" + "github.com/prometheus/client_golang/prometheus/testutil" ) var _ = Describe("managedConn", func() { @@ -34,3 +40,197 @@ var _ = Describe("managedConn", func() { Consistently(readLockAcquired).Should(BeFalse()) }) }) + +/**** Mocks for Prometheus Metrics ****/ + +type mockDriverConn struct{} + +type mockTx struct{} + +func (mockTx) Commit() error { + return nil +} + +func (mockTx) Rollback() error { + return nil +} + +func (mockDriverConn) Prepare(query string) (driver.Stmt, error) { + return nil, nil +} + +func (mockDriverConn) Begin() (driver.Tx, error) { + return mockTx{}, nil +} + +func (mockDriverConn) Close() error { + return nil +} + +func (mockDriverConn) IsValid() bool { + return true +} + +func (mockDriverConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { + return mockTx{}, nil +} + +func (mockDriverConn) Exec(query string, args []driver.Value) (driver.Result, error) { + return nil, nil +} + +func (mockDriverConn) Query(query string, args []driver.Value) (driver.Rows, error) { + return nil, nil +} + +func (mockDriverConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + return nil, nil +} + +func (mockDriverConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { + return nil, nil +} + +/**** End Mocks for Prometheus Metrics ****/ + +var _ = Describe("PrometheusMetrics", func() { + const help = ` + # HELP transaction_sql_stmts The number of sql stmts called in a transaction by statement type per grpc service and method + # TYPE transaction_sql_stmts summary + ` + + var service1Metrics = ` + transaction_sql_stmts_sum{grpc_method="method_1",grpc_service="service_1",stmt="exec"} 3 + transaction_sql_stmts_count{grpc_method="method_1",grpc_service="service_1",stmt="exec"} 1 + transaction_sql_stmts_sum{grpc_method="method_1",grpc_service="service_1",stmt="query"} 3 + transaction_sql_stmts_count{grpc_method="method_1",grpc_service="service_1",stmt="query"} 1 + ` + + var service2Metrics = ` + transaction_sql_stmts_sum{grpc_method="method_2",grpc_service="service_2",stmt="exec"} 4 + transaction_sql_stmts_count{grpc_method="method_2",grpc_service="service_2",stmt="exec"} 1 + transaction_sql_stmts_sum{grpc_method="method_2",grpc_service="service_2",stmt="query"} 4 + transaction_sql_stmts_count{grpc_method="method_2",grpc_service="service_2",stmt="query"} 1 + ` + + var service1RerunMetrics = ` + transaction_sql_stmts_sum{grpc_method="method_1",grpc_service="service_1",stmt="exec"} 4 + transaction_sql_stmts_count{grpc_method="method_1",grpc_service="service_1",stmt="exec"} 2 + transaction_sql_stmts_sum{grpc_method="method_1",grpc_service="service_1",stmt="query"} 4 + transaction_sql_stmts_count{grpc_method="method_1",grpc_service="service_1",stmt="query"} 2 + ` + + var noMethodMetrics = ` + transaction_sql_stmts_sum{grpc_method="",grpc_service="",stmt="exec"} 1 + transaction_sql_stmts_count{grpc_method="",grpc_service="",stmt="exec"} 1 + transaction_sql_stmts_sum{grpc_method="",grpc_service="",stmt="query"} 1 + transaction_sql_stmts_count{grpc_method="",grpc_service="",stmt="query"} 1 + ` + + It("Should emit the correct metrics", func() { + mc := newManagedConn(context.Background(), mockDriverConn{}) + + ctx := ContextWithExecLabels(context.Background(), map[string]string{"grpc_method": "method_1", "grpc_service": "service_1"}) + + // begin a transaction + tx, err := mc.BeginTx(ctx, driver.TxOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + + // exec a statement + mc.Exec("INSERT INTO table (column) VALUES (?)", []driver.Value{"value"}) + + // query a statement + mc.Query("SELECT * FROM table WHERE column = ?", []driver.Value{"value"}) + mc.Query("SELECT * FROM table WHERE column = ?", []driver.Value{"value"}) + + // exec a statement with context + mc.ExecContext(ctx, "INSERT INTO table (column) VALUES (?)", []driver.NamedValue{{Value: "value"}}) + mc.ExecContext(ctx, "INSERT INTO table (column) VALUES (?)", []driver.NamedValue{{Value: "value"}}) + + // query a statement with context + mc.QueryContext(ctx, "SELECT * FROM table WHERE column = ?", []driver.NamedValue{{Value: "value"}}) + + // commit the transaction + err = tx.Commit() + Expect(err).ShouldNot(HaveOccurred()) + + // collect and compare metrics + err = testutil.CollectAndCompare(sqlStmtsSummary, strings.NewReader(help+service1Metrics)) + Expect(err).ShouldNot(HaveOccurred()) + + // reset the metrics + // new context + ctx = ContextWithExecLabels(context.Background(), map[string]string{"grpc_method": "method_2", "grpc_service": "service_2"}) + // begin a transaction + tx, err = mc.BeginTx(ctx, driver.TxOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + + // exec a statement + mc.Exec("INSERT INTO table (column) VALUES (?)", []driver.Value{"value"}) + mc.Exec("INSERT INTO table (column) VALUES (?)", []driver.Value{"value"}) + + // query a statement + mc.Query("SELECT * FROM table WHERE column = ?", []driver.Value{"value"}) + mc.Query("SELECT * FROM table WHERE column = ?", []driver.Value{"value"}) + + // exec a statement with context + mc.ExecContext(ctx, "INSERT INTO table (column) VALUES (?)", []driver.NamedValue{{Value: "value"}}) + mc.ExecContext(ctx, "INSERT INTO table (column) VALUES (?)", []driver.NamedValue{{Value: "value"}}) + + // query a statement with context + mc.QueryContext(ctx, "SELECT * FROM table WHERE column = ?", []driver.NamedValue{{Value: "value"}}) + mc.QueryContext(ctx, "SELECT * FROM table WHERE column = ?", []driver.NamedValue{{Value: "value"}}) + + // commit the transaction + err = tx.Commit() + Expect(err).ShouldNot(HaveOccurred()) + + // collect and compare metrics + err = testutil.CollectAndCompare(sqlStmtsSummary, strings.NewReader(help+service1Metrics+service2Metrics)) + Expect(err).ShouldNot(HaveOccurred()) + + // rerun with initial metrics + ctx = ContextWithExecLabels(context.Background(), map[string]string{"grpc_method": "method_1", "grpc_service": "service_1"}) + // begin a transaction + tx, err = mc.BeginTx(ctx, driver.TxOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + + // exec a statement with context + mc.ExecContext(ctx, "INSERT INTO table (column) VALUES (?)", []driver.NamedValue{{Value: "value"}}) + + // query a statement with context + mc.QueryContext(ctx, "SELECT * FROM table WHERE column = ?", []driver.NamedValue{{Value: "value"}}) + + // rollback the transaction + err = tx.Rollback() + Expect(err).ShouldNot(HaveOccurred()) + + // collect and compare metrics + err = testutil.CollectAndCompare(sqlStmtsSummary, strings.NewReader(help+service1RerunMetrics+service2Metrics)) + Expect(err).ShouldNot(HaveOccurred()) + + // non labeled context + ctx = context.Background() + // begin a transaction + tx, err = mc.BeginTx(ctx, driver.TxOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + + // exec query context + mc.ExecContext(ctx, "INSERT INTO table (column) VALUES (?)", []driver.NamedValue{{Value: "value"}}) + + // query a statement with context + mc.QueryContext(ctx, "SELECT * FROM table WHERE column = ?", []driver.NamedValue{{Value: "value"}}) + + // commit the transaction + err = tx.Commit() + Expect(err).ShouldNot(HaveOccurred()) + + // collect and compare metrics + err = testutil.CollectAndCompare(sqlStmtsSummary, strings.NewReader(help+noMethodMetrics+service1RerunMetrics+service2Metrics)) + Expect(err).ShouldNot(HaveOccurred()) + }) +}) + +func CollectAndCompareMetrics(r io.Reader) error { + return testutil.CollectAndCompare(sqlStmtsSummary, r) +} diff --git a/go.mod b/go.mod index bf48ad7..6251049 100644 --- a/go.mod +++ b/go.mod @@ -9,17 +9,26 @@ require ( github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.27.6 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.18.0 github.com/sirupsen/logrus v1.9.0 ) require ( - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/nxadm/tail v1.4.8 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/tools v0.7.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 53e73c1..ef3409f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,10 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -10,7 +15,6 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -18,16 +22,22 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.8 h1:3fdt97i/cwSU83+E0hZTC/Xpc9mTZxc6UWSCRcSbxiE= github.com/lib/pq v1.10.8/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -44,11 +54,19 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -61,8 +79,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -77,17 +95,16 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -98,9 +115,10 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/integrationtests/hotload_test.go b/integrationtests/hotload_test.go index cd91db1..398a9be 100644 --- a/integrationtests/hotload_test.go +++ b/integrationtests/hotload_test.go @@ -3,15 +3,16 @@ package integrationtests import ( "database/sql" "fmt" + "io/ioutil" + "log" + "time" + "github.com/infobloxopen/hotload" _ "github.com/infobloxopen/hotload/fsnotify" "github.com/lib/pq" _ "github.com/lib/pq" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "io/ioutil" - "log" - "time" ) const ( diff --git a/prometheus.go b/prometheus.go new file mode 100644 index 0000000..4c97af2 --- /dev/null +++ b/prometheus.go @@ -0,0 +1,24 @@ +package hotload + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +const ( + GRPCMethodKey = "grpc_method" + GRPCServiceKey = "grpc_service" + StatementKey = "stmt" // either exec or query + ExecStatement = "exec" + QueryStatement = "query" +) + +// sqlStmtsSummary is a prometheus metric to keep track of the number of times +// a sql statement is called in a transaction by statement type per grpc service +var sqlStmtsSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Name: "transaction_sql_stmts", + Help: "The number of sql stmts called in a transaction by statement type per grpc service and method", +}, []string{GRPCServiceKey, GRPCMethodKey, StatementKey}) + +func init() { + prometheus.MustRegister(sqlStmtsSummary) +} diff --git a/prometheus_test.go b/prometheus_test.go new file mode 100644 index 0000000..64ec7c9 --- /dev/null +++ b/prometheus_test.go @@ -0,0 +1,18 @@ +package hotload + +import ( + "errors" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/prometheus/client_golang/prometheus" +) + +var _ = Describe("PrometheusMetric", func() { + It("Should register a prometheus metric", func() { + // This test is a placeholder for a real test + err := prometheus.Register(sqlStmtsSummary) + Expect(err).Should(HaveOccurred()) + Expect(errors.As(err, &prometheus.AlreadyRegisteredError{})).Should(BeTrue()) + }) +}) diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..7d695bd --- /dev/null +++ b/transaction.go @@ -0,0 +1,59 @@ +package hotload + +import ( + "context" + "database/sql/driver" +) + +// managedTx wraps a sql/driver.Tx so that it can store the context of the +// transaction and clean up the execqueryCallsCounter on Commit or Rollback. +type managedTx struct { + tx driver.Tx + conn *managedConn + ctx context.Context +} + +func (t *managedTx) Commit() error { + err := t.tx.Commit() + t.cleanup() + return err +} + +func (t *managedTx) Rollback() error { + err := t.tx.Rollback() + t.cleanup() + return err +} + +func observeSQLStmtsSummary(ctx context.Context, execStmtsCounter, queryStmtsCounter int) { + labels := GetExecLabelsFromContext(ctx) + service := labels[GRPCServiceKey] + method := labels[GRPCMethodKey] + + sqlStmtsSummary.WithLabelValues(service, method, ExecStatement).Observe(float64(execStmtsCounter)) + sqlStmtsSummary.WithLabelValues(service, method, QueryStatement).Observe(float64(queryStmtsCounter)) +} + +func (t *managedTx) cleanup() { + observeSQLStmtsSummary(t.ctx, t.conn.execStmtsCounter, t.conn.queryStmtsCounter) + t.conn.resetExecStmtsCounter() + t.conn.resetQueryStmtsCounter() +} + +var promLabelKey = struct{}{} + +func ContextWithExecLabels(ctx context.Context, labels map[string]string) context.Context { + return context.WithValue(ctx, promLabelKey, labels) +} + +func GetExecLabelsFromContext(ctx context.Context) map[string]string { + if ctx == nil { + return nil + } + + if ctx.Value(promLabelKey) == nil { + return nil + } + + return ctx.Value(promLabelKey).(map[string]string) +}