Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use (
./otel
./otel/otlp
./slog
./sql
./zap
./zerolog
)
182 changes: 182 additions & 0 deletions sql/conn.go
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()
}
Comment on lines +63 to +67
Copy link
Copy Markdown
Contributor

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 cancel is called before we call the ex.Exec(). If it's already passed, then it won't be canceled at all.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BeginTx legacy fallback checks context after Begin

Medium Severity

In the legacy BeginTx fallback path, c.Begin() is called before checking ctx.Done(). The Go stdlib's ctxDriverBegin checks context cancellation before calling ci.Begin(). This means the wrapper starts a transaction on the database even when the context is already cancelled, then immediately rolls it back — causing unnecessary database round-trips and a different error shape (errors.Join of context error and potential rollback error).

Fix in Cursor Fix in Web

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
}
44 changes: 44 additions & 0 deletions sql/connector.go
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
}
28 changes: 28 additions & 0 deletions sql/doc.go
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
62 changes: 62 additions & 0 deletions sql/driver.go
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 }
13 changes: 13 additions & 0 deletions sql/go.mod
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
)
22 changes: 22 additions & 0 deletions sql/go.sum
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=
Loading
Loading