feat: initial tracing support (#1623)

This commit is contained in:
Dominik Süß 2023-05-09 19:19:48 +02:00 committed by GitHub
commit 6392e00653
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
472 changed files with 102600 additions and 12 deletions

24
vendor/github.com/uptrace/bun/extra/bunotel/LICENSE generated vendored Normal file
View file

@ -0,0 +1,24 @@
Copyright (c) 2021 Vladimir Mihailenco. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,3 @@
# OpenTelemetry instrumentation for Bun
See [example](../example/opentelemetry) for details.

32
vendor/github.com/uptrace/bun/extra/bunotel/option.go generated vendored Normal file
View file

@ -0,0 +1,32 @@
package bunotel
import (
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
)
type Option func(h *QueryHook)
// WithAttributes configures attributes that are used to create a span.
func WithAttributes(attrs ...attribute.KeyValue) Option {
return func(h *QueryHook) {
h.attrs = append(h.attrs, attrs...)
}
}
// WithDBName configures a db.name attribute.
func WithDBName(name string) Option {
return func(h *QueryHook) {
h.attrs = append(h.attrs, semconv.DBNameKey.String(name))
}
}
// WithFormattedQueries enables formatting of the query that is added
// as the statement attribute to the trace.
// This means that all placeholders and arguments will be filled first
// and the query will contain all information as sent to the database.
func WithFormattedQueries(format bool) Option {
return func(h *QueryHook) {
h.formatQueries = format
}
}

188
vendor/github.com/uptrace/bun/extra/bunotel/otel.go generated vendored Normal file
View file

@ -0,0 +1,188 @@
package bunotel
import (
"context"
"database/sql"
"runtime"
"strings"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric/global"
"go.opentelemetry.io/otel/metric/instrument"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
"go.opentelemetry.io/otel/trace"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/schema"
"github.com/uptrace/opentelemetry-go-extra/otelsql"
)
var (
tracer = otel.Tracer("github.com/uptrace/bun")
meter = global.Meter("github.com/uptrace/bun")
queryHistogram, _ = meter.Int64Histogram(
"go.sql.query_timing",
instrument.WithDescription("Timing of processed queries"),
instrument.WithUnit("milliseconds"),
)
)
type QueryHook struct {
attrs []attribute.KeyValue
formatQueries bool
}
var _ bun.QueryHook = (*QueryHook)(nil)
func NewQueryHook(opts ...Option) *QueryHook {
h := new(QueryHook)
for _, opt := range opts {
opt(h)
}
return h
}
func (h *QueryHook) Init(db *bun.DB) {
labels := make([]attribute.KeyValue, 0, len(h.attrs)+1)
labels = append(labels, h.attrs...)
if sys := dbSystem(db); sys.Valid() {
labels = append(labels, sys)
}
otelsql.ReportDBStatsMetrics(db.DB, otelsql.WithAttributes(labels...))
}
func (h *QueryHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) context.Context {
ctx, _ = tracer.Start(ctx, "", trace.WithSpanKind(trace.SpanKindClient))
return ctx
}
func (h *QueryHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {
operation := event.Operation()
dbOperation := semconv.DBOperationKey.String(operation)
labels := make([]attribute.KeyValue, 0, len(h.attrs)+2)
labels = append(labels, h.attrs...)
labels = append(labels, dbOperation)
if event.IQuery != nil {
if tableName := event.IQuery.GetTableName(); tableName != "" {
labels = append(labels, semconv.DBSQLTableKey.String(tableName))
}
}
queryHistogram.Record(ctx, time.Since(event.StartTime).Milliseconds(), labels...)
span := trace.SpanFromContext(ctx)
if !span.IsRecording() {
return
}
span.SetName(operation)
defer span.End()
query := h.eventQuery(event)
fn, file, line := funcFileLine("github.com/uptrace/bun")
attrs := make([]attribute.KeyValue, 0, 10)
attrs = append(attrs, h.attrs...)
attrs = append(attrs,
dbOperation,
semconv.DBStatementKey.String(query),
semconv.CodeFunctionKey.String(fn),
semconv.CodeFilepathKey.String(file),
semconv.CodeLineNumberKey.Int(line),
)
if sys := dbSystem(event.DB); sys.Valid() {
attrs = append(attrs, sys)
}
if event.Result != nil {
if n, _ := event.Result.RowsAffected(); n > 0 {
attrs = append(attrs, attribute.Int64("db.rows_affected", n))
}
}
switch event.Err {
case nil, sql.ErrNoRows, sql.ErrTxDone:
// ignore
default:
span.RecordError(event.Err)
span.SetStatus(codes.Error, event.Err.Error())
}
span.SetAttributes(attrs...)
}
func funcFileLine(pkg string) (string, string, int) {
const depth = 16
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
ff := runtime.CallersFrames(pcs[:n])
var fn, file string
var line int
for {
f, ok := ff.Next()
if !ok {
break
}
fn, file, line = f.Function, f.File, f.Line
if !strings.Contains(fn, pkg) {
break
}
}
if ind := strings.LastIndexByte(fn, '/'); ind != -1 {
fn = fn[ind+1:]
}
return fn, file, line
}
func (h *QueryHook) eventQuery(event *bun.QueryEvent) string {
const softQueryLimit = 8000
const hardQueryLimit = 16000
var query string
if h.formatQueries && len(event.Query) <= softQueryLimit {
query = event.Query
} else {
query = unformattedQuery(event)
}
if len(query) > hardQueryLimit {
query = query[:hardQueryLimit]
}
return query
}
func unformattedQuery(event *bun.QueryEvent) string {
if event.IQuery != nil {
if b, err := event.IQuery.AppendQuery(schema.NewNopFormatter(), nil); err == nil {
return bytesToString(b)
}
}
return string(event.QueryTemplate)
}
func dbSystem(db *bun.DB) attribute.KeyValue {
switch db.Dialect().Name() {
case dialect.PG:
return semconv.DBSystemPostgreSQL
case dialect.MySQL:
return semconv.DBSystemMySQL
case dialect.SQLite:
return semconv.DBSystemSqlite
case dialect.MSSQL:
return semconv.DBSystemMSSQL
default:
return attribute.KeyValue{}
}
}

11
vendor/github.com/uptrace/bun/extra/bunotel/safe.go generated vendored Normal file
View file

@ -0,0 +1,11 @@
// +build appengine
package internal
func bytesToString(b []byte) string {
return string(b)
}
func stringToBytes(s string) []byte {
return []byte(s)
}

18
vendor/github.com/uptrace/bun/extra/bunotel/unsafe.go generated vendored Normal file
View file

@ -0,0 +1,18 @@
// +build !appengine
package bunotel
import "unsafe"
func bytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func stringToBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(
&struct {
string
Cap int
}{s, len(s)},
))
}

View file

@ -0,0 +1,5 @@
issues:
exclude-rules:
- text: 'Drivers should implement'
linters:
- staticcheck

View file

@ -0,0 +1,24 @@
Copyright (c) 2020 github.com/uptrace/opentelemetry-go-extra Contributors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,118 @@
[![PkgGoDev](https://pkg.go.dev/badge/github.com/uptrace/opentelemetry-go-extra/otelsql)](https://pkg.go.dev/github.com/uptrace/opentelemetry-go-extra/otelsql)
# database/sql instrumentation for OpenTelemetry Go
[database/sql OpenTelemetry instrumentation](https://uptrace.dev/opentelemetry/instrumentations/go-database-sql.html)
records database queries (including `Tx` and `Stmt` queries) and reports `DBStats` metrics.
## Installation
```shell
go get github.com/uptrace/opentelemetry-go-extra/otelsql
```
## Usage
To instrument database/sql, you need to connect to a database using the API provided by otelsql:
| sql | otelsql |
| --------------------------- | ------------------------------- |
| `sql.Open(driverName, dsn)` | `otelsql.Open(driverName, dsn)` |
| `sql.OpenDB(connector)` | `otelsql.OpenDB(connector)` |
```go
import (
"github.com/uptrace/opentelemetry-go-extra/otelsql"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
)
db, err := otelsql.Open("sqlite", "file::memory:?cache=shared",
otelsql.WithAttributes(semconv.DBSystemSqlite),
otelsql.WithDBName("mydb"))
if err != nil {
panic(err)
}
// db is *sql.DB
```
And then use context-aware API to propagate the active span via
[context](https://uptrace.dev/opentelemetry/go-tracing.html#context):
```go
var num int
if err := db.QueryRowContext(ctx, "SELECT 42").Scan(&num); err != nil {
panic(err)
}
```
See [example](/example/) for details.
## Options
Both [otelsql.Open](https://pkg.go.dev/github.com/uptrace/opentelemetry-go-extra/otelsql#Open) and
[otelsql.OpenDB](https://pkg.go.dev/github.com/uptrace/opentelemetry-go-extra/otelsql#OpenDB) accept
the same [options](https://pkg.go.dev/github.com/uptrace/opentelemetry-go-extra/otelsql#Option):
- [WithAttributes](https://pkg.go.dev/github.com/uptrace/opentelemetry-go-extra/otelsql#WithAttributes)
configures attributes that are used to create a span.
- [WithDBName](https://pkg.go.dev/github.com/uptrace/opentelemetry-go-extra/otelsql#WithDBName)
configures a `db.name` attribute.
- [WithDBSystem](https://pkg.go.dev/github.com/uptrace/opentelemetry-go-extra/otelsql#WithDBSystem)
configures a `db.system` attribute. When possible, you should prefer using WithAttributes and
[semconv](https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.10.0), for example,
`otelsql.WithAttributes(semconv.DBSystemSqlite)`.
## sqlboiler
You can use otelsql to instrument [sqlboiler](https://github.com/volatiletech/sqlboiler) ORM:
```go
import (
"github.com/uptrace/opentelemetry-go-extra/otelsql"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
)
db, err := otelsql.Open("postgres", "dbname=fun user=abc",
otelsql.WithAttributes(semconv.DBSystemPostgreSQL))
if err != nil {
return err
}
boil.SetDB(db)
```
## GORM 1
You can use otelsql to instrument [GORM 1](https://v1.gorm.io/):
```go
import (
"github.com/jinzhu/gorm"
"github.com/uptrace/opentelemetry-go-extra/otelsql"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
)
// gormOpen is like gorm.Open, but it uses otelsql to instrument the database.
func gormOpen(driverName, dataSourceName string, opts ...otelsql.Option) (*gorm.DB, error) {
db, err := otelsql.Open(driverName, dataSourceName, opts...)
if err != nil {
return nil, err
}
return gorm.Open(driverName, db)
}
db, err := gormOpen("mysql", "user:password@/dbname",
otelsql.WithAttributes(semconv.DBSystemMySQL))
if err != nil {
panic(err)
}
```
To instrument GORM 2, use
[otelgorm](https://github.com/uptrace/opentelemetry-go-extra/tree/main/otelgorm).
## Alternatives
- https://github.com/XSAM/otelsql - different driver registration and no metrics.
- https://github.com/j2gg0s/otsql - like XSAM/otelsql but with Prometheus metrics.

View file

@ -0,0 +1,460 @@
package otelsql
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"go.opentelemetry.io/otel/trace"
)
// Open is a wrapper over sql.Open that instruments the sql.DB to record executed queries
// using OpenTelemetry API.
func Open(driverName, dsn string, opts ...Option) (*sql.DB, error) {
db, err := sql.Open(driverName, dsn)
if err != nil {
return nil, err
}
return patchDB(db, dsn, opts...)
}
func patchDB(db *sql.DB, dsn string, opts ...Option) (*sql.DB, error) {
dbDriver := db.Driver()
d := newDriver(dbDriver, opts)
if _, ok := dbDriver.(driver.DriverContext); ok {
connector, err := d.OpenConnector(dsn)
if err != nil {
return nil, err
}
return sqlOpenDB(connector, d.instrum), nil
}
return sqlOpenDB(&dsnConnector{
driver: d,
dsn: dsn,
}, d.instrum), nil
}
// OpenDB is a wrapper over sql.OpenDB that instruments the sql.DB to record executed queries
// using OpenTelemetry API.
func OpenDB(connector driver.Connector, opts ...Option) *sql.DB {
instrum := newDBInstrum(opts)
c := newConnector(connector.Driver(), connector, instrum)
return sqlOpenDB(c, instrum)
}
func sqlOpenDB(connector driver.Connector, instrum *dbInstrum) *sql.DB {
db := sql.OpenDB(connector)
ReportDBStatsMetrics(db, WithMeterProvider(instrum.meterProvider), WithAttributes(instrum.attrs...))
return db
}
type dsnConnector struct {
driver *otelDriver
dsn string
}
func (c *dsnConnector) Connect(ctx context.Context) (driver.Conn, error) {
var conn driver.Conn
err := c.driver.instrum.withSpan(ctx, "db.Connect", "",
func(ctx context.Context, span trace.Span) error {
var err error
conn, err = c.driver.Open(c.dsn)
return err
})
return conn, err
}
func (c *dsnConnector) Driver() driver.Driver {
return c.driver
}
//------------------------------------------------------------------------------
type otelDriver struct {
driver driver.Driver
driverCtx driver.DriverContext
instrum *dbInstrum
}
var _ driver.DriverContext = (*otelDriver)(nil)
func newDriver(dr driver.Driver, opts []Option) *otelDriver {
driverCtx, _ := dr.(driver.DriverContext)
d := &otelDriver{
driver: dr,
driverCtx: driverCtx,
instrum: newDBInstrum(opts),
}
return d
}
func (d *otelDriver) Open(name string) (driver.Conn, error) {
conn, err := d.driver.Open(name)
if err != nil {
return nil, err
}
return newConn(conn, d.instrum), nil
}
func (d *otelDriver) OpenConnector(dsn string) (driver.Connector, error) {
connector, err := d.driverCtx.OpenConnector(dsn)
if err != nil {
return nil, err
}
return newConnector(d, connector, d.instrum), nil
}
//------------------------------------------------------------------------------
type connector struct {
driver.Connector
driver driver.Driver
instrum *dbInstrum
}
var _ driver.Connector = (*connector)(nil)
func newConnector(d driver.Driver, c driver.Connector, instrum *dbInstrum) *connector {
return &connector{
driver: d,
Connector: c,
instrum: instrum,
}
}
func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
var conn driver.Conn
if err := c.instrum.withSpan(ctx, "db.Connect", "",
func(ctx context.Context, span trace.Span) error {
var err error
conn, err = c.Connector.Connect(ctx)
return err
}); err != nil {
return nil, err
}
return newConn(conn, c.instrum), nil
}
func (c *connector) Driver() driver.Driver {
return c.driver
}
//------------------------------------------------------------------------------
type otelConn struct {
driver.Conn
instrum *dbInstrum
ping pingFunc
exec execFunc
execCtx execCtxFunc
query queryFunc
queryCtx queryCtxFunc
prepareCtx prepareCtxFunc
beginTx beginTxFunc
resetSession resetSessionFunc
checkNamedValue checkNamedValueFunc
}
var _ driver.Conn = (*otelConn)(nil)
func newConn(conn driver.Conn, instrum *dbInstrum) *otelConn {
cn := &otelConn{
Conn: conn,
instrum: instrum,
}
cn.ping = cn.createPingFunc(conn)
cn.exec = cn.createExecFunc(conn)
cn.execCtx = cn.createExecCtxFunc(conn)
cn.query = cn.createQueryFunc(conn)
cn.queryCtx = cn.createQueryCtxFunc(conn)
cn.prepareCtx = cn.createPrepareCtxFunc(conn)
cn.beginTx = cn.createBeginTxFunc(conn)
cn.resetSession = cn.createResetSessionFunc(conn)
cn.checkNamedValue = cn.createCheckNamedValueFunc(conn)
return cn
}
var _ driver.Pinger = (*otelConn)(nil)
func (c *otelConn) Ping(ctx context.Context) error {
return c.ping(ctx)
}
type pingFunc func(ctx context.Context) error
func (c *otelConn) createPingFunc(conn driver.Conn) pingFunc {
if pinger, ok := conn.(driver.Pinger); ok {
return func(ctx context.Context) error {
return c.instrum.withSpan(ctx, "db.Ping", "",
func(ctx context.Context, span trace.Span) error {
return pinger.Ping(ctx)
})
}
}
return func(ctx context.Context) error {
return driver.ErrSkip
}
}
//------------------------------------------------------------------------------
var _ driver.Execer = (*otelConn)(nil)
func (c *otelConn) Exec(query string, args []driver.Value) (driver.Result, error) {
return c.exec(query, args)
}
type execFunc func(query string, args []driver.Value) (driver.Result, error)
func (c *otelConn) createExecFunc(conn driver.Conn) execFunc {
if execer, ok := conn.(driver.Execer); ok {
return func(query string, args []driver.Value) (driver.Result, error) {
return execer.Exec(query, args)
}
}
return func(query string, args []driver.Value) (driver.Result, error) {
return nil, driver.ErrSkip
}
}
//------------------------------------------------------------------------------
var _ driver.ExecerContext = (*otelConn)(nil)
func (c *otelConn) ExecContext(
ctx context.Context, query string, args []driver.NamedValue,
) (driver.Result, error) {
return c.execCtx(ctx, query, args)
}
type execCtxFunc func(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error)
func (c *otelConn) createExecCtxFunc(conn driver.Conn) execCtxFunc {
var fn execCtxFunc
if execer, ok := conn.(driver.ExecerContext); ok {
fn = execer.ExecContext
} else {
fn = func(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
vArgs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
return c.exec(query, vArgs)
}
}
return func(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
var res driver.Result
if err := c.instrum.withSpan(ctx, "db.Exec", query,
func(ctx context.Context, span trace.Span) error {
var err error
res, err = fn(ctx, query, args)
if err != nil {
return err
}
if span.IsRecording() {
rows, err := res.RowsAffected()
if err == nil {
span.SetAttributes(dbRowsAffected.Int64(rows))
}
}
return nil
}); err != nil {
return nil, err
}
return res, nil
}
}
//------------------------------------------------------------------------------
var _ driver.Queryer = (*otelConn)(nil)
func (c *otelConn) Query(query string, args []driver.Value) (driver.Rows, error) {
return c.query(query, args)
}
type queryFunc func(query string, args []driver.Value) (driver.Rows, error)
func (c *otelConn) createQueryFunc(conn driver.Conn) queryFunc {
if queryer, ok := c.Conn.(driver.Queryer); ok {
return func(query string, args []driver.Value) (driver.Rows, error) {
return queryer.Query(query, args)
}
}
return func(query string, args []driver.Value) (driver.Rows, error) {
return nil, driver.ErrSkip
}
}
//------------------------------------------------------------------------------
var _ driver.QueryerContext = (*otelConn)(nil)
func (c *otelConn) QueryContext(
ctx context.Context, query string, args []driver.NamedValue,
) (driver.Rows, error) {
return c.queryCtx(ctx, query, args)
}
type queryCtxFunc func(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error)
func (c *otelConn) createQueryCtxFunc(conn driver.Conn) queryCtxFunc {
var fn queryCtxFunc
if queryer, ok := c.Conn.(driver.QueryerContext); ok {
fn = queryer.QueryContext
} else {
fn = func(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
vArgs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
return c.query(query, vArgs)
}
}
return func(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
var rows driver.Rows
err := c.instrum.withSpan(ctx, "db.Query", query,
func(ctx context.Context, span trace.Span) error {
var err error
rows, err = fn(ctx, query, args)
return err
})
return rows, err
}
}
//------------------------------------------------------------------------------
var _ driver.ConnPrepareContext = (*otelConn)(nil)
func (c *otelConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
return c.prepareCtx(ctx, query)
}
type prepareCtxFunc func(ctx context.Context, query string) (driver.Stmt, error)
func (c *otelConn) createPrepareCtxFunc(conn driver.Conn) prepareCtxFunc {
var fn prepareCtxFunc
if preparer, ok := c.Conn.(driver.ConnPrepareContext); ok {
fn = preparer.PrepareContext
} else {
fn = func(ctx context.Context, query string) (driver.Stmt, error) {
return c.Conn.Prepare(query)
}
}
return func(ctx context.Context, query string) (driver.Stmt, error) {
var stmt driver.Stmt
if err := c.instrum.withSpan(ctx, "db.Prepare", query,
func(ctx context.Context, span trace.Span) error {
var err error
stmt, err = fn(ctx, query)
return err
}); err != nil {
return nil, err
}
return newStmt(stmt, query, c.instrum), nil
}
}
var _ driver.ConnBeginTx = (*otelConn)(nil)
func (c *otelConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
return c.beginTx(ctx, opts)
}
type beginTxFunc func(ctx context.Context, opts driver.TxOptions) (driver.Tx, error)
func (c *otelConn) createBeginTxFunc(conn driver.Conn) beginTxFunc {
var fn beginTxFunc
if txor, ok := conn.(driver.ConnBeginTx); ok {
fn = txor.BeginTx
} else {
fn = func(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
return conn.Begin()
}
}
return func(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
var tx driver.Tx
if err := c.instrum.withSpan(ctx, "db.Begin", "",
func(ctx context.Context, span trace.Span) error {
var err error
tx, err = fn(ctx, opts)
return err
}); err != nil {
return nil, err
}
return newTx(ctx, tx, c.instrum), nil
}
}
//------------------------------------------------------------------------------
var _ driver.SessionResetter = (*otelConn)(nil)
func (c *otelConn) ResetSession(ctx context.Context) error {
return c.resetSession(ctx)
}
type resetSessionFunc func(ctx context.Context) error
func (c *otelConn) createResetSessionFunc(conn driver.Conn) resetSessionFunc {
if resetter, ok := c.Conn.(driver.SessionResetter); ok {
return func(ctx context.Context) error {
return resetter.ResetSession(ctx)
}
}
return func(ctx context.Context) error {
return driver.ErrSkip
}
}
//------------------------------------------------------------------------------
var _ driver.NamedValueChecker = (*otelConn)(nil)
func (c *otelConn) CheckNamedValue(value *driver.NamedValue) error {
return c.checkNamedValue(value)
}
type checkNamedValueFunc func(*driver.NamedValue) error
func (c *otelConn) createCheckNamedValueFunc(conn driver.Conn) checkNamedValueFunc {
if checker, ok := c.Conn.(driver.NamedValueChecker); ok {
return func(value *driver.NamedValue) error {
return checker.CheckNamedValue(value)
}
}
return func(value *driver.NamedValue) error {
return driver.ErrSkip
}
}
//------------------------------------------------------------------------------
func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {
args := make([]driver.Value, len(named))
for n, param := range named {
if len(param.Name) > 0 {
return nil, errors.New("otelsql: driver does not support named parameters")
}
args[n] = param.Value
}
return args, nil
}

View file

@ -0,0 +1,254 @@
package otelsql
import (
"context"
"database/sql"
"database/sql/driver"
"io"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/global"
"go.opentelemetry.io/otel/metric/instrument"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
"go.opentelemetry.io/otel/trace"
)
const instrumName = "github.com/uptrace/opentelemetry-go-extra/otelsql"
var dbRowsAffected = attribute.Key("db.rows_affected")
type config struct {
tracerProvider trace.TracerProvider
tracer trace.Tracer //nolint:structcheck
meterProvider metric.MeterProvider
meter metric.Meter
attrs []attribute.KeyValue
queryFormatter func(query string) string
}
func newConfig(opts []Option) *config {
c := &config{
tracerProvider: otel.GetTracerProvider(),
meterProvider: global.MeterProvider(),
}
for _, opt := range opts {
opt(c)
}
return c
}
func (c *config) formatQuery(query string) string {
if c.queryFormatter != nil {
return c.queryFormatter(query)
}
return query
}
type dbInstrum struct {
*config
queryHistogram instrument.Int64Histogram
}
func newDBInstrum(opts []Option) *dbInstrum {
t := &dbInstrum{
config: newConfig(opts),
}
if t.tracer == nil {
t.tracer = t.tracerProvider.Tracer(instrumName)
}
if t.meter == nil {
t.meter = t.meterProvider.Meter(instrumName)
}
var err error
t.queryHistogram, err = t.meter.Int64Histogram(
"go.sql.query_timing",
instrument.WithDescription("Timing of processed queries"),
instrument.WithUnit("milliseconds"),
)
if err != nil {
panic(err)
}
return t
}
func (t *dbInstrum) withSpan(
ctx context.Context,
spanName string,
query string,
fn func(ctx context.Context, span trace.Span) error,
) error {
var startTime time.Time
if query != "" {
startTime = time.Now()
}
attrs := make([]attribute.KeyValue, 0, len(t.attrs)+1)
attrs = append(attrs, t.attrs...)
if query != "" {
attrs = append(attrs, semconv.DBStatementKey.String(t.formatQuery(query)))
}
ctx, span := t.tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(attrs...))
err := fn(ctx, span)
span.End()
if query != "" {
t.queryHistogram.Record(ctx, time.Since(startTime).Milliseconds(), t.attrs...)
}
if !span.IsRecording() {
return err
}
switch err {
case nil,
driver.ErrSkip,
io.EOF, // end of rows iterator
sql.ErrNoRows:
// ignore
default:
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
return err
}
type Option func(c *config)
// WithTracerProvider configures a tracer provider that is used to create a tracer.
func WithTracerProvider(tracerProvider trace.TracerProvider) Option {
return func(c *config) {
c.tracerProvider = tracerProvider
}
}
// WithAttributes configures attributes that are used to create a span.
func WithAttributes(attrs ...attribute.KeyValue) Option {
return func(c *config) {
c.attrs = append(c.attrs, attrs...)
}
}
// WithDBSystem configures a db.system attribute. You should prefer using
// WithAttributes and semconv, for example, `otelsql.WithAttributes(semconv.DBSystemSqlite)`.
func WithDBSystem(system string) Option {
return func(c *config) {
c.attrs = append(c.attrs, semconv.DBSystemKey.String(system))
}
}
// WithDBName configures a db.name attribute.
func WithDBName(name string) Option {
return func(c *config) {
c.attrs = append(c.attrs, semconv.DBNameKey.String(name))
}
}
// WithMeterProvider configures a metric.Meter used to create instruments.
func WithMeterProvider(meterProvider metric.MeterProvider) Option {
return func(c *config) {
c.meterProvider = meterProvider
}
}
// WithQueryFormatter configures a query formatter
func WithQueryFormatter(queryFormatter func(query string) string) Option {
return func(c *config) {
c.queryFormatter = queryFormatter
}
}
// ReportDBStatsMetrics reports DBStats metrics using OpenTelemetry Metrics API.
func ReportDBStatsMetrics(db *sql.DB, opts ...Option) {
cfg := newConfig(opts)
if cfg.meter == nil {
cfg.meter = cfg.meterProvider.Meter(instrumName)
}
meter := cfg.meter
labels := cfg.attrs
maxOpenConns, _ := meter.Int64ObservableGauge(
"go.sql.connections_max_open",
instrument.WithDescription("Maximum number of open connections to the database"),
)
openConns, _ := meter.Int64ObservableGauge(
"go.sql.connections_open",
instrument.WithDescription("The number of established connections both in use and idle"),
)
inUseConns, _ := meter.Int64ObservableGauge(
"go.sql.connections_in_use",
instrument.WithDescription("The number of connections currently in use"),
)
idleConns, _ := meter.Int64ObservableGauge(
"go.sql.connections_idle",
instrument.WithDescription("The number of idle connections"),
)
connsWaitCount, _ := meter.Int64ObservableCounter(
"go.sql.connections_wait_count",
instrument.WithDescription("The total number of connections waited for"),
)
connsWaitDuration, _ := meter.Int64ObservableCounter(
"go.sql.connections_wait_duration",
instrument.WithDescription("The total time blocked waiting for a new connection"),
instrument.WithUnit("nanoseconds"),
)
connsClosedMaxIdle, _ := meter.Int64ObservableCounter(
"go.sql.connections_closed_max_idle",
instrument.WithDescription("The total number of connections closed due to SetMaxIdleConns"),
)
connsClosedMaxIdleTime, _ := meter.Int64ObservableCounter(
"go.sql.connections_closed_max_idle_time",
instrument.WithDescription("The total number of connections closed due to SetConnMaxIdleTime"),
)
connsClosedMaxLifetime, _ := meter.Int64ObservableCounter(
"go.sql.connections_closed_max_lifetime",
instrument.WithDescription("The total number of connections closed due to SetConnMaxLifetime"),
)
if _, err := meter.RegisterCallback(
func(ctx context.Context, o metric.Observer) error {
stats := db.Stats()
o.ObserveInt64(maxOpenConns, int64(stats.MaxOpenConnections), labels...)
o.ObserveInt64(openConns, int64(stats.OpenConnections), labels...)
o.ObserveInt64(inUseConns, int64(stats.InUse), labels...)
o.ObserveInt64(idleConns, int64(stats.Idle), labels...)
o.ObserveInt64(connsWaitCount, stats.WaitCount, labels...)
o.ObserveInt64(connsWaitDuration, int64(stats.WaitDuration), labels...)
o.ObserveInt64(connsClosedMaxIdle, stats.MaxIdleClosed, labels...)
o.ObserveInt64(connsClosedMaxIdleTime, stats.MaxIdleTimeClosed, labels...)
o.ObserveInt64(connsClosedMaxLifetime, stats.MaxLifetimeClosed, labels...)
return nil
},
maxOpenConns,
openConns,
inUseConns,
idleConns,
connsWaitCount,
connsWaitDuration,
connsClosedMaxIdle,
connsClosedMaxIdleTime,
connsClosedMaxLifetime,
); err != nil {
panic(err)
}
}

View file

@ -0,0 +1,120 @@
package otelsql
import (
"context"
"database/sql/driver"
"go.opentelemetry.io/otel/trace"
)
type otelStmt struct {
driver.Stmt
query string
instrum *dbInstrum
execCtx stmtExecCtxFunc
queryCtx stmtQueryCtxFunc
}
var _ driver.Stmt = (*otelStmt)(nil)
func newStmt(stmt driver.Stmt, query string, instrum *dbInstrum) *otelStmt {
s := &otelStmt{
Stmt: stmt,
query: query,
instrum: instrum,
}
s.execCtx = s.createExecCtxFunc(stmt)
s.queryCtx = s.createQueryCtxFunc(stmt)
return s
}
//------------------------------------------------------------------------------
var _ driver.StmtExecContext = (*otelStmt)(nil)
func (stmt *otelStmt) ExecContext(
ctx context.Context, args []driver.NamedValue,
) (driver.Result, error) {
return stmt.execCtx(ctx, args)
}
type stmtExecCtxFunc func(ctx context.Context, args []driver.NamedValue) (driver.Result, error)
func (s *otelStmt) createExecCtxFunc(stmt driver.Stmt) stmtExecCtxFunc {
var fn stmtExecCtxFunc
if execer, ok := s.Stmt.(driver.StmtExecContext); ok {
fn = execer.ExecContext
} else {
fn = func(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
vArgs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
return stmt.Exec(vArgs)
}
}
return func(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
var res driver.Result
err := s.instrum.withSpan(ctx, "stmt.Exec", s.query,
func(ctx context.Context, span trace.Span) error {
var err error
res, err = fn(ctx, args)
if err != nil {
return err
}
if span.IsRecording() {
rows, err := res.RowsAffected()
if err == nil {
span.SetAttributes(dbRowsAffected.Int64(rows))
}
}
return nil
})
return res, err
}
}
//------------------------------------------------------------------------------
var _ driver.StmtQueryContext = (*otelStmt)(nil)
func (stmt *otelStmt) QueryContext(
ctx context.Context, args []driver.NamedValue,
) (driver.Rows, error) {
return stmt.queryCtx(ctx, args)
}
type stmtQueryCtxFunc func(ctx context.Context, args []driver.NamedValue) (driver.Rows, error)
func (s *otelStmt) createQueryCtxFunc(stmt driver.Stmt) stmtQueryCtxFunc {
var fn stmtQueryCtxFunc
if queryer, ok := s.Stmt.(driver.StmtQueryContext); ok {
fn = queryer.QueryContext
} else {
fn = func(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
vArgs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
return s.Query(vArgs)
}
}
return func(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
var rows driver.Rows
err := s.instrum.withSpan(ctx, "stmt.Query", s.query,
func(ctx context.Context, span trace.Span) error {
var err error
rows, err = fn(ctx, args)
return err
})
return rows, err
}
}

View file

@ -0,0 +1,38 @@
package otelsql
import (
"context"
"database/sql/driver"
"go.opentelemetry.io/otel/trace"
)
type otelTx struct {
ctx context.Context
tx driver.Tx
instrum *dbInstrum
}
var _ driver.Tx = (*otelTx)(nil)
func newTx(ctx context.Context, tx driver.Tx, instrum *dbInstrum) *otelTx {
return &otelTx{
ctx: ctx,
tx: tx,
instrum: instrum,
}
}
func (tx *otelTx) Commit() error {
return tx.instrum.withSpan(tx.ctx, "tx.Commit", "",
func(ctx context.Context, span trace.Span) error {
return tx.tx.Commit()
})
}
func (tx *otelTx) Rollback() error {
return tx.instrum.withSpan(tx.ctx, "tx.Rollback", "",
func(ctx context.Context, span trace.Span) error {
return tx.tx.Rollback()
})
}

View file

@ -0,0 +1,6 @@
package otelsql
// Version is the current release version.
func Version() string {
return "0.1.21"
}