Skip to content
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

contrib: add gorm.io/gorm #759

Merged
merged 27 commits into from
Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contrib/database/sql/internal/dsn.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func ParseDSN(driverName, dsn string) (meta map[string]string, err error) {
if err != nil {
return
}
case "postgres":
case "postgres", "pgx":
meta, err = parsePostgresDSN(dsn)
if err != nil {
return
Expand Down
41 changes: 41 additions & 0 deletions contrib/gorm.io/gorm/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-2020 Datadog, Inc.

package gorm_test

import (
"database/sql"
"github.com/jackc/pgx/v4/stdlib"
gorm2 "gopkg.in/DataDog/dd-trace-go.v1/contrib/gorm.io/gorm"
AdallomRoy marked this conversation as resolved.
Show resolved Hide resolved
"gorm.io/driver/postgres"
"log"

"gorm.io/gorm"

sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
)

func postgresDialector(db *sql.DB) gorm.Dialector {
knusbaum marked this conversation as resolved.
Show resolved Hide resolved
return postgres.New(postgres.Config{Conn: db})
}

func ExampleOpen() {
// Register augments the provided driver with tracing, enabling it to be loaded by gormtrace.Open.
sqltrace.Register("pgx", &stdlib.Driver{}, sqltrace.WithServiceName("my-service"))

// Open the registered driver, allowing all uses of the returned *gorm.DB to be traced.
db, err := gorm2.Open(postgresDialector, "pgx", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable")
if err != nil {
log.Fatal(err)
}

user := struct {
gorm.Model
Name string
}{}

// All calls through gorm.DB are now traced.
db.Where("name = ?", "jinzhu").First(&user)
}
181 changes: 181 additions & 0 deletions contrib/gorm.io/gorm/gorm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-2020 Datadog, Inc.

// Package gorm provides helper functions for tracing the gorm.io/gorm package (https://github.com/go-gorm/gorm).
package gorm

import (
"context"
"database/sql"
"math"
"time"

sqltraced "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"

"gorm.io/gorm"
)

const (
gormConfigKey = "dd-trace-go:config"
gormSpanStartTimeKey = "dd-trace-go:span"
)

// Open opens a new (traced) database connection. The used driver must be formerly registered
// using (gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql).Register.
func Open(getDialector func(db *sql.DB) gorm.Dialector, driverName, source string, opts ...Option) (*gorm.DB, error) {
knusbaum marked this conversation as resolved.
Show resolved Hide resolved
dialector := getDialector(nil)
AdallomRoy marked this conversation as resolved.
Show resolved Hide resolved

sqldb, err := sqltraced.Open(driverName, source)
if err != nil {
return nil, err
}

dialector = getDialector(sqldb)

db, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
return db, err
}

return WithCallbacks(db, opts...)
}

// WithCallbacks registers callbacks to the gorm.DB for tracing.
// It should be called once, after opening the db.
// The callbacks are triggered by Create, Update, Delete,
// Query and RowQuery operations.
func WithCallbacks(db *gorm.DB, opts ...Option) (*gorm.DB, error) {
knusbaum marked this conversation as resolved.
Show resolved Hide resolved
afterFunc := func(operationName string) func(*gorm.DB) {
return func(db *gorm.DB) {
after(db, operationName)
}
}

cfg := new(config)
defaults(cfg)
for _, fn := range opts {
fn(cfg)
}

ctx := context.Background()
if db.Statement != nil && db.Statement.Context != nil {
ctx = db.Statement.Context
}

db = db.WithContext(context.WithValue(ctx, gormConfigKey, cfg))
knusbaum marked this conversation as resolved.
Show resolved Hide resolved

cb := db.Callback()
err := cb.Create().Before("gorm:create").Register("dd-trace-go:before_create", before)
if err != nil {
return db, err
}
err = cb.Create().After("gorm:create").Register("dd-trace-go:after_create", afterFunc("gorm.create"))
if err != nil {
return db, err
}
err = cb.Update().Before("gorm:update").Register("dd-trace-go:before_update", before)
if err != nil {
return db, err
}
err = cb.Update().After("gorm:update").Register("dd-trace-go:after_update", afterFunc("gorm.update"))
if err != nil {
return db, err
}
err = cb.Delete().Before("gorm:delete").Register("dd-trace-go:before_delete", before)
if err != nil {
return db, err
}
err = cb.Delete().After("gorm:delete").Register("dd-trace-go:after_delete", afterFunc("gorm.delete"))
if err != nil {
return db, err
}
err = cb.Query().Before("gorm:query").Register("dd-trace-go:before_query", before)
if err != nil {
return db, err
}
err = cb.Query().After("gorm:query").Register("dd-trace-go:after_query", afterFunc("gorm.query"))
if err != nil {
return db, err
}
err = cb.Row().Before("gorm:query").Register("dd-trace-go:before_row_query", before)
if err != nil {
return db, err
}
err = cb.Row().After("gorm:query").Register("dd-trace-go:after_row_query", afterFunc("gorm.row_query"))
if err != nil {
return db, err
}



AdallomRoy marked this conversation as resolved.
Show resolved Hide resolved
return db, nil
}

// WithContext attaches the specified context to the given db. The context will
// be used as a basis for creating new spans. An example use case is providing
// a context which contains a span to be used as a parent.
func WithContext(ctx context.Context, db *gorm.DB) *gorm.DB {
if ctx == nil {
return db
}

AdallomRoy marked this conversation as resolved.
Show resolved Hide resolved
if db.Statement != nil && db.Statement.Context != nil {
ctx = context.WithValue(ctx, gormConfigKey, db.Statement.Context.Value(gormConfigKey))
}

knusbaum marked this conversation as resolved.
Show resolved Hide resolved

AdallomRoy marked this conversation as resolved.
Show resolved Hide resolved
return db.WithContext(ctx)
}

// ContextFromDB returns any context previously attached to db using WithContext,
// otherwise returning context.Background.
func ContextFromDB(db *gorm.DB) context.Context {
if db.Statement != nil {
if v, ok := db.Statement.Context.(context.Context); ok {
knusbaum marked this conversation as resolved.
Show resolved Hide resolved
return v
}
}

return context.Background()
}

func before(scope *gorm.DB) {
if scope.Statement != nil && scope.Statement.Context != nil {
scope.Statement.Context = context.WithValue(scope.Statement.Context, gormSpanStartTimeKey, time.Now())
}
}

func after(db *gorm.DB, operationName string) {
ctx := db.Statement.Context
if ctx == nil {
return
}

cfg, ok := ctx.Value(gormConfigKey).(*config)
if !ok {
return
}

t, ok := ctx.Value(gormSpanStartTimeKey).(time.Time)
if !ok {
return
}

opts := []ddtrace.StartSpanOption{
tracer.StartTime(t),
tracer.ServiceName(cfg.serviceName),
tracer.SpanType(ext.SpanTypeSQL),
tracer.ResourceName(db.Statement.SQL.String()),
}
if !math.IsNaN(cfg.analyticsRate) {
opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate))
}

span, _ := tracer.StartSpanFromContext(ctx, operationName, opts...)
span.Finish(tracer.WithError(db.Error))
}
Loading