github.com/instana/go-sensor@v1.62.2-0.20240520081010-4919868049e1/instrumentation_sql.go (about)

     1  // (c) Copyright IBM Corp. 2021
     2  // (c) Copyright Instana Inc. 2020
     3  
     4  package instana
     5  
     6  import (
     7  	"context"
     8  	"database/sql"
     9  	"database/sql/driver"
    10  	"net/url"
    11  	"regexp"
    12  	"strings"
    13  	"sync"
    14  	_ "unsafe"
    15  
    16  	ot "github.com/opentracing/opentracing-go"
    17  	"github.com/opentracing/opentracing-go/ext"
    18  )
    19  
    20  var (
    21  	sqlDriverRegistrationMu sync.Mutex
    22  )
    23  
    24  // InstrumentSQLDriver instruments provided database driver for  use with `sql.Open()`.
    25  // This method will ignore any attempt to register the driver with the same name again.
    26  //
    27  // The instrumented version is registered with `_with_instana` suffix, e.g.
    28  // if `postgres` provided as a name, the instrumented version is registered as
    29  // `postgres_with_instana`.
    30  func InstrumentSQLDriver(sensor TracerLogger, name string, driver driver.Driver) {
    31  	sqlDriverRegistrationMu.Lock()
    32  	defer sqlDriverRegistrationMu.Unlock()
    33  
    34  	instrumentedName := name + "_with_instana"
    35  
    36  	// Check if the instrumented version of a driver has already been registered
    37  	// with database/sql and ignore the second attempt to avoid panicking
    38  	for _, drv := range sql.Drivers() {
    39  		if drv == instrumentedName {
    40  			return
    41  		}
    42  	}
    43  
    44  	sql.Register(instrumentedName, &wrappedSQLDriver{
    45  		Driver: driver,
    46  		sensor: sensor,
    47  	})
    48  }
    49  
    50  // SQLOpen is a convenience wrapper for `sql.Open()` to use the instrumented version
    51  // of a driver previosly registered using `instana.InstrumentSQLDriver()`
    52  func SQLOpen(driverName, dataSourceName string) (*sql.DB, error) {
    53  
    54  	if !strings.HasSuffix(driverName, "_with_instana") {
    55  		driverName += "_with_instana"
    56  	}
    57  
    58  	return sql.Open(driverName, dataSourceName)
    59  }
    60  
    61  //go:linkname drivers database/sql.drivers
    62  var drivers map[string]driver.Driver
    63  
    64  // SQLInstrumentAndOpen returns instrumented `*sql.DB`.
    65  // It takes already registered `driver.Driver` by name, instruments it and additionally registers
    66  // it with different name. After that it returns instrumented `*sql.DB` or error if any.
    67  //
    68  // This function can be used as a convenient shortcut for InstrumentSQLDriver and SQLOpen functions.
    69  // The main difference is that this approach will use the already registered driver and using InstrumentSQLDriver
    70  // requires to explicitly provide an instance of the driver to instrument.
    71  func SQLInstrumentAndOpen(sensor TracerLogger, driverName, dataSourceName string) (*sql.DB, error) {
    72  	if d, ok := drivers[driverName]; ok {
    73  		InstrumentSQLDriver(sensor, driverName, d)
    74  	}
    75  
    76  	return SQLOpen(driverName, dataSourceName)
    77  }
    78  
    79  type wrappedSQLDriver struct {
    80  	driver.Driver
    81  
    82  	sensor TracerLogger
    83  }
    84  
    85  func (drv *wrappedSQLDriver) Open(name string) (driver.Conn, error) {
    86  	conn, err := drv.Driver.Open(name)
    87  	if err != nil {
    88  		return conn, err
    89  	}
    90  
    91  	if connAlreadyWrapped(conn) {
    92  		return conn, nil
    93  	}
    94  
    95  	w := wrapConn(ParseDBConnDetails(name), conn, drv.sensor)
    96  
    97  	return w, nil
    98  }
    99  
   100  func postgresSpan(ctx context.Context, conn DbConnDetails, query string, sensor TracerLogger) ot.Span {
   101  	tags := ot.Tags{
   102  		"pg.stmt": query,
   103  		"pg.user": conn.User,
   104  		"pg.host": conn.Host,
   105  	}
   106  
   107  	if conn.Schema != "" {
   108  		tags["pg.db"] = conn.Schema
   109  	} else {
   110  		tags["pg.db"] = conn.RawString
   111  	}
   112  
   113  	if conn.Port != "" {
   114  		tags["pg.port"] = conn.Port
   115  	}
   116  
   117  	opts := []ot.StartSpanOption{ext.SpanKindRPCClient, tags}
   118  	if parentSpan, ok := SpanFromContext(ctx); ok {
   119  		opts = append(opts, ot.ChildOf(parentSpan.Context()))
   120  	}
   121  
   122  	return sensor.StartSpan(string(PostgreSQLSpanType), opts...)
   123  }
   124  
   125  func mySQLSpan(ctx context.Context, conn DbConnDetails, query string, sensor TracerLogger) ot.Span {
   126  	tags := ot.Tags{
   127  		"mysql.stmt": query,
   128  		"mysql.user": conn.User,
   129  		"mysql.host": conn.Host,
   130  	}
   131  
   132  	if conn.Schema != "" {
   133  		tags["mysql.db"] = conn.Schema
   134  	} else {
   135  		tags["mysql.db"] = conn.RawString
   136  	}
   137  
   138  	if conn.Port != "" {
   139  		tags["mysql.port"] = conn.Port
   140  	}
   141  
   142  	opts := []ot.StartSpanOption{ext.SpanKindRPCClient, tags}
   143  	if parentSpan, ok := SpanFromContext(ctx); ok {
   144  		opts = append(opts, ot.ChildOf(parentSpan.Context()))
   145  	}
   146  
   147  	return sensor.StartSpan(string(MySQLSpanType), opts...)
   148  }
   149  
   150  var redisCmds = regexp.MustCompile(`(?i)SET|GET|DEL|INCR|DECR|APPEND|GETRANGE|SETRANGE|STRLEN|HSET|HGET|HMSET|HMGET|HDEL|HGETALL|HKEYS|HVALS|HLEN|HINCRBY|LPUSH|RPUSH|LPOP|RPOP|LLEN|LRANGE|LREM|LINDEX|LSET|SADD|SREM|SMEMBERS|SISMEMBER|SCARD|SINTER|SUNION|SDIFF|SRANDMEMBER|SPOP|ZADD|ZREM|ZRANGE|ZREVRANGE|ZRANK|ZREVRANK|ZRANGEBYSCORE|ZCARD|ZSCORE|PFADD|PFCOUNT|PFMERGE|SUBSCRIBE|UNSUBSCRIBE|PUBLISH|MULTI|EXEC|DISCARD|WATCH|UNWATCH|KEYS|EXISTS|EXPIRE|TTL|PERSIST|RENAME|RENAMENX|TYPE|SCAN|PING|INFO|CLIENT LIST|CONFIG GET|CONFIG SET|FLUSHDB|FLUSHALL|DBSIZE|SAVE|BGSAVE|BGREWRITEAOF|SHUTDOWN`)
   151  
   152  func redisSpan(ctx context.Context, conn DbConnDetails, query string, sensor TracerLogger) ot.Span {
   153  	qarr := strings.Fields(query)
   154  	var q string
   155  
   156  	for _, w := range qarr {
   157  		if redisCmds.MatchString(w) {
   158  			q += w + " "
   159  		}
   160  	}
   161  
   162  	tags := ot.Tags{
   163  		"redis.command": strings.TrimSpace(q),
   164  	}
   165  
   166  	if conn.Error != nil {
   167  		tags["redis.error"] = conn.Error.Error()
   168  	}
   169  
   170  	connection := conn.Host + ":" + conn.Port
   171  
   172  	if conn.Host == "" || conn.Port == "" {
   173  		i := strings.LastIndex(conn.RawString, "@")
   174  		connection = conn.RawString[i+1:]
   175  	}
   176  
   177  	tags["redis.connection"] = connection
   178  
   179  	opts := []ot.StartSpanOption{ext.SpanKindRPCClient, tags}
   180  	if parentSpan, ok := SpanFromContext(ctx); ok {
   181  		opts = append(opts, ot.ChildOf(parentSpan.Context()))
   182  	}
   183  
   184  	return sensor.StartSpan(string(RedisSpanType), opts...)
   185  }
   186  
   187  func couchbaseSpan(ctx context.Context, conn DbConnDetails, query string, sensor TracerLogger) ot.Span {
   188  	tags := ot.Tags{
   189  		"couchbase.hostname": conn.RawString,
   190  		"couchbase.sql":      query,
   191  	}
   192  
   193  	opts := []ot.StartSpanOption{ext.SpanKindRPCClient, tags}
   194  	if parentSpan, ok := SpanFromContext(ctx); ok {
   195  		opts = append(opts, ot.ChildOf(parentSpan.Context()))
   196  	}
   197  
   198  	return sensor.StartSpan(string(CouchbaseSpanType), opts...)
   199  }
   200  
   201  func cosmosSpan(ctx context.Context, conn DbConnDetails, query string, sensor TracerLogger) ot.Span {
   202  	tags := ot.Tags{
   203  		"cosmos.cmd": query,
   204  	}
   205  
   206  	opts := []ot.StartSpanOption{ext.SpanKindRPCClient, tags}
   207  	if parentSpan, ok := SpanFromContext(ctx); ok {
   208  		opts = append(opts, ot.ChildOf(parentSpan.Context()))
   209  	}
   210  
   211  	return sensor.StartSpan(string(CosmosSpanType), opts...)
   212  }
   213  
   214  func genericSQLSpan(ctx context.Context, conn DbConnDetails, query string, sensor TracerLogger) ot.Span {
   215  	tags := ot.Tags{
   216  		string(ext.DBType):      "sql",
   217  		string(ext.DBStatement): query,
   218  		string(ext.PeerAddress): conn.RawString,
   219  	}
   220  
   221  	if conn.Schema != "" {
   222  		tags[string(ext.DBInstance)] = conn.Schema
   223  	} else {
   224  		tags[string(ext.DBInstance)] = conn.RawString
   225  	}
   226  
   227  	if conn.Host != "" {
   228  		tags[string(ext.PeerHostname)] = conn.Host
   229  	}
   230  
   231  	if conn.Port != "" {
   232  		tags[string(ext.PeerPort)] = conn.Port
   233  	}
   234  
   235  	opts := []ot.StartSpanOption{ext.SpanKindRPCClient, tags}
   236  	if parentSpan, ok := SpanFromContext(ctx); ok {
   237  		opts = append(opts, ot.ChildOf(parentSpan.Context()))
   238  	}
   239  
   240  	return sensor.StartSpan("sdk.database", opts...)
   241  }
   242  
   243  // dbNameByQuery attempts to guess what is the database based on the query.
   244  func dbNameByQuery(q string) string {
   245  	qf := strings.Fields(q)
   246  
   247  	if len(qf) > 0 && redisCmds.MatchString(qf[0]) {
   248  		return "redis"
   249  	}
   250  
   251  	return ""
   252  }
   253  
   254  // StartSQLSpan creates a span based on DbConnDetails and a query, and attempts to detect which kind of database it belongs.
   255  // If a database is detected and it is already part of the registered spans, the span details will be specific to that
   256  // database.
   257  // Otherwise, the span will have generic database fields.
   258  func StartSQLSpan(ctx context.Context, conn DbConnDetails, query string, sensor TracerLogger) (sp ot.Span, dbKey string) {
   259  	return startSQLSpan(ctx, conn, query, sensor)
   260  }
   261  
   262  func startSQLSpan(ctx context.Context, conn DbConnDetails, query string, sensor TracerLogger) (sp ot.Span, dbKey string) {
   263  	if conn.DatabaseName == "" {
   264  		conn.DatabaseName = dbNameByQuery(query)
   265  	}
   266  
   267  	switch conn.DatabaseName {
   268  	case "postgres":
   269  		return postgresSpan(ctx, conn, query, sensor), "pg"
   270  	case "redis":
   271  		return redisSpan(ctx, conn, query, sensor), "redis"
   272  	case "mysql":
   273  		return mySQLSpan(ctx, conn, query, sensor), "mysql"
   274  	case "couchbase":
   275  		return couchbaseSpan(ctx, conn, query, sensor), "couchbase"
   276  	case "cosmos":
   277  		return cosmosSpan(ctx, conn, query, sensor), "cosmos"
   278  	}
   279  
   280  	return genericSQLSpan(ctx, conn, query, sensor), "db"
   281  }
   282  
   283  type DbConnDetails struct {
   284  	RawString    string
   285  	Host, Port   string
   286  	Schema       string
   287  	User         string
   288  	DatabaseName string
   289  	Error        error
   290  }
   291  
   292  func ParseDBConnDetails(connStr string) DbConnDetails {
   293  	strategies := [...]func(string) (DbConnDetails, bool){
   294  		parseMySQLGoSQLDriver,
   295  		parsePostgresConnDetailsKV,
   296  		parseMySQLConnDetailsKV,
   297  		parseRedisConnString,
   298  		parseDBConnDetailsURI,
   299  	}
   300  	for _, parseFn := range strategies {
   301  		if details, ok := parseFn(connStr); ok {
   302  			return details
   303  		}
   304  	}
   305  
   306  	return DbConnDetails{RawString: connStr}
   307  }
   308  
   309  // parseDBConnDetailsURI attempts to parse a connection string as an URI, assuming that it has
   310  // following format: [scheme://][user[:[password]]@]host[:port][/schema][?attribute1=value1&attribute2=value2...]
   311  func parseDBConnDetailsURI(connStr string) (DbConnDetails, bool) {
   312  	u, err := url.Parse(connStr)
   313  	if err != nil {
   314  		return DbConnDetails{}, false
   315  	}
   316  
   317  	if u.Scheme == "" {
   318  		return DbConnDetails{}, false
   319  	}
   320  
   321  	path := ""
   322  	if len(u.Path) > 1 {
   323  		path = u.Path[1:]
   324  	}
   325  
   326  	details := DbConnDetails{
   327  		RawString: connStr,
   328  		Host:      u.Hostname(),
   329  		Port:      u.Port(),
   330  		Schema:    path,
   331  	}
   332  
   333  	if u.User != nil {
   334  		details.User = u.User.Username()
   335  
   336  		// create a copy without user password
   337  		u := cloneURL(u)
   338  		u.User = url.User(details.User)
   339  		details.RawString = u.String()
   340  	}
   341  
   342  	if u.Scheme == "postgres" {
   343  		details.DatabaseName = u.Scheme
   344  	}
   345  
   346  	return details, true
   347  }
   348  
   349  var postgresKVPasswordRegex = regexp.MustCompile(`(^|\s)password=[^\s]+(\s|$)`)
   350  
   351  // parsePostgresConnDetailsKV parses a space-separated PostgreSQL-style connection string
   352  func parsePostgresConnDetailsKV(connStr string) (DbConnDetails, bool) {
   353  	var details DbConnDetails
   354  
   355  	for _, field := range strings.Split(connStr, " ") {
   356  		fieldNorm := strings.ToLower(field)
   357  
   358  		var (
   359  			prefix   string
   360  			fieldPtr *string
   361  		)
   362  		switch {
   363  		case strings.HasPrefix(fieldNorm, "host="):
   364  			if details.Host != "" {
   365  				// hostaddr= takes precedence
   366  				continue
   367  			}
   368  
   369  			prefix, fieldPtr = "host=", &details.Host
   370  		case strings.HasPrefix(fieldNorm, "hostaddr="):
   371  			prefix, fieldPtr = "hostaddr=", &details.Host
   372  		case strings.HasPrefix(fieldNorm, "port="):
   373  			prefix, fieldPtr = "port=", &details.Port
   374  		case strings.HasPrefix(fieldNorm, "user="):
   375  			prefix, fieldPtr = "user=", &details.User
   376  		case strings.HasPrefix(fieldNorm, "dbname="):
   377  			prefix, fieldPtr = "dbname=", &details.Schema
   378  		default:
   379  			continue
   380  		}
   381  
   382  		*fieldPtr = field[len(prefix):]
   383  	}
   384  
   385  	if details.Schema == "" {
   386  		return DbConnDetails{}, false
   387  	}
   388  
   389  	details.RawString = postgresKVPasswordRegex.ReplaceAllString(connStr, " ")
   390  	details.DatabaseName = "postgres"
   391  
   392  	return details, true
   393  }
   394  
   395  var mysqlKVPasswordRegex = regexp.MustCompile(`(?i)(^|;)Pwd=[^;]+(;|$)`)
   396  
   397  // parseMySQLConnDetailsKV parses a semicolon-separated MySQL-style connection string
   398  func parseMySQLConnDetailsKV(connStr string) (DbConnDetails, bool) {
   399  	details := DbConnDetails{RawString: connStr, DatabaseName: "mysql"}
   400  
   401  	for _, field := range strings.Split(connStr, ";") {
   402  		fieldNorm := strings.ToLower(field)
   403  
   404  		var (
   405  			prefix   string
   406  			fieldPtr *string
   407  		)
   408  		switch {
   409  		case strings.HasPrefix(fieldNorm, "server="):
   410  			prefix, fieldPtr = "server=", &details.Host
   411  		case strings.HasPrefix(fieldNorm, "port="):
   412  			prefix, fieldPtr = "port=", &details.Port
   413  		case strings.HasPrefix(fieldNorm, "uid="):
   414  			prefix, fieldPtr = "uid=", &details.User
   415  		case strings.HasPrefix(fieldNorm, "database="):
   416  			prefix, fieldPtr = "database=", &details.Schema
   417  		default:
   418  			continue
   419  		}
   420  
   421  		*fieldPtr = field[len(prefix):]
   422  	}
   423  
   424  	if details.Schema == "" {
   425  		return DbConnDetails{}, false
   426  	}
   427  
   428  	details.RawString = mysqlKVPasswordRegex.ReplaceAllString(connStr, ";")
   429  
   430  	return details, true
   431  }
   432  
   433  var mySQLGoDriverRe = regexp.MustCompile(`^(.*):(.*)@((.*)\((.*):([0-9]+)\))?\/(.*)$`)
   434  
   435  // parseMySQLGoSQLDriver parses the connection string from https://github.com/go-sql-driver/mysql
   436  // Format: user:password@protocol(host:port)/databasename
   437  // When protocol(host:port) is omitted, assume "tcp(localhost:3306)"
   438  func parseMySQLGoSQLDriver(connStr string) (DbConnDetails, bool) {
   439  	// Expected matches
   440  	// 0 - Entire match. eg: go:gopw@tcp(localhost:3306)/godb
   441  	// 1 - User
   442  	// 2 - password
   443  	// 3 - protocol+host+port. Eg: tcp(localhost:3306)
   444  	// 4 - protocol (if "" use tcp)
   445  	// 5 - host (if "" use localhost)
   446  	// 6 - port (if "" use 3306)
   447  	// 7 - database name
   448  	matches := mySQLGoDriverRe.FindAllStringSubmatch(connStr, -1)
   449  
   450  	if len(matches) == 0 {
   451  		return DbConnDetails{}, false
   452  	}
   453  
   454  	values := matches[0]
   455  
   456  	host := values[5]
   457  	port := values[6]
   458  
   459  	if host == "" {
   460  		host = "localhost"
   461  	}
   462  
   463  	if port == "" {
   464  		port = "3306"
   465  	}
   466  
   467  	d := DbConnDetails{
   468  		RawString:    connStr,
   469  		User:         values[1],
   470  		Host:         host,
   471  		Port:         port,
   472  		Schema:       values[7],
   473  		DatabaseName: "mysql",
   474  	}
   475  
   476  	return d, true
   477  }
   478  
   479  var redisOptionalUser = regexp.MustCompile(`^(.*:\/\/)?(.+)?:.+@(.+):(\d+)`)
   480  
   481  // parseRedisConnString attempts to parse: user:password@host:port
   482  // Based on conn string from github.com/bonede/go-redis-driver
   483  func parseRedisConnString(connStr string) (DbConnDetails, bool) {
   484  	// Expected matches
   485  	// 0 - mysql://user:password@localhost:9898 or db://user:password@localhost:9898 and so on
   486  	// 1 - mysql:// or db:// and so on
   487  	// 2 - user
   488  	// 3 - localhost
   489  	// 4 - 1234
   490  	matches := redisOptionalUser.FindAllStringSubmatch(connStr, -1)
   491  
   492  	var d = DbConnDetails{}
   493  
   494  	if len(matches) == 0 {
   495  		return d, false
   496  	}
   497  
   498  	// We want to ignore the first match. for instance db:// or mysql:// will be ignored if matched
   499  	if matches[0][1] == "" {
   500  		return DbConnDetails{
   501  				Host:         matches[0][3],
   502  				Port:         matches[0][4],
   503  				DatabaseName: "redis",
   504  				RawString:    connStr,
   505  			},
   506  			true
   507  	}
   508  
   509  	return d, false
   510  }
   511  
   512  type dsnConnector struct {
   513  	dsn    string
   514  	driver driver.Driver
   515  }
   516  
   517  func (t dsnConnector) Connect(_ context.Context) (driver.Conn, error) {
   518  	return t.driver.Open(t.dsn)
   519  }
   520  
   521  func (t dsnConnector) Driver() driver.Driver {
   522  	return t.driver
   523  }