OpenTelemetry GORM monitoring [otelgorm]

Vladimir Mihailenco
February 14, 2026
6 min read

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:

shell
go get github.com/uptrace/opentelemetry-go-extra/otelgorm

Quick setup

To instrument GORM, register the otelgorm plugin after opening the database:

go
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:

go
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:

OptionDescription
WithTracerProviderUse a custom TracerProvider instead of the global one
WithDBNameSet the database name attribute on spans
WithAttributesAdd custom attributes to all spans
WithoutQueryVariablesExclude query variables from span attributes
WithQueryFormatterCustom function to format SQL queries in spans
WithoutMetricsDisable automatic metrics collection
WithDryRunTxCreate spans for dry-run transactions

Custom tracer provider

go
import (
    "github.com/uptrace/opentelemetry-go-extra/otelgorm"
    "go.opentelemetry.io/otel"
)

db.Use(otelgorm.NewPlugin(
    otelgorm.WithTracerProvider(otel.GetTracerProvider()),
))

Setting database name

go
db.Use(otelgorm.NewPlugin(
    otelgorm.WithDBName("myapp_production"),
))

Adding custom attributes

go
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:

go
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:

go
db.Use(otelgorm.NewPlugin(
    otelgorm.WithoutQueryVariables(),
))

Span names and attributes

otelgorm creates spans for each database operation with the following naming convention:

OperationSpan NameExample
Find/Firstgorm.QuerySELECT * FROM users WHERE id = ?
Creategorm.CreateINSERT INTO users (name) VALUES (?)
Update/Savegorm.UpdateUPDATE users SET name = ? WHERE ...
Deletegorm.DeleteDELETE FROM users WHERE id = ?
Raw querygorm.RowSELECT 42
Raw execgorm.RawTRUNCATE TABLE logs

Each span includes these attributes:

AttributeDescription
db.systemDatabase type (postgresql, mysql, sqlite)
db.statementThe SQL query text
db.rows_affectedNumber of rows affected (for write operations)

Tracing CRUD operations

Create

go
user := User{Name: "Alice", Email: "alice@example.com"}
result := db.WithContext(ctx).Create(&user)
// Creates span: gorm.Create with INSERT INTO users ...

Read

go
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

go
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

go
db.WithContext(ctx).Delete(&user, 1)
// Creates span: gorm.Delete with DELETE FROM users WHERE id = 1

Raw queries

go
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:

go
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:

go
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:

go
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:

go
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 Overview

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: