# GORM

> Instrument GORM v2 applications with the official OpenTelemetry plugin and pair it with the Last9 Go Agent's database wrapper for two-layer tracing

Source: https://last9.io/docs/integrations/frameworks/go/gorm/

For GORM v2, use the official [`gorm.io/plugin/opentelemetry`](https://github.com/go-gorm/opentelemetry) tracing plugin. The Last9 Go Agent does not ship a GORM wrapper — the upstream plugin is maintained alongside GORM itself, ships current OpenTelemetry semantic conventions, and emits connection-pool metrics by default.

Pair the upstream plugin with [`integrations/database`](/docs/integrations/databases/postgresql/) from `last9/go-agent` and you get a two-layer trace per query:

```
Gin / HTTP server span
  └─ select users   (gorm.io/plugin/opentelemetry — db.system.name,
                                                    db.collection.name,
                                                    db.query.text)
        └─ SELECT users   (integrations/database — wire-level SQL,
                                                   rows affected)
```

The GORM span (named after the operation and table — `select users`, `insert users`, etc.) carries the OTel database semantic-convention attributes; the `integrations/database` span underneath carries the wire-level SQL.

## Prerequisites

- Go 1.22 or higher
- GORM v2 (`gorm.io/gorm`)
- A SQL driver (`github.com/lib/pq`, `github.com/jackc/pgx/v5`, `github.com/go-sql-driver/mysql`, …)
- Last9 account with OTLP credentials

## Installation

1. **Install the agent and the upstream tracing plugin**

   ```bash
   go get github.com/last9/go-agent
   go get gorm.io/plugin/opentelemetry
   ```

2. **Set environment variables**

   ```bash
   export OTEL_SERVICE_NAME="your-service"
   export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"
   export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
   export OTEL_TRACES_SAMPLER="always_on"
   export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"
   ```

3. **Wire the two-layer instrumentation**

   ```go
   package main

   import (
       "log"

       agent "github.com/last9/go-agent"
       "github.com/last9/go-agent/integrations/database"
       _ "github.com/lib/pq" // register the "postgres" driver
       "gorm.io/driver/postgres"
       "gorm.io/gorm"
       "gorm.io/plugin/opentelemetry/tracing"
   )

   func main() {
       if err := agent.Start(); err != nil {
           log.Fatalf("agent: %v", err)
       }
       defer agent.Shutdown()

       // Layer 1: SQL driver instrumentation.
       sqlDB, err := database.Open(database.Config{
           DriverName: "postgres",
           DSN:        "postgres://user:pass@host:5432/db?sslmode=disable",
       })
       if err != nil {
           log.Fatalf("open sql: %v", err)
       }
       defer sqlDB.Close()

       // Layer 2: official GORM tracing plugin. Hand the otelsql-wrapped
       // *sql.DB to the dialector via Conn so GORM uses the instrumented
       // connection pool.
       db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
       if err != nil {
           log.Fatalf("gorm open: %v", err)
       }
       if err := db.Use(tracing.NewPlugin()); err != nil {
           log.Fatalf("install tracing plugin: %v", err)
       }

       // … run your application. Pass request contexts via db.WithContext(ctx).
   }
   ```

4. **Propagate context per request**

   GORM operations only attach to the parent trace if you pass a context with the active span:

   ```go
   func ListUsers(c *gin.Context) {
       var users []User
       if err := db.WithContext(c.Request.Context()).Find(&users).Error; err != nil {
           c.JSON(500, gin.H{"error": err.Error()})
           return
       }
       c.JSON(200, users)
   }
   ```

## What you get on every span

| Attribute            | Source                                                             |
| -------------------- | ------------------------------------------------------------------ |
| `db.system.name`     | dialector — `postgresql`, `mysql`, `sqlite`, `mssql`, `clickhouse` |
| `db.operation.name`  | parsed from the SQL — `select`, `insert`, `update`, `delete`       |
| `db.collection.name` | `Statement.Table` (the table name)                                 |
| `db.query.text`      | rendered SQL, with bound variables by default                      |
| `db.query.summary`   | `<operation> <table>` (also used as the span name)                 |
| `db.rows_affected`   | `Statement.RowsAffected` (only when ≥ 0)                           |
| `server.address`     | extracted from the dialector for postgres / mysql / clickhouse     |

Span name: `<operation> <table>` (e.g. `select users`, `insert users`). Span kind: `Client`. Span status is `Error` for everything except `gorm.ErrRecordNotFound`, `sql.ErrNoRows`, `io.EOF`, and `driver.ErrSkip` — a 404 lookup does not pollute your error rate.

The plugin also registers connection-pool gauges (`go.sql.connections_open`, `_idle`, `_in_use`, `_wait_count`, `_wait_duration`, `_max_open`, and others) by default.

## Plugin options

The upstream plugin exposes the following functional options. See the [plugin source](https://github.com/go-gorm/opentelemetry/blob/master/tracing/option.go) for the authoritative list.

```go
tracing.WithTracerProvider(tp trace.TracerProvider)
tracing.WithAttributes(attrs ...attribute.KeyValue)   // static extras
tracing.WithDBSystem(name string)                     // override db.system.name
tracing.WithoutQueryVariables()                       // strip bound vars
tracing.WithQueryFormatter(fn func(string) string)    // redact / transform SQL
tracing.WithoutMetrics()                              // suppress connection-pool gauges
tracing.WithoutServerAddress()                        // suppress server.address
tracing.WithRecordStackTrace()                        // attach a stack trace to error spans
```

## Trace shape

A request that hits a Gin handler and runs one query produces three spans:

```
GET /users         SERVER   (gin)
└─ select users    CLIENT   (gorm.io/plugin/opentelemetry)
    └─ SELECT      CLIENT   (integrations/database / otelsql)
```

## End-to-end example

A complete docker-compose stack with Postgres, the Go agent, GORM, and the upstream tracing plugin lives at [opentelemetry-examples/go/gorm](https://github.com/last9/opentelemetry-examples/tree/main/go/gorm).

---

## Troubleshooting

- **Span attributes show `db.query.text` parameterized rather than rendered.** That's `tracing.WithoutQueryVariables()` taking effect — remove it (or your `WithQueryFormatter`) to capture full SQL.
- **No GORM spans on requests, but SQL spans still appear.** You probably forgot `db.WithContext(ctx)` in the handler — without it, GORM uses `context.Background()` and the span has no parent.
- **Connection-pool gauges appear unexpectedly.** Upstream emits them by default. Set `tracing.WithoutMetrics()` to suppress.

Please get in touch with us on [Discord](https://discord.com/invite/Q3p2EEucx9) or [Email](mailto:support@last9.io) if you have any questions.
