summaryrefslogtreecommitdiff
path: root/vendor/github.com/uptrace/bun/extra/bunotel/otel.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/uptrace/bun/extra/bunotel/otel.go')
-rw-r--r--vendor/github.com/uptrace/bun/extra/bunotel/otel.go188
1 files changed, 188 insertions, 0 deletions
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{}
+ }
+}