-
Notifications
You must be signed in to change notification settings - Fork 243
feat: database/sql integration #893
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?
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #893 +/- ##
==========================================
- Coverage 86.81% 86.63% -0.19%
==========================================
Files 62 69 +7
Lines 6092 6391 +299
==========================================
+ Hits 5289 5537 +248
- Misses 589 626 +37
- Partials 214 228 +14 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
I really don't like golangci-lint, it's so pedantic :( |
|
Should be good by now. |
|
I'll test this locally myself this week and report back. We should also update the docs and main repository with this integration. |
|
You're missing examples in _examples. These should also include example on how to set the DSN. |
@ribice I've been occupied so much with work this week. Will find the time to work on this later. |
giortzisg
left a comment
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.
Overall looks good with the most recent changes and should be functionally correct. Just wanna be sure that the context is passed correctly and also add some minor improvements
giortzisg
left a comment
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.
some test changes.
| // if WantSpan is nil, yet we got some spans, it should be an error | ||
| if tt.WantSpan == nil { | ||
| t.Errorf("Expecting no spans, but got %d spans: %v", len(gotSpans), gotSpans) | ||
| continue | ||
| } | ||
|
|
||
| // if WantSpan is not nil, we should have at least one span | ||
| if len(gotSpans) == 0 { | ||
| t.Errorf("Expecting at least one span, but got %d spans: %v", len(gotSpans), gotSpans) | ||
| continue | ||
| } | ||
|
|
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.
Can we not have tt.WantSpans as a []Span so that we can compare immediately with gotSpans? I'd say that it's not very readable.
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.
Did some rounds with this, I can't get []Span comparison working (problems with google/go-cmp package). Is it okay if we backlog this somehow?
| func TestMain(m *testing.M) { | ||
| sql.Register("sentrysql-sqlite", sentrysql.NewSentrySQL(&sqlite.Driver{}, sentrysql.WithDatabaseName("memory"), sentrysql.WithDatabaseSystem(sentrysql.DatabaseSystem("sqlite")), sentrysql.WithServerAddress("localhost", "5432"))) | ||
| // sentrysql-legacy is used by `sentrysql_legacy_test.go` | ||
| sql.Register("sentrysql-legacy", sentrysql.NewSentrySQL(ldriver, sentrysql.WithDatabaseSystem(sentrysql.DatabaseSystem("legacydb")), sentrysql.WithDatabaseName("fake"))) | ||
|
|
||
| os.Exit(m.Run()) | ||
| } |
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.
This is obfuscated from the other tests. Should probably be an init statement on each file that registers whatever we need only for the tests on the file.
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.
From the docs: If Register is called twice with the same name or if driver is nil, it panics.
I believe setting this here is the "right" way to do so, since it'll be run after any library's init invocation.
| var foundMatch = false | ||
| gotSpans := got[i] | ||
|
|
||
| var diffs []string | ||
| for _, gotSpan := range gotSpans { | ||
| if diff := cmp.Diff(tt.WantSpan, gotSpan, optstrans); diff != "" { | ||
| diffs = append(diffs, diff) | ||
| } else { | ||
| foundMatch = true | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if !foundMatch { | ||
| t.Errorf("Span mismatch (-want +got):\n%s", strings.Join(diffs, "\n")) | ||
| } |
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.
Same thing applies here. We should probably use a []Span for WantSpans
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.
Pull Request Overview
Adds Sentry tracing integration to Go’s database/sql by wrapping drivers, connectors, connections, statements, and transactions to automatically start spans for SQL operations.
- Introduce
sentrySQLConfigandOptionfunctions to attach metadata (db system, name, host, port). - Wrap
driver.Driver,driver.Connector,driver.Conn, anddriver.Stmtto emit spans on Exec/Query (including context versions). - Add
parseDatabaseOperationhelper to extract SQL operation names and tests for it.
Reviewed Changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| sentrysql/tx.go | Added sentryTx wrapper for driver.Tx (currently no spans). |
| sentrysql/stmt.go | Implemented sentryStmt with Exec/Query and ExecContext/QueryContext tracing. |
| sentrysql/sentrysql.go | Defined DatabaseSystem, config struct, and constructors (NewSentrySQL, NewSentrySQLConnector). |
| sentrysql/options.go | Added Option functions to set database metadata. |
| sentrysql/operation.go | Added parseDatabaseOperation to derive operation names. |
| sentrysql/operation_test.go | Added tests for parseDatabaseOperation. |
| sentrysql/driver.go | Wrapped driver.Driver and driver.Connector for injection. |
| sentrysql/conn.go | Implemented sentryConn with span creation for Exec/Query/Ping/Tx. |
| sentrysql/go.mod | Initialized module and dependencies. |
| sentrysql/example_test.go | Added examples for NewSentrySQL and NewSentrySQLConnector. |
| _examples/sql/main.go | End-to-end example demonstrating tracing of queries and transactions. |
Comments suppressed due to low confidence (1)
sentrysql/stmt.go:75
- There are no unit tests covering
ExecContextandQueryContextinsentryStmt. Adding tests would verify span creation and fallback behavior.
func (s *sentryStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error)
sentrysql/stmt.go
Outdated
| return s.Exec(values) | ||
| } | ||
|
|
||
| parentSpan := sentry.SpanFromContext(s.ctx) |
Copilot
AI
Jun 17, 2025
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.
In ExecContext, you’re calling SpanFromContext on s.ctx instead of the passed-in ctx. This can attach spans to a stale context. Use sentry.SpanFromContext(ctx) instead.
| parentSpan := sentry.SpanFromContext(s.ctx) | |
| parentSpan := sentry.SpanFromContext(ctx) |
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.
This is wrong. ctx provided from the function argument does not have sentry span context. SInce what matters is the context in which the statement was started.
sentrysql/stmt.go
Outdated
| return s.Query(values) | ||
| } | ||
|
|
||
| parentSpan := sentry.SpanFromContext(s.ctx) |
Copilot
AI
Jun 17, 2025
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.
In QueryContext, you’re using s.ctx for the parent span lookup instead of the method’s ctx parameter. Update to sentry.SpanFromContext(ctx) to ensure the correct context is used.
| parentSpan := sentry.SpanFromContext(s.ctx) | |
| parentSpan := sentry.SpanFromContext(ctx) |
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.
This is wrong. ctx provided from the function argument does not have sentry span context. SInce what matters is the context in which the statement was started.
| type sentryTx struct { | ||
| originalTx driver.Tx | ||
| ctx context.Context | ||
| config *sentrySQLConfig |
Copilot
AI
Jun 17, 2025
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.
[nitpick] The config field in sentryTx is never used. Consider removing it or adding tracing for Commit/Rollback to leverage the config metadata.
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.
Tracing on Commit / Rollback would be useless.
|
@cleptric @giortzisg any updates? |
|
We're currently doing a big refactoring on the transport layer, will have a look after that. |
sentrysql/conn.go
Outdated
| return nil, err | ||
| } | ||
|
|
||
| return &sentryTx{originalTx: tx, ctx: s.ctx, config: s.config}, nil |
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.
Bug: Context Propagation Issue in Sentry Wrappers
In sentryConn.BeginTx and sentryStmt's ExecContext and QueryContext methods, the wrapper incorrectly uses the stored s.ctx field instead of the method's ctx parameter when the underlying driver supports context. This can lead to stale context propagation and incorrect Sentry span parenting.
Additional Locations (2)
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.
the comment is actually correct here, we should derive the ctx from the caller and if that is nil then fallback to the parent ctx.
sentrysql/stmt.go
Outdated
| return s.Query(values) | ||
| } | ||
|
|
||
| parentSpan := sentry.SpanFromContext(s.ctx) |
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.
Bug: Incorrect Span Parentage in Context-Aware Drivers
In both ExecContext and QueryContext, when the underlying driver supports context-aware interfaces, sentry.SpanFromContext uses s.ctx instead of the ctx parameter. Since s.ctx is intended for fallback scenarios, using it here can result in incorrect span parentage or missing spans in the tracing hierarchy.
Additional Locations (1)
giortzisg
left a comment
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.
We can keep the tests as is for now. Added a few comments, let's amend them and bump the go version and we should be good to merge this.
sentrysql/conn.go
Outdated
| return nil, err | ||
| } | ||
|
|
||
| return &sentryTx{originalTx: tx, ctx: s.ctx, config: s.config}, nil |
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.
the comment is actually correct here, we should derive the ctx from the caller and if that is nil then fallback to the parent ctx.
sentrysql/conn.go
Outdated
| s.config.SetData(span, query) | ||
| defer span.Finish() | ||
|
|
||
| rows, err := queryerContext.QueryContext(ctx, query, args) |
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.
We should propagate the span context here.
| rows, err := queryerContext.QueryContext(ctx, query, args) | |
| rows, err := queryerContext.QueryContext(span.Context(), query, args) |
sentrysql/conn.go
Outdated
| s.config.SetData(span, query) | ||
| defer span.Finish() | ||
|
|
||
| rows, err := execerContext.ExecContext(ctx, query, args) |
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.
| rows, err := execerContext.ExecContext(ctx, query, args) | |
| rows, err := execerContext.ExecContext(span.Context(), query, args) |
sentrysql/stmt.go
Outdated
| s.config.SetData(span, s.query) | ||
| defer span.Finish() | ||
|
|
||
| result, err := stmtExecContext.ExecContext(ctx, args) |
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.
| result, err := stmtExecContext.ExecContext(ctx, args) | |
| result, err := stmtExecContext.ExecContext(span.Context(), args) |
sentrysql/stmt.go
Outdated
| s.config.SetData(span, s.query) | ||
| defer span.Finish() | ||
|
|
||
| rows, err := stmtQueryContext.QueryContext(ctx, args) |
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.
| rows, err := stmtQueryContext.QueryContext(ctx, args) | |
| rows, err := stmtQueryContext.QueryContext(span.Context(), args) |
| } | ||
|
|
||
| span := parentSpan.StartChild("db.sql.query", sentry.WithDescription(s.query)) | ||
| s.config.SetData(span, s.query) |
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.
we need to set span data according to this page https://develop.sentry.dev/sdk/telemetry/traces/sql-transactions/
sentrysql/conn.go
Outdated
| return nil, err | ||
| } | ||
|
|
||
| return &sentryTx{originalTx: tx, ctx: s.ctx, config: s.config}, nil |
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.
Bug: Wrong context passed to transaction wrapper
The BeginTx method passes s.ctx (the connection's stored context) to sentryTx instead of the ctx parameter received from the caller. This causes the transaction to use an incorrect context, which breaks tracing functionality since subsequent database operations within the transaction won't have access to the correct parent span from the caller's context. The fix is to pass ctx instead of s.ctx when creating the sentryTx instance.
sentrysql/stmt.go
Outdated
| return s.Exec(values) | ||
| } | ||
|
|
||
| parentSpan := sentry.SpanFromContext(s.ctx) |
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.
Bug: Wrong context used in statement ExecContext
The ExecContext method retrieves the parent span from s.ctx instead of the ctx parameter passed to the method. This causes tracing to use an incorrect or stale context rather than the caller's context, breaking the span hierarchy and potentially missing trace data for database operations executed through prepared statements.
| s.ctx = ctx | ||
| return s.Query(values) | ||
| } | ||
|
|
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.
Bug: Wrong context used in statement QueryContext
The QueryContext method retrieves the parent span from s.ctx instead of the ctx parameter passed to the method. This causes tracing to use an incorrect or stale context rather than the caller's context, breaking the span hierarchy and potentially missing trace data for database queries executed through prepared statements.
sentrysql/conn.go
Outdated
| return nil, err | ||
| } | ||
|
|
||
| return &sentryTx{originalTx: tx, ctx: s.ctx, config: s.config}, nil |
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.
Bug: Wrong context passed to transaction wrapper
In BeginTx, when the driver supports ConnBeginTx, the code passes s.ctx to sentryTx instead of the ctx parameter. This means the transaction wrapper receives a stale context instead of the current one provided by the caller, potentially breaking context propagation and span tracking for database transactions. The fallback path correctly updates s.ctx before use, but the modern path does not.
sentrysql/stmt.go
Outdated
| return s.Exec(values) | ||
| } | ||
|
|
||
| parentSpan := sentry.SpanFromContext(s.ctx) |
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.
Bug: Wrong context used for span in statement execution
In ExecContext, when the original statement implements StmtExecContext, the code retrieves the parent span from s.ctx instead of the ctx parameter. This causes span tracking to use a stale context rather than the current one provided by the caller, potentially breaking the span hierarchy and context propagation for prepared statement executions.
| s.ctx = ctx | ||
| return s.Query(values) | ||
| } | ||
|
|
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.
Bug: Wrong context used for span in statement query
In QueryContext, when the original statement implements StmtQueryContext, the code retrieves the parent span from s.ctx instead of the ctx parameter. This causes span tracking to use a stale context rather than the current one provided by the caller, potentially breaking the span hierarchy and context propagation for prepared statement queries.
|
|
||
| return s.Exec(values) | ||
| } | ||
|
|
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.
Bug: Context from ExecContext lost in legacy driver fallback
When the underlying driver doesn't implement StmtExecContext, the method falls back to calling s.Exec(values) which uses the stored s.ctx field from prepare time. However, the ctx parameter passed to ExecContext is ignored. This differs from the pattern in conn.go where the context field is updated before calling legacy methods. The execution-time context should take precedence to properly capture spans and cancellation signals. The same issue exists in QueryContext at lines 113-123.
| connPrepareContext, ok := s.originalConn.(driver.ConnPrepareContext) | ||
| if !ok { | ||
| // We can't return driver.ErrSkip here. We should fall back to Prepare without context. | ||
| s.ctx = ctx |
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.
Bug: Race condition from concurrent context field mutation
The sentryConn.ctx field is mutated without synchronization in PrepareContext, BeginTx, QueryContext, and ExecContext when falling back to non-context methods. Since database connections can be used concurrently from multiple goroutines, this creates a race condition where one goroutine's context can overwrite another's, potentially causing spans to be attached to the wrong parent or contexts to be cancelled unexpectedly. The same issue exists in sentryStmt at line 85 and 119. This violates Go's database/sql package concurrency guarantees where connections from a pool can be used by multiple goroutines.
| return nil, err | ||
| } | ||
|
|
||
| return s.Query(values) |
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.
Bug: Missing context assignment in QueryContext fallback path
In sentryStmt.QueryContext, when the underlying driver doesn't implement StmtQueryContext and falls back to the non-context Query method, the code fails to assign the context to s.ctx before calling s.Query(values). This causes the context and any associated span information to be lost. The ExecContext method correctly does s.ctx = ctx before calling s.Exec(values) at line 85, but QueryContext is missing this assignment at line 123, resulting in inconsistent behavior where query tracing won't work for legacy drivers.
Queries insights tracing for Go.
closes #1128
closes GO-95