diff options
Diffstat (limited to 'vendor/github.com/uptrace')
14 files changed, 1301 insertions, 0 deletions
diff --git a/vendor/github.com/uptrace/bun/extra/bunotel/LICENSE b/vendor/github.com/uptrace/bun/extra/bunotel/LICENSE new file mode 100644 index 000000000..7ec81810c --- /dev/null +++ b/vendor/github.com/uptrace/bun/extra/bunotel/LICENSE @@ -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. diff --git a/vendor/github.com/uptrace/bun/extra/bunotel/README.md b/vendor/github.com/uptrace/bun/extra/bunotel/README.md new file mode 100644 index 000000000..50b3e6c48 --- /dev/null +++ b/vendor/github.com/uptrace/bun/extra/bunotel/README.md @@ -0,0 +1,3 @@ +# OpenTelemetry instrumentation for Bun + +See [example](../example/opentelemetry) for details. diff --git a/vendor/github.com/uptrace/bun/extra/bunotel/option.go b/vendor/github.com/uptrace/bun/extra/bunotel/option.go new file mode 100644 index 000000000..dc294ffa5 --- /dev/null +++ b/vendor/github.com/uptrace/bun/extra/bunotel/option.go @@ -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 + } +} diff --git a/vendor/github.com/uptrace/bun/extra/bunotel/otel.go b/vendor/github.com/uptrace/bun/extra/bunotel/otel.go new file mode 100644 index 000000000..25000307d --- /dev/null +++ b/vendor/github.com/uptrace/bun/extra/bunotel/otel.go @@ -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{} + } +} diff --git a/vendor/github.com/uptrace/bun/extra/bunotel/safe.go b/vendor/github.com/uptrace/bun/extra/bunotel/safe.go new file mode 100644 index 000000000..fab151a78 --- /dev/null +++ b/vendor/github.com/uptrace/bun/extra/bunotel/safe.go @@ -0,0 +1,11 @@ +// +build appengine + +package internal + +func bytesToString(b []byte) string { + return string(b) +} + +func stringToBytes(s string) []byte { + return []byte(s) +} diff --git a/vendor/github.com/uptrace/bun/extra/bunotel/unsafe.go b/vendor/github.com/uptrace/bun/extra/bunotel/unsafe.go new file mode 100644 index 000000000..23accd40e --- /dev/null +++ b/vendor/github.com/uptrace/bun/extra/bunotel/unsafe.go @@ -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)}, + )) +} diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/.golangci.yml b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/.golangci.yml new file mode 100644 index 000000000..65b3c9e6e --- /dev/null +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/.golangci.yml @@ -0,0 +1,5 @@ +issues: + exclude-rules: + - text: 'Drivers should implement' + linters: + - staticcheck diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/LICENSE b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/LICENSE new file mode 100644 index 000000000..83bbb00f4 --- /dev/null +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/LICENSE @@ -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. diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/README.md b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/README.md new file mode 100644 index 000000000..dbded166d --- /dev/null +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/README.md @@ -0,0 +1,118 @@ +[](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. diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/driver.go b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/driver.go new file mode 100644 index 000000000..056af3c6c --- /dev/null +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/driver.go @@ -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 +} diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/otel.go b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/otel.go new file mode 100644 index 000000000..0932e2759 --- /dev/null +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/otel.go @@ -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) + } +} diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/stmt.go b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/stmt.go new file mode 100644 index 000000000..e87a1e73f --- /dev/null +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/stmt.go @@ -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 + } +} diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/tx.go b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/tx.go new file mode 100644 index 000000000..c4bd55e13 --- /dev/null +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/tx.go @@ -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() + }) +} diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/version.go b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/version.go new file mode 100644 index 000000000..97134301d --- /dev/null +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/version.go @@ -0,0 +1,6 @@ +package otelsql + +// Version is the current release version. +func Version() string { + return "0.1.21" +} |