otgorm

package module
v0.19.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 23, 2021 License: MIT Imports: 13 Imported by: 0

README

OpenTelemetry Package for the Gorm ORM

This package is meant to simplify wrapping Gorm requests to databases with OpenTelemetry Tracing Spans. The functionality within the package is as of OpenTelemetry-go v0.2.1 and is subject to change fairly rapidly as the standard is evolved.

This package is B.Y.O.E. (Bring Your Own Exporter)

Metrics support coming soon!

Example Usage

Make sure you have the following:

  • Docker
  • Go 1.13
  • cURL or Postman for testing

Run the following commands to create the testing environment:

  • docker run -d -p 5432:5432 -e POSTGRES_USER=testuser -e POSTGRES_PASSWORD=password! -e POSTGRES_DB=test --name postgres postgres:alpine
  • docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 14250:14250 -p 9411:9411 jaegertracing/all-in-one:1.16
.env File
JAEGER_HOST=127.0.0.1
DB_USER=testuser
DB_PASS=password!
DB_HOST=127.0.0.1
DB_PORT=5432
DB_NAME=test
DB_SSLMODE=disable
Example App
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/jdefrank/otgorm"

	"github.com/go-chi/chi"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
	_ "github.com/joho/godotenv/autoload"
	"go.opentelemetry.io/otel/api/global"
	"go.opentelemetry.io/otel/exporter/trace/jaeger"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

type user struct {
	ID        uint      `gorm:"primary_key" json:"id"`
	FirstName string    `json:"firstName"`
	LastName  string    `json:"lastName"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

func readBody(bodyreader io.ReadCloser) (data []byte, err error) {
	body, err := ioutil.ReadAll(io.LimitReader(bodyreader, 1048576))
	if err != nil {
		return nil, err
	}
	if err := bodyreader.Close(); err != nil {
		return nil, err
	}
	return body, nil
}

func httpTraceWrapper(h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		t := global.TraceProvider().Tracer("component-http")
		ctx, span := t.Start(r.Context(), r.URL.Path)
		r = r.WithContext(ctx)
		h.ServeHTTP(w, r)
		span.End()
	}
	return http.HandlerFunc(fn)
}

// NOTE: I've found that if you'd like to separate services in the Jaeger UI,
// you'll need to create multiple exporters which in turn will show you different colors
// per service.
func initTracer() func() {
	//Create Jaeger exporter
	exporter, err := jaeger.NewExporter(
		jaeger.WithCollectorEndpoint(fmt.Sprintf("http://%s:14268/api/traces", os.Getenv("JAEGER_HOST"))),
		jaeger.WithProcess(jaeger.Process{
			ServiceName: "go-otel-gorm",
		}),
	)
	if err != nil {
		log.Fatal(err)
	}
	// For demoing purposes, always sample. In a production application, you should
	// configure this to a trace.ProbabilitySampler set at the desired
	// probability.
	tp, err := sdktrace.NewProvider(
		sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}),
		sdktrace.WithSyncer(exporter))
	if err != nil {
		log.Fatal(err)
	}
	global.SetTraceProvider(tp)
	return func() {
		exporter.Flush()
	}
}

func main() {
	//Setup the exporter and defer close until main exits
	fn := initTracer()
	defer fn()

	// Connect to database
	connString := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s",
		os.Getenv("DB_HOST"),
		os.Getenv("DB_PORT"),
		os.Getenv("DB_USER"),
		os.Getenv("DB_NAME"),
		os.Getenv("DB_PASS"),
		os.Getenv("DB_SSLMODE"),
	)
	db, err := gorm.Open("postgres", connString)
	if err != nil {
		panic(err)
	}

	//Register callbacks for GORM, while also passing in config Opts
	otgorm.RegisterCallbacks(db,
		otgorm.WithTracer(global.TraceProvider().Tracer("component-gorm")),
		otgorm.Query(true),
		otgorm.AllowRoot(true),
	)

	//Run migration and create a record
	db.AutoMigrate(user{})
	newUser := user{
		FirstName: "John",
		LastName:  "Smith",
	}
	//Since this first DB call is outside of a parent,
	//lets set up empty context and the DB client with that context
	ctx := context.Background()
	orm := otgorm.WithContext(ctx, db)
	err = orm.Create(&newUser).Error
	if err != nil {
		log.Print(err)
	}

	//Create router
	r := chi.NewRouter()

	//Register Endpoints for the API
	r.Post("/user", func(w http.ResponseWriter, r *http.Request) {
		orm := otgorm.WithContext(r.Context(), db)
		var u user
		body, err := readBody(r.Body)
		if err != nil {
			log.Print(err)
			return
		}
		err = json.Unmarshal(body, &u)
		if err != nil {
			log.Print(err)
			return
		}
		err = orm.Create(&u).Error
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			json.NewEncoder(w).Encode("{\"Error\":\"" + err.Error() + "\"")
			return
		}
		w.WriteHeader(http.StatusOK)
		return
	})
	http.ListenAndServe(":3000", httpTraceWrapper(r))
}

License

The MIT License (MIT). Please see License File for more information.

Documentation

Overview

Package otgorm allows for the wrapping of GORM calls to databases with OpenTelemetry tracing spans. You only need to create your GORM db client and pass that into otgorm.WithContext along with context.Context(). If there is a parent span referenced within the context the GORM call will be a child span.

Index

Constants

View Source
const (
	TableKey = attribute.Key("gorm.table") //The table the GORM query is acting upon
	QueryKey = attribute.Key("gorm.query") //The GORM query itself
)

Attributes that may or may not be added to a span based on Options used

Variables

View Source
var LogFormatter = func(values ...interface{}) string {
	var (
		sql             string
		formattedValues []string
	)

	for _, value := range values[1].([]interface{}) {
		indirectValue := reflect.Indirect(reflect.ValueOf(value))
		if indirectValue.IsValid() {
			value = indirectValue.Interface()
			if t, ok := value.(time.Time); ok {
				if t.IsZero() {
					formattedValues = append(formattedValues, fmt.Sprintf("'%v'", "0000-00-00 00:00:00"))
				} else {
					formattedValues = append(formattedValues, fmt.Sprintf("'%v'", t.Format("2006-01-02 15:04:05")))
				}
			} else if b, ok := value.([]byte); ok {
				if str := string(b); isPrintable(str) {
					formattedValues = append(formattedValues, fmt.Sprintf("'%v'", str))
				} else {
					formattedValues = append(formattedValues, "'<binary>'")
				}
			} else if r, ok := value.(driver.Valuer); ok {
				if value, err := r.Value(); err == nil && value != nil {
					formattedValues = append(formattedValues, fmt.Sprintf("'%v'", value))
				} else {
					formattedValues = append(formattedValues, "NULL")
				}
			} else {
				switch value.(type) {
				case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool:
					formattedValues = append(formattedValues, fmt.Sprintf("%v", value))
				default:
					formattedValues = append(formattedValues, fmt.Sprintf("'%v'", value))
				}
			}
		} else {
			formattedValues = append(formattedValues, "NULL")
		}
	}

	if numericPlaceHolderRegexp.MatchString(values[0].(string)) {
		sql = values[0].(string)
		for index, value := range formattedValues {
			placeholder := fmt.Sprintf(`\$%d([^\d]|$)`, index+1)
			sql = regexp.MustCompile(placeholder).ReplaceAllString(sql, value+"$1")
		}
	} else {
		formattedValuesLength := len(formattedValues)
		for index, value := range sqlRegexp.Split(values[0].(string), -1) {
			sql += value
			if index < formattedValuesLength {
				sql += formattedValues[index]
			}
		}
	}

	return sql
}

Functions

func RegisterCallbacks

func RegisterCallbacks(db *gorm.DB, opts ...Option)

RegisterCallbacks registers the necessary callbacks in Gorm's hook system for instrumentation with OpenTelemetry Spans.

func WithContext

func WithContext(ctx context.Context, db *gorm.DB) *gorm.DB

WithContext sets the current context in the db instance for instrumentation.

Types

type AllowRoot

type AllowRoot bool

AllowRoot allows creating root spans in the absence of existing spans.

type DefaultAttributes

type DefaultAttributes []attribute.KeyValue

DefaultAttributes sets attributes to each span.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option allows for managing otgorm configuration using functional options.

type OptionFunc

type OptionFunc func(c *callbacks)

OptionFunc converts a regular function to an Option if it's definition is compatible.

func WithSpanOptions

func WithSpanOptions(opts ...trace.SpanOption) OptionFunc

WithSpanOptions configures the db callback functions with an additional set of trace.StartOptions which will be applied to each new span

func WithTracer

func WithTracer(tracer trace.Tracer) OptionFunc

WithTracer configures the tracer to use when starting spans. Otherwise the global tracer is used with a default name

type Query

type Query bool

Query allows recording the sql queries in spans.

type Table

type Table bool

Table allows for recording the table affected by sql queries in spans

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL