-
Notifications
You must be signed in to change notification settings - Fork 255
feat(sql): add base sql/driver wrappers #1281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ use ( | |
| ./otel | ||
| ./otel/otlp | ||
| ./slog | ||
| ./sql | ||
| ./zap | ||
| ./zerolog | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| package sentrysql | ||
|
|
||
| import ( | ||
| "context" | ||
| "database/sql/driver" | ||
| "errors" | ||
| ) | ||
|
|
||
| // sentryConn wraps a driver.Conn. | ||
| type sentryConn struct { | ||
| conn driver.Conn | ||
| cfg *config | ||
| } | ||
|
|
||
| func newConn(c driver.Conn, cfg *config) driver.Conn { | ||
| return &sentryConn{conn: c, cfg: cfg} | ||
| } | ||
|
|
||
| // Ping implements driver.Pinger when the underlying connection does. | ||
| func (c *sentryConn) Ping(ctx context.Context) error { | ||
| if p, ok := c.conn.(driver.Pinger); ok { | ||
| return p.Ping(ctx) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // QueryContext implements driver.QueryerContext with fallback to the legacy | ||
| // driver.Queryer path. | ||
| func (c *sentryConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { | ||
| if qc, ok := c.conn.(driver.QueryerContext); ok { | ||
| return qc.QueryContext(ctx, query, args) | ||
| } | ||
| qr, ok := c.conn.(driver.Queryer) //nolint:staticcheck // legacy driver.Queryer fallback is intentional. | ||
| if !ok { | ||
| return nil, driver.ErrSkip | ||
| } | ||
| values, err := namedValuesToValues(args) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| select { | ||
| default: | ||
| case <-ctx.Done(): | ||
| return nil, ctx.Err() | ||
| } | ||
| return qr.Query(query, values) | ||
| } | ||
|
|
||
| // ExecContext implements driver.ExecerContext with fallback to the legacy | ||
| // driver.Execer path. | ||
| func (c *sentryConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { | ||
| if ec, ok := c.conn.(driver.ExecerContext); ok { | ||
| return ec.ExecContext(ctx, query, args) | ||
| } | ||
| ex, ok := c.conn.(driver.Execer) //nolint:staticcheck // legacy driver.Execer fallback is intentional. | ||
| if !ok { | ||
| return nil, driver.ErrSkip | ||
| } | ||
| values, err := namedValuesToValues(args) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| select { | ||
| default: | ||
| case <-ctx.Done(): | ||
| return nil, ctx.Err() | ||
| } | ||
| return ex.Exec(query, values) | ||
| } | ||
|
|
||
| // PrepareContext implements driver.ConnPrepareContext with fallback to | ||
| // Prepare when the underlying connection does not support it. | ||
| func (c *sentryConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { | ||
| if cp, ok := c.conn.(driver.ConnPrepareContext); ok { | ||
| stmt, err := cp.PrepareContext(ctx, query) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return newStmt(stmt, c.cfg, query), nil | ||
| } | ||
| stmt, err := c.Prepare(query) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| select { | ||
| default: | ||
| case <-ctx.Done(): | ||
| return nil, errors.Join(ctx.Err(), stmt.Close()) | ||
| } | ||
| return stmt, nil | ||
| } | ||
|
|
||
| // Prepare implements driver.Conn. | ||
| func (c *sentryConn) Prepare(query string) (driver.Stmt, error) { | ||
| stmt, err := c.conn.Prepare(query) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return newStmt(stmt, c.cfg, query), nil | ||
| } | ||
|
|
||
| // Close implements driver.Conn. | ||
| func (c *sentryConn) Close() error { return c.conn.Close() } | ||
|
|
||
| // Begin implements driver.Conn. | ||
| func (c *sentryConn) Begin() (driver.Tx, error) { | ||
| tx, err := c.conn.Begin() //nolint:staticcheck // required by driver.Conn; BeginTx covers the modern path. | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &sentryTx{tx: tx}, nil | ||
| } | ||
|
|
||
| // BeginTx implements driver.ConnBeginTx with fallback to Begin. | ||
| func (c *sentryConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { | ||
| if cb, ok := c.conn.(driver.ConnBeginTx); ok { | ||
| tx, err := cb.BeginTx(ctx, opts) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &sentryTx{tx: tx}, nil | ||
| } | ||
| // Mirror stdlib ctxDriverBegin: reject non-default TxOptions that can't be | ||
| // expressed through the legacy Begin(). | ||
| if opts.Isolation != 0 || opts.ReadOnly { | ||
| return nil, errors.New("sentrysql: driver does not support non-default TxOptions") | ||
| } | ||
| tx, err := c.Begin() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| select { | ||
| default: | ||
| case <-ctx.Done(): | ||
| return nil, errors.Join(ctx.Err(), tx.Rollback()) | ||
| } | ||
| return tx, nil | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BeginTx legacy fallback checks context after BeginMedium Severity In the legacy Reviewed by Cursor Bugbot for commit 3f8f67c. Configure here. |
||
| } | ||
|
|
||
| // ResetSession implements driver.SessionResetter. | ||
| func (c *sentryConn) ResetSession(ctx context.Context) error { | ||
| if r, ok := c.conn.(driver.SessionResetter); ok { | ||
| return r.ResetSession(ctx) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // IsValid implements driver.Validator. | ||
| func (c *sentryConn) IsValid() bool { | ||
| if v, ok := c.conn.(driver.Validator); ok { | ||
| return v.IsValid() | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| // CheckNamedValue implements driver.NamedValueChecker when the underlying | ||
| // connection supports it; otherwise it returns driver.ErrSkip so the standard | ||
| // library falls back to default value conversion. | ||
| func (c *sentryConn) CheckNamedValue(nv *driver.NamedValue) error { | ||
| if ch, ok := c.conn.(driver.NamedValueChecker); ok { | ||
| return ch.CheckNamedValue(nv) | ||
| } | ||
| return driver.ErrSkip | ||
| } | ||
|
|
||
| // Raw returns the underlying driver connection. Useful for type-assertions. | ||
| func (c *sentryConn) Raw() driver.Conn { | ||
| return c.conn | ||
| } | ||
|
|
||
| // namedValuesToValues converts []driver.NamedValue to []driver.Value for | ||
| // fallback calls to the legacy driver.Execer and driver.Queryer interfaces. | ||
| func namedValuesToValues(named []driver.NamedValue) ([]driver.Value, error) { | ||
| out := make([]driver.Value, len(named)) | ||
| for i, nv := range named { | ||
| if nv.Name != "" { | ||
| return nil, errors.New("sql: driver does not support named arguments") | ||
| } | ||
| out[i] = nv.Value | ||
| } | ||
| return out, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package sentrysql | ||
|
|
||
| import ( | ||
| "context" | ||
| "database/sql/driver" | ||
| "io" | ||
| ) | ||
|
|
||
| // sentryConnector wraps a driver.Connector so that returned connections are | ||
| // wrapped with sentryConn. | ||
| type sentryConnector struct { | ||
| connector driver.Connector | ||
| drv driver.Driver | ||
| cfg *config | ||
| } | ||
|
|
||
| func newConnector(c driver.Connector, cfg *config) *sentryConnector { | ||
| return &sentryConnector{ | ||
| connector: c, | ||
| drv: newDriver(c.Driver(), cfg), | ||
| cfg: cfg, | ||
| } | ||
| } | ||
|
|
||
| // Connect implements driver.Connector. | ||
| func (c *sentryConnector) Connect(ctx context.Context) (driver.Conn, error) { | ||
| conn, err := c.connector.Connect(ctx) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return newConn(conn, c.cfg), nil | ||
| } | ||
|
|
||
| // Driver implements driver.Connector. | ||
| func (c *sentryConnector) Driver() driver.Driver { return c.drv } | ||
|
|
||
| // Close checks if underlying connector implements io.Closer to Close | ||
| // the connection. | ||
| func (c *sentryConnector) Close() error { | ||
| if cl, ok := c.connector.(io.Closer); ok { | ||
| return cl.Close() | ||
| } | ||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // Package sentrysql provides Sentry instrumentation for database/sql drivers. | ||
| // | ||
| // It wraps an existing driver so that queries and executions become Sentry | ||
| // spans and are surfaced in the Queries module. | ||
| // | ||
| // Example: | ||
| // | ||
| // import ( | ||
| // _ "modernc.org/sqlite" | ||
| // "github.com/getsentry/sentry-go" | ||
| // sentrysql "github.com/getsentry/sentry-go/sql" | ||
| // ) | ||
| // | ||
| // db, err := sentrysql.Open("sqlite", ":memory:", | ||
| // sentrysql.WithDatabaseSystem(sentrysql.SystemSQLite), | ||
| // sentrysql.WithDatabaseName("main"), | ||
| // ) | ||
| // | ||
| // For well-known drivers (postgres, pgx, mysql, sqlite, sqlite3, sqlserver, | ||
| // mssql, mariadb, oracle, godror, clickhouse, snowflake, ...), Open | ||
| // and Register can infer db.system from the registration name, so the option | ||
| // is optional: | ||
| // | ||
| // db, err := sentrysql.Open("postgres", dsn) // db.system = "postgresql" | ||
| // | ||
| // OpenDB, WrapDriver, and WrapConnector cannot see a driver name and always | ||
| // require WithDatabaseSystem. | ||
| package sentrysql |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package sentrysql | ||
|
|
||
| import ( | ||
| "context" | ||
| "database/sql/driver" | ||
| "errors" | ||
| ) | ||
|
|
||
| var ( | ||
| _ driver.Driver = (*sentryDriver)(nil) | ||
| _ driver.DriverContext = (*sentryDriver)(nil) | ||
| ) | ||
|
|
||
| type sentryDriver struct { | ||
| drv driver.Driver | ||
| cfg *config | ||
| } | ||
|
|
||
| // newDriver returns a driver wrapper. | ||
| func newDriver(drv driver.Driver, cfg *config) driver.Driver { | ||
| d := &sentryDriver{drv: drv, cfg: cfg} | ||
| if _, ok := drv.(driver.DriverContext); ok { | ||
| return d | ||
| } | ||
| // implement only driver.Driver | ||
| return struct{ driver.Driver }{d} | ||
| } | ||
|
|
||
| // Open implements driver.Driver. | ||
| func (d *sentryDriver) Open(name string) (driver.Conn, error) { | ||
| c, err := d.drv.Open(name) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return newConn(c, d.cfg), nil | ||
| } | ||
|
|
||
| // OpenConnector implements driver.DriverContext. | ||
| func (d *sentryDriver) OpenConnector(name string) (driver.Connector, error) { | ||
| dc, ok := d.drv.(driver.DriverContext) | ||
| if !ok { | ||
| return nil, errors.New("sentrysql: inner driver does not implement driver.DriverContext") | ||
| } | ||
| c, err := dc.OpenConnector(name) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return newConnector(c, d.cfg), nil | ||
| } | ||
|
|
||
| // dsnConnector is a connector for drivers that do not implement | ||
| // driver.DriverContext. | ||
| type dsnConnector struct { | ||
| dsn string | ||
| drv driver.Driver | ||
| } | ||
|
|
||
| func (c *dsnConnector) Connect(_ context.Context) (driver.Conn, error) { | ||
| return c.drv.Open(c.dsn) | ||
| } | ||
|
|
||
| func (c *dsnConnector) Driver() driver.Driver { return c.drv } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| module github.com/getsentry/sentry-go/sql | ||
|
|
||
| go 1.25.0 | ||
|
|
||
| replace github.com/getsentry/sentry-go => ../ | ||
|
|
||
| require github.com/getsentry/sentry-go v0.46.0 | ||
|
|
||
| require ( | ||
| github.com/stretchr/testify v1.11.1 // indirect | ||
| golang.org/x/sys v0.18.0 // indirect | ||
| golang.org/x/text v0.14.0 // indirect | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| 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= | ||
| github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | ||
| github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= | ||
| 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/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= | ||
| github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= | ||
| 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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= | ||
| github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= | ||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||
| 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | ||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly this is useless, it just guards whenever the
cancelis called before we call theex.Exec(). If it's already passed, then it won't be canceled at all.