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/uptrace/bun: initial implementation #2771

Merged
merged 6 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
83 changes: 83 additions & 0 deletions contrib/uptrace/bun/bun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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 2024 Datadog, Inc.

// Package bun provides helper functions for tracing the github.com/uptrace/bun package (https://github.com/uptrace/bun).
package bun

import (
"context"
"math"

"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"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"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry"
)

const componentName = "uptrace/bun"

func init() {
telemetry.LoadIntegration(componentName)
tracer.MarkIntegrationImported("github.com/uptrace/bun")
}

// Wrap augments the given DB with tracing.
func Wrap(db *bun.DB, opts ...Option) {
cfg := new(config)
defaults(cfg)
for _, opt := range opts {
opt(cfg)
}
log.Debug("contrib/uptrace/bun: Wrapping Database")
db.AddQueryHook(&queryHook{cfg: cfg})
}

type queryHook struct {
cfg *config
}

var _ bun.QueryHook = (*queryHook)(nil)

// BeforeQuery starts a span before a query is executed.
func (qh *queryHook) BeforeQuery(ctx context.Context, qe *bun.QueryEvent) context.Context {
var dbSystem string
switch qe.DB.Dialect().Name() {
case dialect.PG:
dbSystem = ext.DBSystemPostgreSQL
case dialect.MySQL:
dbSystem = ext.DBSystemMySQL
case dialect.MSSQL:
dbSystem = ext.DBSystemMicrosoftSQLServer
default:
dbSystem = ext.DBSystemOtherSQL
}
var (
query = qe.Query
opts = []ddtrace.StartSpanOption{
tracer.SpanType(ext.SpanTypeSQL),
tracer.ResourceName(string(query)),
tracer.ServiceName(qh.cfg.serviceName),
tracer.Tag(ext.Component, componentName),
tracer.Tag(ext.DBSystem, dbSystem),
}
)
if !math.IsNaN(qh.cfg.analyticsRate) {
opts = append(opts, tracer.Tag(ext.EventSampleRate, qh.cfg.analyticsRate))
}
_, ctx = tracer.StartSpanFromContext(ctx, "bun", opts...)
darccio marked this conversation as resolved.
Show resolved Hide resolved
return ctx
}

// AfterQuery finishes a span when a query returns.
func (qh *queryHook) AfterQuery(ctx context.Context, qe *bun.QueryEvent) {
span, ok := tracer.SpanFromContext(ctx)
if !ok {
return
}
span.Finish(tracer.WithError(qe.Err))
}
246 changes: 246 additions & 0 deletions contrib/uptrace/bun/bun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// 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 2024 Datadog, Inc.

package bun

import (
"context"
"database/sql"
"fmt"
"os"
"testing"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
_ "modernc.org/sqlite"
)

func TestMain(m *testing.M) {
_, ok := os.LookupEnv("INTEGRATION")
if !ok {
fmt.Println("--- SKIP: to enable integration test, set the INTEGRATION environment variable")
os.Exit(0)
}
os.Exit(m.Run())
}

func setupDB(opts ...Option) *bun.DB {
sqlite, err := sql.Open("sqlite", "file::memory:?cache=shared")
if err != nil {
panic(err)
}

db := bun.NewDB(sqlite, sqlitedialect.New())
Wrap(db, opts...)

return db
}

func TestImplementsHook(_ *testing.T) {
var _ bun.QueryHook = (*queryHook)(nil)
}

func TestSelect(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

db := setupDB()
parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request",
tracer.ServiceName("fake-http-server"),
tracer.SpanType(ext.SpanTypeWeb),
)

var n, rows int64
// Using WithContext will make the postgres span a child of
// the span inside ctx (parentSpan)
res, err := db.NewSelect().ColumnExpr("1").Exec(ctx, &n)
parentSpan.Finish()
spans := mt.FinishedSpans()

require.NoError(t, err)
rows, _ = res.RowsAffected()
assert.Equal(int64(1), rows)
assert.Equal(2, len(spans))
assert.Equal(nil, err)
assert.Equal(int64(1), n)
assert.Equal("bun", spans[0].OperationName())
assert.Equal("http.request", spans[1].OperationName())
assert.Equal("uptrace/bun", spans[0].Tag(ext.Component))
assert.Equal(ext.DBSystemOtherSQL, spans[0].Tag(ext.DBSystem))
}

func TestServiceName(t *testing.T) {
t.Run("default", func(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

db := setupDB()
parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request",
tracer.ServiceName("fake-http-server"),
tracer.SpanType(ext.SpanTypeWeb),
)

var n int
// Using WithContext will make the postgres span a child of
// the span inside ctx (parentSpan)
darccio marked this conversation as resolved.
Show resolved Hide resolved
res, err := db.NewSelect().ColumnExpr("1").Exec(ctx, &n)
parentSpan.Finish()
spans := mt.FinishedSpans()

require.NoError(t, err)
rows, _ := res.RowsAffected()
assert.Equal(int64(1), rows)
assert.Equal(2, len(spans))
darccio marked this conversation as resolved.
Show resolved Hide resolved
assert.Equal(nil, err)
assert.Equal(1, n)
assert.Equal("bun", spans[0].OperationName())
assert.Equal("http.request", spans[1].OperationName())
assert.Equal("bun.db", spans[0].Tag(ext.ServiceName))
assert.Equal("fake-http-server", spans[1].Tag(ext.ServiceName))
assert.Equal("uptrace/bun", spans[0].Tag(ext.Component))
assert.Equal(ext.DBSystemOtherSQL, spans[0].Tag(ext.DBSystem))
darccio marked this conversation as resolved.
Show resolved Hide resolved
})

t.Run("global", func(t *testing.T) {
globalconfig.SetServiceName("global-service")
defer globalconfig.SetServiceName("")
darccio marked this conversation as resolved.
Show resolved Hide resolved

assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

db := setupDB()
parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request",
tracer.ServiceName("fake-http-server"),
tracer.SpanType(ext.SpanTypeWeb),
)

var n int
// Using WithContext will make the postgres span a child of
// the span inside ctx (parentSpan)
res, err := db.NewSelect().ColumnExpr("1").Exec(ctx, &n)
parentSpan.Finish()
spans := mt.FinishedSpans()

require.NoError(t, err)
rows, _ := res.RowsAffected()
assert.Equal(int64(1), rows)
assert.Equal(2, len(spans))
assert.Equal(nil, err)
assert.Equal(1, n)
assert.Equal("bun", spans[0].OperationName())
assert.Equal("http.request", spans[1].OperationName())
assert.Equal("global-service", spans[0].Tag(ext.ServiceName))
assert.Equal("fake-http-server", spans[1].Tag(ext.ServiceName))
assert.Equal("uptrace/bun", spans[0].Tag(ext.Component))
assert.Equal(ext.DBSystemOtherSQL, spans[0].Tag(ext.DBSystem))
})

t.Run("custom", func(t *testing.T) {
assert := assert.New(t)
mt := mocktracer.Start()
defer mt.Stop()

db := setupDB(WithService("my-service-name"))
parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request",
tracer.ServiceName("fake-http-server"),
tracer.SpanType(ext.SpanTypeWeb),
)

var n int
// Using WithContext will make the postgres span a child of
// the span inside ctx (parentSpan)
res, err := db.NewSelect().ColumnExpr("1").Exec(ctx, &n)
parentSpan.Finish()
spans := mt.FinishedSpans()

require.NoError(t, err)
rows, _ := res.RowsAffected()
assert.Equal(int64(1), rows)
assert.Equal(2, len(spans))
assert.Equal(nil, err)
assert.Equal(1, n)
assert.Equal("bun", spans[0].OperationName())
assert.Equal("http.request", spans[1].OperationName())
assert.Equal("my-service-name", spans[0].Tag(ext.ServiceName))
assert.Equal("fake-http-server", spans[1].Tag(ext.ServiceName))
assert.Equal("uptrace/bun", spans[0].Tag(ext.Component))
assert.Equal(ext.DBSystemOtherSQL, spans[0].Tag(ext.DBSystem))
})
}

func TestAnalyticsSettings(t *testing.T) {
assertRate := func(t *testing.T, mt mocktracer.Tracer, rate interface{}, opts ...Option) {
db := setupDB(opts...)
parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request",
tracer.ServiceName("fake-http-server"),
tracer.SpanType(ext.SpanTypeWeb),
)

var n int
_, err := db.NewSelect().ColumnExpr("1").Exec(ctx, &n)
parentSpan.Finish()

require.NoError(t, err)

spans := mt.FinishedSpans()
assert.Len(t, spans, 2)
s := spans[0]
assert.Equal(t, rate, s.Tag(ext.EventSampleRate))
}

t.Run("defaults", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

assertRate(t, mt, nil)
})

t.Run("global", func(t *testing.T) {
t.Skip("global flag disabled")
mt := mocktracer.Start()
defer mt.Stop()

rate := globalconfig.AnalyticsRate()
defer globalconfig.SetAnalyticsRate(rate)
globalconfig.SetAnalyticsRate(0.4)

assertRate(t, mt, 0.4)
})

t.Run("enabled", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

assertRate(t, mt, 1.0, WithAnalytics(true))
})

t.Run("disabled", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

assertRate(t, mt, nil, WithAnalytics(false))
})

t.Run("override", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

rate := globalconfig.AnalyticsRate()
defer globalconfig.SetAnalyticsRate(rate)
globalconfig.SetAnalyticsRate(0.4)

assertRate(t, mt, 0.23, WithAnalyticsRate(0.23))
})
}
31 changes: 31 additions & 0 deletions contrib/uptrace/bun/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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 2024 Datadog, Inc.

package bun_test

import (
"context"
"database/sql"

"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
buntrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/uptrace/bun"
_ "modernc.org/sqlite"
darccio marked this conversation as resolved.
Show resolved Hide resolved
)

func Example() {
sqlite, err := sql.Open("sqlite", "file::memory:?cache=shared")
if err != nil {
panic(err)
}
db := bun.NewDB(sqlite, sqlitedialect.New())

// Wrap the connection with the APM hook.
buntrace.Wrap(db)
var user struct {
Name string
}
_ = db.NewSelect().Column("name").Table("users").Scan(context.Background(), &user)
}
Loading
Loading