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 }