2024-08-23 00:34:37 +02:00
|
|
|
package unitelsql
|
2024-07-23 17:46:15 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-07-23 17:55:58 +02:00
|
|
|
"database/sql/driver"
|
2024-07-23 17:46:15 +02:00
|
|
|
"fmt"
|
|
|
|
"strings"
|
2024-07-23 18:23:02 +02:00
|
|
|
"time"
|
2024-07-23 17:46:15 +02:00
|
|
|
|
2024-08-23 00:34:37 +02:00
|
|
|
"git.devminer.xyz/devminer/unitel"
|
|
|
|
"git.devminer.xyz/devminer/unitel/unitelutils"
|
|
|
|
"github.com/go-logr/logr"
|
2024-07-23 17:46:15 +02:00
|
|
|
"github.com/qustavo/sqlhooks/v2"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
2024-07-23 18:23:02 +02:00
|
|
|
"go.opentelemetry.io/otel/metric"
|
|
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
2024-07-23 17:46:15 +02:00
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
)
|
|
|
|
|
2024-08-10 02:00:07 +02:00
|
|
|
// TODO: Port to github.com/loghole/dbhook for proper error handling
|
|
|
|
|
2024-07-23 17:46:15 +02:00
|
|
|
const (
|
2024-08-23 00:34:37 +02:00
|
|
|
sqlClientID = unitelutils.Base + "#TracedSQL@" + unitelutils.Version
|
2024-07-23 17:46:15 +02:00
|
|
|
)
|
|
|
|
|
2024-08-23 00:34:37 +02:00
|
|
|
type dbCtxKeyT struct{}
|
|
|
|
|
|
|
|
var dbCtxKey = dbCtxKeyT{}
|
2024-07-27 19:07:11 +02:00
|
|
|
|
|
|
|
type dbCtxVal struct {
|
|
|
|
start time.Time
|
|
|
|
attrs []attribute.KeyValue
|
|
|
|
}
|
2024-07-23 18:23:02 +02:00
|
|
|
|
2024-07-28 16:56:41 +02:00
|
|
|
type tracedSQLHooks struct {
|
2024-08-23 00:34:37 +02:00
|
|
|
telemetry *unitel.Telemetry
|
|
|
|
logger logr.Logger
|
2024-07-27 19:07:11 +02:00
|
|
|
|
2024-08-23 00:34:37 +02:00
|
|
|
dbType string
|
2024-07-23 18:23:02 +02:00
|
|
|
|
|
|
|
mDuration metric.Float64Histogram
|
2024-08-23 00:34:37 +02:00
|
|
|
tracer trace.Tracer
|
|
|
|
}
|
|
|
|
|
|
|
|
type TracedSQLOpt func(t *tracedSQLHooks)
|
|
|
|
|
|
|
|
func WithLogger(l logr.Logger) TracedSQLOpt {
|
|
|
|
return func(t *tracedSQLHooks) {
|
|
|
|
t.logger = l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewTracedSQL(t *unitel.Telemetry, driver driver.Driver, dbType string, opts ...TracedSQLOpt) driver.Driver {
|
|
|
|
meter := t.MeterProvider.Meter(sqlClientID, metric.WithInstrumentationVersion(unitelutils.Version))
|
|
|
|
mDuration, err := meter.Float64Histogram(
|
|
|
|
"db.client.operation.duration",
|
|
|
|
metric.WithDescription("Database query response times"),
|
|
|
|
metric.WithUnit("ms"),
|
|
|
|
metric.WithExplicitBucketBoundaries(0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("failed to create request duration histogram")
|
|
|
|
}
|
|
|
|
|
|
|
|
traced := &tracedSQLHooks{
|
|
|
|
dbType: dbType,
|
|
|
|
|
|
|
|
telemetry: t,
|
|
|
|
|
|
|
|
mDuration: mDuration,
|
|
|
|
tracer: t.TracerProvider.Tracer(sqlClientID, trace.WithInstrumentationVersion(unitelutils.Version)),
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, o := range opts {
|
|
|
|
o(traced)
|
|
|
|
}
|
|
|
|
|
|
|
|
return sqlhooks.Wrap(driver, traced)
|
2024-07-23 17:46:15 +02:00
|
|
|
}
|
|
|
|
|
2024-07-28 16:56:41 +02:00
|
|
|
func (h *tracedSQLHooks) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
|
2024-07-23 17:46:15 +02:00
|
|
|
cleanedQuery := strings.ReplaceAll(query, "\n", " ")
|
|
|
|
cleanedQuery = strings.ReplaceAll(cleanedQuery, "\t", " ")
|
|
|
|
cleanedQuery = strings.ReplaceAll(cleanedQuery, " ", " ")
|
|
|
|
cleanedQuery = strings.TrimSpace(cleanedQuery)
|
|
|
|
|
2024-07-23 18:23:02 +02:00
|
|
|
operation := strings.Split(query, " ")[0]
|
|
|
|
|
|
|
|
attrs := []attribute.KeyValue{
|
2024-07-27 19:07:11 +02:00
|
|
|
semconv.DBSystemKey.String(h.dbType),
|
2024-07-23 18:23:02 +02:00
|
|
|
semconv.DBOperationName(operation),
|
2024-07-27 19:07:11 +02:00
|
|
|
|
2024-07-23 18:23:02 +02:00
|
|
|
/* Sentry */
|
2024-07-23 17:46:15 +02:00
|
|
|
attribute.String("db.statement", cleanedQuery),
|
|
|
|
attribute.StringSlice("db.params", formatArgs(args)),
|
2024-07-23 18:23:02 +02:00
|
|
|
|
|
|
|
/* OpenTelemetry */
|
|
|
|
attribute.String("db.query.text", cleanedQuery),
|
|
|
|
}
|
|
|
|
for i, arg := range args {
|
|
|
|
attrs = append(attrs, attribute.String(fmt.Sprintf("db.query.parameter.%d", i), formatArg(arg)))
|
|
|
|
}
|
|
|
|
|
2024-08-23 00:34:37 +02:00
|
|
|
s := h.telemetry.StartSpan(ctx, "db.sql.query", cleanedQuery, unitel.WithOtelTracer(h.tracer))
|
2024-07-23 18:23:02 +02:00
|
|
|
s.AddAttributes(attrs...)
|
2024-07-23 17:46:15 +02:00
|
|
|
|
2024-08-23 00:34:37 +02:00
|
|
|
opts := make([]any, 2*len(args))
|
|
|
|
for i, arg := range args {
|
|
|
|
opts = append(opts, fmt.Sprintf("arg%d", i), arg)
|
2024-07-23 17:46:15 +02:00
|
|
|
}
|
2024-08-23 00:34:37 +02:00
|
|
|
h.logger.Info(cleanedQuery, opts...)
|
|
|
|
|
|
|
|
s.CaptureBreadcrumb(unitel.SeverityDebug, unitel.BreadcrumbTypeQuery, "started", query, map[string]any{"args": args}).
|
|
|
|
End()
|
2024-07-23 17:46:15 +02:00
|
|
|
|
2024-07-27 19:07:11 +02:00
|
|
|
return context.WithValue(s.Context(), dbCtxKey, dbCtxVal{
|
|
|
|
start: time.Now(),
|
|
|
|
attrs: attrs,
|
|
|
|
}), nil
|
2024-07-23 17:46:15 +02:00
|
|
|
}
|
|
|
|
|
2024-07-28 16:56:41 +02:00
|
|
|
func (h *tracedSQLHooks) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
|
2024-07-27 19:07:11 +02:00
|
|
|
if val, ok := ctx.Value(dbCtxKey).(dbCtxVal); ok {
|
|
|
|
attrs := val.attrs
|
|
|
|
|
|
|
|
h.mDuration.Record(ctx, float64(time.Since(val.start).Milliseconds()), metric.WithAttributes(attrs...))
|
2024-07-23 18:23:02 +02:00
|
|
|
}
|
|
|
|
|
2024-08-23 00:34:37 +02:00
|
|
|
if s := unitel.SpanFromContext(ctx); s != nil {
|
|
|
|
s.CaptureBreadcrumb(unitel.SeverityDebug, unitel.BreadcrumbTypeQuery, "finished", query, map[string]any{"args": args}).
|
2024-08-10 02:00:07 +02:00
|
|
|
End()
|
|
|
|
}
|
|
|
|
|
2024-07-23 17:46:15 +02:00
|
|
|
return ctx, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatArgs(args []interface{}) []string {
|
|
|
|
formattedArgs := make([]string, len(args))
|
|
|
|
|
|
|
|
for i, arg := range args {
|
2024-07-23 18:23:02 +02:00
|
|
|
formattedArgs[i] = formatArg(arg)
|
2024-07-23 17:46:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return formattedArgs
|
|
|
|
}
|
2024-07-23 18:23:02 +02:00
|
|
|
|
|
|
|
func formatArg(arg interface{}) string {
|
|
|
|
switch v := arg.(type) {
|
|
|
|
case int:
|
|
|
|
return fmt.Sprint(v)
|
|
|
|
case int64:
|
|
|
|
return fmt.Sprint(v)
|
|
|
|
case float64:
|
|
|
|
return fmt.Sprint(v)
|
|
|
|
case string:
|
|
|
|
return v
|
|
|
|
case []byte:
|
|
|
|
return string(v)
|
|
|
|
case bool:
|
|
|
|
return fmt.Sprint(v)
|
|
|
|
case fmt.Stringer:
|
|
|
|
return v.String()
|
|
|
|
default:
|
|
|
|
return fmt.Sprintf("%+v", v)
|
|
|
|
}
|
|
|
|
}
|