Categorygo.nhat.io/otelsql
repositorypackage
0.13.0
Repository: https://github.com/nhatthm/otelsql.git
Documentation: pkg.go.dev

# Packages

No description provided by the author

# README

OpenTelemetry SQL database driver wrapper for Go

GitHub Releases Build Status codecov Go Report Card GoDevDoc Donate

Add a OpenTelemetry wrapper to your existing database code to instrument the interactions with the database. The wrapper supports both traces and metrics.

Table of Contents

Prerequisites

  • Go >= 1.21

[table of contents]

Install

⚠️ From v0.5.0, the project is rebranded to go.nhat.io/otelsql. v0.4.x is the last version with github.com/nhatthm/otelsql.

go get go.nhat.io/otelsql

Compatibility

otelsqlgo.opentelemetry.io/otel/tracego.opentelemetry.io/otel/metric
v0.13.*  v1.24.0 ~> latestv1.24.0 ~> latest
v0.12.*  v1.17.0 ~> latestv0.40.0 ~> latest
v0.11.*  v1.16.0 ~> latestv0.39.0 ~> latest
v0.10.*  v1.15.0 ~> latestv0.38.*
v0.9.*  v1.14.0 ~> latestv0.37.*
v0.8.*  v1.12.0 ~> latestv0.35.0 ~> v0.36.*
v0.7.*  v1.11.1 ~> latestv0.33.0 ~> v0.34.*
v0.6.*  v1.10.0 ~> latestv0.32.*
v0.5.*  v1.10.0 ~> latestv0.31.*
v0.4.* 1v1.9.0 ~> latestv0.31.*
v0.3.* 1v1.7.0 ~> latestv0.28.0 ~> v0.30.*
v0.2.* 1v1.6.2 ~> latestv0.28.0 ~> v0.30.*
v0.1.* 1v1.4.1 ~> latestv0.26.0 ~> v0.27.*

1 Old versions were shipped under github.com/nhatthm/otelsql. Use go get github.com/nhatthm/otelsql instead.

[table of contents]

Usage

To use otelsql with your application, register the otelsql wrapper by using otelsql.Register(driverName string, opts ...otelsql.DriverOption). For example:

package example

import (
	"database/sql"

	"go.nhat.io/otelsql"
	semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
)

func openDB(dsn string) (*sql.DB, error) {
	// Register the otelsql wrapper for the provided postgres driver.
	driverName, err := otelsql.Register("postgres",
		otelsql.AllowRoot(),
		otelsql.TraceQueryWithoutArgs(),
		otelsql.TraceRowsClose(),
		otelsql.TraceRowsAffected(),
		otelsql.WithDatabaseName("my_database"),        // Optional.
		otelsql.WithSystem(semconv.DBSystemPostgreSQL), // Optional.
	)
	if err != nil {
		return nil, err
	}

	// Connect to a Postgres database using the postgres driver wrapper.
	return sql.Open(driverName, dsn)
}

The wrapper will automatically instrument the interactions with the database.

Optionally, you could record database connection metrics using the otelsql.RecordStats(). For example:

package example

import (
	"database/sql"

	"go.nhat.io/otelsql"
	semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
)

func openDB(dsn string) (*sql.DB, error) {
	// Register the otelsql wrapper for the provided postgres driver.
	driverName, err := otelsql.Register("postgres",
		otelsql.AllowRoot(),
		otelsql.TraceQueryWithoutArgs(),
		otelsql.TraceRowsClose(),
		otelsql.TraceRowsAffected(),
		otelsql.WithDatabaseName("my_database"),        // Optional.
		otelsql.WithSystem(semconv.DBSystemPostgreSQL), // Optional.
	)
	if err != nil {
		return nil, err
	}

	// Connect to a Postgres database using the postgres driver wrapper.
	db, err := sql.Open(driverName, dsn)
	if err != nil {
		return nil, err
	}

	if err := otelsql.RecordStats(db); err != nil {
		return nil, err
	}

	return db, nil
}

[table of contents]

Options

Driver Options

OptionDescription
WithMeterProvider(metric.MeterProvider)Specify a meter provider
WithTracerProvider(trace.TracerProvider)Specify a tracer provider
WithDefaultAttributes(...attribute.KeyValue)Add extra attributes for the recorded spans and metrics
WithInstanceName(string)Add an extra attribute for annotating the instance name
WithSystem(attribute.KeyValue)Add an extra attribute for annotating the type of database server.
The value is set by using the well-known identifiers in semconv. For example: semconv.DBSystemPostgreSQL. See more
WithDatabaseName(string)Add an extra attribute for annotating the database name
WithSpanNameFormatter(spanNameFormatter)Set a custom span name formatter
ConvertErrorToSpanStatus(errorToSpanStatus)Set a custom converter for span status
DisableErrSkip()sql.ErrSkip is considered as OK in span status
TraceQuery()Set a custom function for tracing query
TraceQueryWithArgs()Trace query and all arguments
TraceQueryWithoutArgs()Trace query without the arguments
AllowRoot()Create root spans in absence of existing spans or even context
TracePing()Enable the creation of spans on Ping requests
TraceRowsNext()Enable the creation of spans on RowsNext calls. (This can result in many spans)
TraceRowsClose()Enable the creation of spans on RowsClose calls
TraceRowsAffected()Enable the creation of spans on RowsAffected calls
TraceLastInsertID()Enable the creation of spans on LastInsertId call
TraceAll()Turn on all tracing options, including AllowRoot() and TraceQueryWithArgs()

Record Stats Options

OptionDescription
WithMeterProvider(metric.MeterProvider)Specify a meter provider
WithMinimumReadDBStatsInterval(time.Duration)The minimum interval between calls to db.Stats(). Negative values are ignored.
WithDefaultAttributes(...attribute.KeyValue)Add extra attributes for the recorded metrics
WithInstanceName(string)Add an extra attribute for annotating the instance name
WithSystem(attribute.KeyValue)Add an extra attribute for annotating the type of database server.
The value is set by using the well-known identifiers in semconv. For example: semconv.DBSystemPostgreSQL. See more
WithDatabaseName(string)Add an extra attribute for annotating the database name

[table of contents]

Extras

[table of contents]

Span Name Formatter

By default, spans will be created with the sql:METHOD format, like sql:exec or sql:query. You could change this behavior by using the WithSpanNameFormatter() option and set your own logic.

For example

package example

import (
	"context"
	"database/sql"

	"go.nhat.io/otelsql"
)

func openDB(dsn string) (*sql.DB, error) {
	driverName, err := otelsql.Register("my-driver",
		otelsql.WithSpanNameFormatter(func(_ context.Context, op string) string {
			return "main-db:" + op
		}),
	)
	if err != nil {
		return nil, err
	}

	return sql.Open(driverName, dsn)
}

With traces of ExecContext() and QueryContext() (either DB, Stmt, or Tx), you could get the SQL query from the context using otelsql.QueryFromContext(). For example:

package example

import (
	"context"
	"database/sql"

	"go.nhat.io/otelsql"
)

func openDB(dsn string) (*sql.DB, error) {
	driverName, err := otelsql.Register("my-driver",
		otelsql.WithSpanNameFormatter(func(ctx context.Context, op string) string {
			if op != "exec" {
				return "main-db:" + op
			}

			query := otelsql.QueryFromContext(ctx)

			// Make span name from the query here and return.
		}),
	)
	if err != nil {
		return nil, err
	}

	return sql.Open(driverName, dsn)
}

[table of contents]

Convert Error to Span Status

By default, all errors are considered as ERROR while setting span status, except io.EOF on RowsNext calls (which is OK). otelsql also provides an extra option DisableErrSkip() if you want to ignore the sql.ErrSkip.

You can write your own conversion by using the ConvertErrorToSpanStatus() option. For example

package example

import (
	"database/sql"
	"errors"

	"go.nhat.io/otelsql"
	"go.opentelemetry.io/otel/codes"
)

func openDB(dsn string) (*sql.DB, error) {
	driverName, err := otelsql.Register("my-driver",
		otelsql.ConvertErrorToSpanStatus(func(err error) (codes.Code, string) {
			if err == nil || errors.Is(err, ignoredError) {
				return codes.Ok, ""
			}

			return codes.Error, err.Error()
		}),
	)
	if err != nil {
		return nil, err
	}

	return sql.Open(driverName, dsn)
}

[table of contents]

Trace Query

By default, otelsql does not trace query and arguments. When you use these options:

  • TraceQueryWithArgs(): Trace the query and all arguments.
  • TraceQueryWithoutArgs(): Trace only the query, without the arguments.

The traced query will be set in the semconv.DBStatementKey attribute (db.statement) and the arguments are set as follows:

  • db.sql.args.NAME: if the arguments are named.
  • db.sql.args.ORDINAL: Otherwise.

Example #1:

SELECT *
FROM data
WHERE country = :country

The argument attribute will be db.sql.args.country

Example #2:

SELECT *
FROM data
WHERE country = $1

The argument attribute will be db.sql.args.1

You can change this behavior for your own purpose (like, redaction or stripping out sensitive information) by using the TraceQuery() option. For example:

package example

import (
	"context"
	"database/sql"
	"database/sql/driver"

	"go.nhat.io/otelsql"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
)

func openDB(dsn string) (*sql.DB, error) {
	driverName, err := otelsql.Register("my-driver",
		otelsql.TraceQuery(func(_ context.Context, query string, args []driver.NamedValue) []attribute.KeyValue {
			attrs := make([]attribute.KeyValue, 0, 1+len(args))

			attrs = append(attrs, semconv.DBStatementKey.String(query))

			// Your redaction goes here.

			return attrs
		}),
	)
	if err != nil {
		return nil, err
	}

	return sql.Open(driverName, dsn)
}

[table of contents]

AllowRoot() and Span Context

To fully take advantage of otelsql, all database calls should be made using the *Context methods. Failing to do so will result in many orphaned traces if the AllowRoot() is used. By default, AllowRoot() is disabled and will result in otelsql not tracing the database calls if context or parent spans are missing.

OldNew
*DB.Begin*DB.BeginTx
*DB.Exec*DB.ExecContext
*DB.Ping*DB.PingContext
*DB.Prepare*DB.PrepareContext
*DB.Query*DB.QueryContext
*DB.QueryRow*DB.QueryRowContext
*Stmt.Exec*Stmt.ExecContext
*Stmt.Query*Stmt.QueryContext
*Stmt.QueryRow*Stmt.QueryRowContext
*Tx.Exec*Tx.ExecContext
*Tx.Prepare*Tx.PrepareContext
*Tx.Query*Tx.QueryContext
*Tx.QueryRow*Tx.QueryRowContext

[table of contents]

jmoiron/sqlx

If using the jmoiron/sqlx library with named queries you will need to use the sqlx.NewDb function to wrap an existing *sql.DB connection. Do not use the sqlx.Open and sqlx.Connect methods. jmoiron/sqlx uses the driver name to figure out which database is being used. It uses this knowledge to convert named queries to the correct bind type (dollar sign, question mark) if named queries are not supported natively by the database. Since otelsql creates a new driver name it will not be recognized by jmoiron/sqlx and named queries will fail.

For example:

package example

import (
	"database/sql"

	"github.com/jmoiron/sqlx"
	"go.nhat.io/otelsql"
)

func openDB(dsn string) (*sql.DB, error) {
	driverName, err := otelsql.Register("my-driver",
		otelsql.AllowRoot(),
		otelsql.TraceQueryWithoutArgs(),
		otelsql.TraceRowsClose(),
		otelsql.TraceRowsAffected(),
	)
	if err != nil {
		return nil, err
	}

	db, err := sql.Open(driverName, dsn)
	if err != nil {
		return nil, err
	}

	return sqlx.NewDb(db, "my-driver"), nil
}

[table of contents]

Metrics

Attributes (applies to all the metrics below)

AttributeDescriptionNote
db_operationThe executed sql methodFor example: exec, query, prepare
db_sql_statusThe execution statusOK if no error, otherwise ERROR
db_sql_errorThe error messageWhen status is ERROR. The value is the error message
db_instanceThe instance nameOnly when using WithInstanceName() option
db_systemThe system nameOnly when using WithSystem() option
db_nameThe database nameOnly when using WithDatabaseName() option

WithDefaultAttributes(attrs ...attribute.KeyValue) will also add the attrs to the recorded metrics.

[table of contents]

Client Metrics

MetricDescription
db_sql_client_calls{db_instance,db_operation,db_sql_status,db_system,db_name}Number of Calls (Counter)
db_sql_client_latency_bucket{db_instance,db_operation,db_sql_status,db_system,db_name,le}Latency in milliseconds (Histogram)
db_sql_client_latency_sum{db_instance,db_operation,db_sql_status,db_system,db_name}
db_sql_client_latency_count{db_instance,db_operation,db_sql_status,db_system,db_name}

[table of contents]

Database Connection Metrics

MetricDescription
db_sql_connections_active{db_instance,db_system,db_name}Number of active connections
db_sql_connections_idle{db_instance,db_system,db_name}Number of idle connections
db_sql_connections_idle_closed{db_instance,db_system,db_name}Total number of closed connections by SetMaxIdleConns
db_sql_connections_lifetime_closed{db_instance,db_system,db_name}Total number of closed connections by SetConnMaxLifetime
db_sql_connections_open{db_instance,db_system,db_name}Number of open connections
db_sql_connections_wait_count{db_instance,db_system,db_name}Total number of connections waited for
db_sql_connections_wait_duration{db_instance,db_system,db_name}Total time blocked waiting for new connections

[table of contents]

Traces

OperationTrace
*DB.BeginTxAlways
*DB.ExecContextAlways
*DB.PingContextDisabled. Use TracePing() to enable
*DB.PrepareContextAlways
*DB.QueryContextAlways
*DB.QueryRowContextAlways
*Stmt.ExecContextAlways
*Stmt.QueryContextAlways
*Stmt.QueryRowContextAlways
*Tx.ExecContextAlways
*Tx.PrepareContextAlways
*Tx.QueryContextAlways
*Tx.QueryRowContextAlways
*Rows.NextDisabled. Use TraceRowsNext() to enable
*Rows.CloseDisabled. Use TraceRowsClose() to enable
*Result.LastInsertIDDisabled. Use TraceLastInsertID() to enable
*Result.RowsAffectedDisabled. Use TraceRowsAffected() to enable

ExecContext, QueryContext, QueryRowContext, PrepareContext are always traced without query args unless using TraceQuery(), TraceQueryWithArgs(), or TraceQueryWithoutArgs() option.

Using WithDefaultAttributes(...attribute.KeyValue) will add extra attributes to the recorded spans.

[table of contents]

Migration from ocsql

The migration is easy because the behaviors of otelsql are the same as ocsql, and all options are almost similar.

ocsqlotelsql
Register driver wrapperRegister(driverName string, options ...TraceOption)Register(driverName string, options ...DriverOption)
Records database statisticsRecordStats(db *sql.DB, interval time.Duration)RecordStats(db *sql.DB, opts ...StatsOption)

The interval in RecordStats() is replaced with WithMinimumReadDBStatsInterval(time.Duration) option.

[table of contents]

Options

ocsqlotelsql
WithAllTraceOptions()TraceAll()
otelsql always set to true
WithOptions(ocsql.TraceOptions)Dropped
WithAllowRoot(bool)AllowRoot()
otelsql always set to true
WithPing(bool)TracePing()
otelsql always set to true
WithRowsNext(bool)TraceRowsNext()
otelsql always set to true
WithRowsClose(bool)TraceRowsClose()
otelsql always set to true
WithRowsAffected(bool)TraceRowsAffected()
otelsql always set to true
WithLastInsertID(bool)TraceLastInsertID()
otelsql always set to true
WithQuery(bool)
WithQueryParams(bool)
TraceQueryWithArgs()
TraceQueryWithoutArgs()
WithDefaultAttributes(...trace.Attribute)WithDefaultAttributes(...attribute.KeyValue)
WithDisableErrSkip(bool)DisableErrSkip()
WithSampler(trace.Sampler)Dropped
WithInstanceName(string)WithInstanceName(string)

[table of contents]

Metrics

Attributes (applies to all the metrics below)

ocsqlotelsqlNote
go_sql_instancedb_instanceOnly when using WithInstanceName() option
go_sql_methoddb_operation
go_sql_statusdb_sql_status
n/adb_systemOnly when using WithSystem() option
n/adb_nameOnly when using WithDatabaseName() option

Client Metrics

ocsqlotelsql
go_sql_client_calls{go_sql_instance,go_sql_method,go_sql_status}db_sql_client_calls{db_instance,db_operation,db_sql_status,db_system,db_name}
go_sql_client_latency_bucket{go_sql_instance,go_sql_method,go_sql_status,le}db_sql_client_latency_bucket{db_instance,db_operation,db_sql_status,db_system,db_name,le}
go_sql_client_latency_sum{go_sql_instance,go_sql_method,go_sql_status}db_sql_client_latency_sum{db_instance,db_operation,db_sql_status,db_system,db_name}
go_sql_client_latency_count{go_sql_instance,go_sql_method,go_sql_status}db_sql_client_latency_count{db_instance,db_operation,db_sql_status,db_system,db_name}

Connection Metrics

ocsqlotelsql
go_sql_db_connections_active{go_sql_instance}db_sql_connections_active{db_instance,db_system,db_name}
go_sql_db_connections_idle{go_sql_instance}db_sql_connections_idle{db_instance,db_system,db_name}
go_sql_db_connections_idle_closed_count{go_sql_instance}db_sql_connections_idle_closed{db_instance,db_system,db_name}
go_sql_db_connections_lifetime_closed_count{go_sql_instance}db_sql_connections_lifetime_closed{db_instance,db_system,db_name}
go_sql_db_connections_open{go_sql_instance}db_sql_connections_open{db_instance,db_system,db_name}
go_sql_db_connections_wait_count{go_sql_instance}db_sql_connections_wait_count{db_instance,db_system,db_name}
go_sql_db_connections_wait_duration{go_sql_instance}db_sql_connections_wait_duration{db_instance,db_system,db_name}

[table of contents]

Traces

The traces are almost identical with some minor changes:

  1. Named arguments are not just recorder as <NAME> in the span. They are now db.sql.args.<NAME>.
  2. sql.query is now db.statement.

[table of contents]

Compatibility

OS
DriverDatabaseUbuntuMacOSWindows
go 1.21go 1.22go 1.21go 1.22go 1.21go 1.22
DATA-DOG/go-sqlmock Build Status
jmoiron/sqlx Manually Tested
denisenkom/go-mssqldb SQL Server 2019 Build Status
go-sql-driver/mysql MySQL 8 Build Status
jackc/pgx/v4/stdlib Postgres 12, 13, 14, 15, 16 Build Status
jackc/pgx/v5/stdlib Postgres 12, 13, 14, 15, 16 Build Status
lib/pq Postgres 12, 13, 14, 15, 16 Build Status

If you don't see a driver in the list, it doesn't mean the wrapper is incompatible. it's just not tested. Let me know if it works with your driver

[table of contents]

Donation

If this project help you reduce time to develop, you can give me a cup of coffee :)

[table of contents]

Paypal donation

paypal

       or scan this