feat: [#726] database auto instrumentation [8] - #1477
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1477 +/- ##
==========================================
+ Coverage 69.46% 69.63% +0.16%
==========================================
Files 378 384 +6
Lines 29710 30183 +473
==========================================
+ Hits 20639 21017 +378
- Misses 8118 8174 +56
- Partials 953 992 +39 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds OpenTelemetry database instrumentation to Goravel, covering GORM operations, query-builder calls, and connection-pool metrics, with a config toggle to enable/disable the feature.
Changes:
- Introduces a new
telemetry/instrumentation/databasepackage (spans, operation-duration histogram, pool stats metrics, and wrappers for DB builders). - Wires database instrumentation into GORM connection setup and DB query builder execution paths behind
telemetry.instrumentation.database.enabled. - Adjusts
Tx.Selectto execute parameterized SQL (placeholders) while still logging the explained SQL, and adds tests around the behavior.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| telemetry/setup/stubs.go | Adds default config stub for database instrumentation enablement. |
| telemetry/instrumentation/database/database.go | Core DB span + duration metric helpers and semantic attributes. |
| telemetry/instrumentation/database/database_test.go | Unit tests for DB system mapping, operation parsing, and recordable-error filtering. |
| telemetry/instrumentation/database/pool.go | Registers observable metrics from sql.DBStats for connection pool telemetry. |
| telemetry/instrumentation/database/pool_test.go | Validates pool metric registration and emitted metric names/attributes. |
| telemetry/instrumentation/database/gorm.go | GORM plugin to create spans for ORM operations and optionally register pool metrics once. |
| telemetry/instrumentation/database/gorm_test.go | Ensures GORM spans are created, named, and do not include interpolated bound values. |
| telemetry/instrumentation/database/builder.go | Wraps query-builder interfaces to create spans/metrics for sqlx operations. |
| telemetry/instrumentation/database/builder_test.go | Tests traced builder wrappers for spans and error status behavior. |
| telemetry/instrumentation/database/helpers_test.go | Shared test telemetry setup (recording exporter + facade mocking). |
| database/driver/gorm.go | Enables GORM plugin registration (with pool metrics) when database instrumentation is enabled. |
| database/db/db.go | Enables builder wrapping + fixes Tx.Select to execute parameterized SQL while logging explained SQL. |
| database/db/db_test.go | Tests that Tx.Select passes placeholder SQL through to builder methods. |
…abase-instrumentation
hwbrzzl
left a comment
There was a problem hiding this comment.
Great PR, could you add some screenshots for this feature to confirm if it works?
|
Please let me know if it's ready, wish to release it in v1.18 if we can. |
The gorm plugin and query-builder decorator captured the telemetry facade when the connection was built. If the database connection is established before the telemetry provider boots, the facade was still nil, so no plugin was registered and the cached connection stayed uninstrumented. Resolve telemetry lazily through a resolver on first query instead: always register the plugin and wrap the builder, and no-op until telemetry is available and enabled.
4337f0e to
e0e0131
Compare
Replace the hand-rolled ready/disabled atomics and double-checked locking in Instrument.active with a sync.Once, and read the telemetry facade directly instead of through a single-implementation resolver. Drop the now dead nil guards in WrapBuilder/WrapTxBuilder and the redundant active checks in startSpan/endSpan.
Switch database instrumentation from direct telemetry injection to the lazy resolver pattern used by the log handler. The Instrument is created once per DB connection and shared across all Tx instances, eliminating per-transaction allocations. What changed: - NewInstrument takes a Resolver closure instead of a concrete Telemetry - active() resolves lazily with nil-safe guards and mutex for thread safety - Shared Instrument: DB creates one, passes it to every Tx/BeginTransaction - operationName uses TrimLeft+IndexByte instead of strings.Fields (zero alloc) - Pool metrics now include wait_time and timeouts from sql.DBStats - ContextWithTable moved from NewQuery to Tx.Table to avoid breaking clone ctx - builder_test refactored to use testify suite pattern - Added tests: nil resolver guard, raw query without table, negative rows, operationName edge cases (whitespace, newline, single word)
Expose Enabled() to skip plugin registration when disabled. Remove config dependency from Instrument and GormPlugin. Replace contextWrapper with gorm's tx.Statement.Settings for passing span state between before/after callbacks. Rewrite all test files to testify suite with table-driven cases.
hwbrzzl
left a comment
There was a problem hiding this comment.
LGTM, is it possible to add tests in goravel/example to test this feature?
Sure will add tests |
📑 Description
RelatedTo goravel/goravel#726
The #726 series instruments HTTP, gRPC, and logs, but database queries stay invisible: no spans, no query metrics, no connection pool visibility. Telemetry is unreleased, so this lands before the v1.18 surface freezes.
What is changing:
facades.Orm()through a gorm callback plugin (create/query/update/delete/row/raw), andfacades.DB()(the sqlx query builder, raw and fluent) through aCommonBuilderdecorator. The builder runs sqlx on the raw*sql.DBand never reaches gorm callbacks, so the two paths stay disjoint: one operation produces one span.telemetry.instrumentation.database.enabled(default true). When disabled, the builder is returned unwrapped, so there is no overhead. The instrument is cached with the connection, so everyfacades.DB()/facades.Orm()call on the same connection (includingConnection("...")switches) reuses it and both facades stay instrumented.What it provides:
SELECT users, nested under the request span, with semconv v1.37 attributes. Structured queries on both facades carrydb.collection.name; raw SQL stays operation-only, matchingOrm().Raw().db.query.textkeeps placeholders only.ErrRecordNotFoundandsql.ErrNoRowsare not treated as errors.db.client.operation.durationhistogram ({operation, table}), plus connection pool gaugesdb.client.connection.count(by state idle/used) anddb.client.connection.max.How to use:
Nothing to wire up. With telemetry enabled, both facades emit automatically:
Each produces a
SELECT usersclient span under the request span and adb.client.operation.duration{operation=SELECT, table=users}measurement. Settelemetry.instrumentation.database.enabledto false to turn it off.Caveats:
db.query.textcarries the parameterized SQL with placeholders only, so values (including PII) do not leak into traces.db.query.text(high cardinality); the query text lives on spans only.countandmaxonly. Richer pool signals (wait time, timeouts) are not exported becausesql.DBStatsgives cumulative snapshots, not the per-event observations the OTel pool conventions expect.gorm.ConnPoolinstances with no publicsql.DBStats, so they are not measured; spans still carrydb.client.connection.pool.stateto distinguish source from replica.✅ Checks