OpenTelemetry GORM monitoring [otelgorm]
Learn how to monitor GORM performance using OpenTelemetry GORM instrumentation. The otelgorm plugin automatically traces all database operations including queries, creates, updates, deletes, and transactions.
What is GORM?
GORM is the most popular ORM library for Go. It provides a developer-friendly API for database operations with features like auto migrations, associations, hooks, and support for multiple databases.
Key features of GORM include:
- Full-featured ORM with associations, hooks, preloading, and transactions
- Auto migrations for schema management
- Multiple database support including PostgreSQL, MySQL, SQLite, and SQL Server
- Batch operations for bulk inserts and updates
- Query builder with method chaining
What is OpenTelemetry?
OpenTelemetry is an open-source observability framework that aims to standardize and simplify the collection, processing, and export of telemetry data from applications and systems.
OpenTelemetry supports multiple programming languages and platforms, making it suitable for a wide range of applications and environments.
OpenTelemetry enables developers to instrument their code and collect telemetry data, which can then be exported to various OpenTelemetry backends or observability platforms for analysis and visualization. Understanding the OpenTelemetry architecture helps developers leverage its full potential with proper instrumentation and data collection strategies.
Installation
To install GORM OpenTelemetry instrumentation:
go get github.com/uptrace/opentelemetry-go-extra/otelgorm
Quick setup
To instrument GORM, register the otelgorm plugin after opening the database:
import (
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
dsn := "host=localhost user=postgres dbname=myapp sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
if err := db.Use(otelgorm.NewPlugin()); err != nil {
panic(err)
}
Context propagation
You must use db.WithContext(ctx) to propagate the active span via context. Without context, otelgorm cannot link database spans to parent spans:
var user User
if err := db.WithContext(ctx).First(&user, 1).Error; err != nil {
panic(err)
}
Plugin options
The otelgorm.NewPlugin() function accepts these options:
| Option | Description |
|---|---|
WithTracerProvider | Use a custom TracerProvider instead of the global one |
WithDBName | Set the database name attribute on spans |
WithAttributes | Add custom attributes to all spans |
WithoutQueryVariables | Exclude query variables from span attributes |
WithQueryFormatter | Custom function to format SQL queries in spans |
WithoutMetrics | Disable automatic metrics collection |
WithDryRunTx | Create spans for dry-run transactions |
Custom tracer provider
import (
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"go.opentelemetry.io/otel"
)
db.Use(otelgorm.NewPlugin(
otelgorm.WithTracerProvider(otel.GetTracerProvider()),
))
Setting database name
db.Use(otelgorm.NewPlugin(
otelgorm.WithDBName("myapp_production"),
))
Adding custom attributes
import "go.opentelemetry.io/otel/attribute"
db.Use(otelgorm.NewPlugin(
otelgorm.WithAttributes(
attribute.String("environment", "production"),
attribute.String("team", "backend"),
),
))
Formatting queries
Customize how SQL queries appear in spans:
db.Use(otelgorm.NewPlugin(
otelgorm.WithQueryFormatter(func(query string) string {
// Truncate long queries
if len(query) > 500 {
return query[:500] + "..."
}
return query
}),
))
Excluding query variables
For security, exclude query parameter values from spans:
db.Use(otelgorm.NewPlugin(
otelgorm.WithoutQueryVariables(),
))
Span names and attributes
otelgorm creates spans for each database operation with the following naming convention:
| Operation | Span Name | Example |
|---|---|---|
| Find/First | gorm.Query | SELECT * FROM users WHERE id = ? |
| Create | gorm.Create | INSERT INTO users (name) VALUES (?) |
| Update/Save | gorm.Update | UPDATE users SET name = ? WHERE ... |
| Delete | gorm.Delete | DELETE FROM users WHERE id = ? |
| Raw query | gorm.Row | SELECT 42 |
| Raw exec | gorm.Raw | TRUNCATE TABLE logs |
Each span includes these attributes:
| Attribute | Description |
|---|---|
db.system | Database type (postgresql, mysql, sqlite) |
db.statement | The SQL query text |
db.rows_affected | Number of rows affected (for write operations) |
Tracing CRUD operations
Create
user := User{Name: "Alice", Email: "alice@example.com"}
result := db.WithContext(ctx).Create(&user)
// Creates span: gorm.Create with INSERT INTO users ...
Read
var user User
db.WithContext(ctx).First(&user, 1)
// Creates span: gorm.Query with SELECT * FROM users WHERE id = 1
var users []User
db.WithContext(ctx).Where("age > ?", 18).Find(&users)
// Creates span: gorm.Query with SELECT * FROM users WHERE age > 18
Update
db.WithContext(ctx).Model(&user).Update("name", "Bob")
// Creates span: gorm.Update with UPDATE users SET name = 'Bob' WHERE ...
db.WithContext(ctx).Model(&user).Updates(User{Name: "Bob", Age: 30})
// Creates span: gorm.Update with UPDATE users SET name = 'Bob', age = 30 WHERE ...
Delete
db.WithContext(ctx).Delete(&user, 1)
// Creates span: gorm.Delete with DELETE FROM users WHERE id = 1
Raw queries
var result int
db.WithContext(ctx).Raw("SELECT COUNT(*) FROM users WHERE active = ?", true).Scan(&result)
// Creates span: gorm.Row with SELECT COUNT(*) FROM users WHERE active = true
Transactions
Transactions are automatically traced. Each operation within a transaction creates its own span, and Begin, Commit, and Rollback are also traced:
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "Alice"}).Error; err != nil {
return err // triggers tx.Rollback span
}
if err := tx.Create(&Order{UserID: 1, Amount: 100}).Error; err != nil {
return err
}
return nil // triggers tx.Commit span
})
Preloading associations
Preloaded associations generate separate spans for each query:
var user User
db.WithContext(ctx).Preload("Orders").Preload("Profile").First(&user, 1)
// Creates spans:
// gorm.Query: SELECT * FROM users WHERE id = 1
// gorm.Query: SELECT * FROM orders WHERE user_id = 1
// gorm.Query: SELECT * FROM profiles WHERE user_id = 1
Error handling
When a GORM operation fails, otelgorm automatically records the error on the span and sets the span status to error:
var user User
err := db.WithContext(ctx).First(&user, "invalid").Error
// Span status: Error
// Span status description: "record not found" or SQL error message
You don't need to manually record errors for database operations.
Combining with HTTP instrumentation
For end-to-end tracing, combine otelgorm with HTTP server instrumentation. Database spans become children of the HTTP request span:
import (
"net/http"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.Use(otelgorm.NewPlugin())
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var users []User
// Database span is a child of the HTTP request span
db.WithContext(r.Context()).Find(&users)
// ... respond with users
})
http.ListenAndServe(":8080", otelhttp.NewHandler(handler, "my-service"))
}
What is Uptrace?
Uptrace is an OpenTelemetry APM that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.
Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.
In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.
FAQ
What is otelgorm? The otelgorm package is an OpenTelemetry instrumentation plugin for GORM. It automatically creates spans for all database operations, capturing SQL queries, timing, and error information.
Do I need to use WithContext for every query? Yes, you must call db.WithContext(ctx) for each query to propagate the trace context. Without it, otelgorm cannot create properly linked spans.
What databases does otelgorm support? otelgorm works with any database supported by GORM, including PostgreSQL, MySQL, SQLite, and SQL Server. The db.system attribute is set automatically based on the driver.
Are transactions automatically traced? Yes, Begin, Commit, and Rollback operations each create their own spans. Individual queries within the transaction are also traced as separate spans.
How do I hide sensitive query data? Use otelgorm.WithoutQueryVariables() to exclude query parameter values from spans. You can also use otelgorm.WithQueryFormatter() to customize or sanitize the SQL text.
Does otelgorm affect query performance? The overhead is minimal. otelgorm hooks into GORM's callback system and only adds span creation/closure around each operation. For most applications, the tracing overhead is negligible compared to actual database query time.
What's the difference between otelgorm and otelsql? otelgorm operates at the GORM level and understands GORM operations (Create, Query, Update, Delete). otelsql operates at the database/sql driver level and traces raw SQL operations. Use otelgorm if you're using GORM; use otelsql if you're using database/sql directly.
What's next?
GORM instrumentation provides detailed insights into your database operations, including query tracing and performance metrics.
Next steps to enhance your observability:
- For lower-level SQL monitoring, check out database/sql instrumentation
- Explore Ent for an alternative ORM approach
- Add HTTP instrumentation with Gin, Echo, or net/http
- Correlate logs with traces using Zap or slog
- Create custom spans using the OpenTelemetry Go Tracing API