Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
093ef23
Extract db.namespace from DSN in AttributesFromDSN
cyrille-leclerc Mar 25, 2026
6fabdca
Extract db.namespace from DSN in AttributesFromDSN
cyrille-leclerc Mar 25, 2026
e4d5e0e
Better code coverage through unit tests
cyrille-leclerc Mar 26, 2026
ae48eda
Fix test-coverage goal
cyrille-leclerc Mar 26, 2026
0115bb2
update changelog.md
cyrille-leclerc Mar 26, 2026
04afb00
Merge branch 'main' into extract-db-namespace-from-dsn-2
cyrille-leclerc Mar 31, 2026
3582ac6
Better comments
cyrille-leclerc Mar 31, 2026
dce7fbd
Remove db.system.name from AttributesFromDSN
cyrille-leclerc Mar 31, 2026
3b8b728
Better commeRemove db.system.name from AttributesFromDSNnts
cyrille-leclerc Mar 31, 2026
6d55c88
remove the `scheme` returned value + fix edge case
cyrille-leclerc Apr 2, 2026
1d35f5c
Fix CHANGELOG.md
cyrille-leclerc Apr 2, 2026
0a00b8e
Better comments
cyrille-leclerc Apr 2, 2026
6cdd26f
Revert Makefile change
cyrille-leclerc Apr 2, 2026
8a42793
Fix CHANGELOG.md
cyrille-leclerc Apr 2, 2026
70690a7
Better align with previous code
cyrille-leclerc Apr 3, 2026
7add5cc
Better align with previous code
cyrille-leclerc Apr 3, 2026
1ee7247
Better align with previous code
cyrille-leclerc Apr 3, 2026
e74e5cb
Better align with previous code
cyrille-leclerc Apr 3, 2026
55c61f5
Merge branch 'main' into extract-db-namespace-from-dsn-2
cyrille-leclerc Apr 7, 2026
7ef7c35
Add sqlserver DSN support to parseDSN and extend tests
cyrille-leclerc Apr 7, 2026
77e2718
Simplify parseDSN: use url.ParseQuery for sqlserver database param
cyrille-leclerc Apr 7, 2026
a9ff19a
restore "nolint"
cyrille-leclerc Apr 7, 2026
ae95af2
Restrict db.namespace extraction to known DSN schemes and reduce comp…
cyrille-leclerc Apr 13, 2026
05e3bde
Restore nolint:gosec on TestAttributesFromDSN
cyrille-leclerc Apr 13, 2026
1e56f4f
Update CHANGELOG and add test for malformed DSN
cyrille-leclerc Apr 13, 2026
a56b1cc
Merge branch 'main' into extract-db-namespace-from-dsn-2
cyrille-leclerc Apr 13, 2026
0232957
Preallocate attrs slice in getDummyAttributesGetter
cyrille-leclerc Apr 13, 2026
c17a842
Consolidate Fixed section in CHANGELOG [Unreleased]
cyrille-leclerc Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added

- `AttributesFromDSN` now extracts the database name from the DSN and sets it as the `db.namespace` attribute
([`semconv.DBNamespaceKey`](https://opentelemetry.io/docs/specs/semconv/database/database-spans/#common-attributes)). [#608](https://github.com/XSAM/otelsql/pull/608)

## [0.42.0] - 2026-03-30

### ⚠️ Notice ⚠️
Expand Down
62 changes: 48 additions & 14 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,34 @@ import (
)

// AttributesFromDSN returns attributes extracted from a DSN string.
// It makes the best effort to retrieve values for [semconv.ServerAddressKey] and [semconv.ServerPortKey].
// It makes the best effort to retrieve values for
// [semconv.ServerAddressKey], [semconv.ServerPortKey], and [semconv.DBNamespaceKey].
func AttributesFromDSN(dsn string) []attribute.KeyValue {
serverAddress, serverPort, dbName := parseDSN(dsn)

var attrs []attribute.KeyValue

if serverAddress != "" {
attrs = append(attrs, semconv.ServerAddress(serverAddress))
}

if serverPort != -1 {
attrs = append(attrs, semconv.ServerPortKey.Int64(serverPort))
}

if dbName != "" {
attrs = append(attrs, semconv.DBNamespace(dbName))
}

return attrs
}

// parseDSN parses a DSN string and returns the server address, server port, and database name.
// It handles the format: [scheme://][user[:password]@][protocol([addr])]/dbname[?params]
// serverAddress and dbName are empty strings if not found. serverPort is -1 if not found.
func parseDSN(dsn string) (serverAddress string, serverPort int64, dbName string) {
serverPort = -1

// [scheme://][user[:password]@][protocol([addr])]/dbname[?param1=value1&paramN=valueN]
// Find the schema part.
schemaIndex := strings.Index(dsn, "://")
Expand All @@ -47,38 +73,46 @@ func AttributesFromDSN(dsn string) []attribute.KeyValue {
pathIndex := strings.Index(dsn, "/")
if pathIndex != -1 {
// Remove the path part from the DSN.
path := dsn[pathIndex+1:]
// dbname[?param1=value1&paramN=valueN]
if questionMarkIndex := strings.Index(path, "?"); questionMarkIndex != -1 {
path = path[:questionMarkIndex]
}
// Extract db name
dbName = path
// [protocol([addr])] or [addr]
dsn = dsn[:pathIndex]
}

// [protocol([addr])] or [addr]
// Find the '(' that starts the address part.
openParen := strings.Index(dsn, "(")
if openParen != -1 {
rest := dsn[openParen+1:]
if closeParen := strings.Index(rest, ")"); closeParen != -1 {
rest = rest[:closeParen]
}
// Remove the protocol part from the DSN.
dsn = dsn[openParen+1 : len(dsn)-1]
dsn = rest
}

// [addr]
if len(dsn) == 0 {
return nil
return serverAddress, serverPort, dbName
}

// Extract host and port
host, portStr, err := net.SplitHostPort(dsn)
if err != nil {
host = dsn
serverAddress = dsn
return serverAddress, serverPort, dbName
}

attrs := make([]attribute.KeyValue, 0, 2)
if host != "" {
attrs = append(attrs, semconv.ServerAddress(host))
}
serverAddress = host

if portStr != "" {
port, err := strconv.ParseInt(portStr, 10, 64)
if err == nil {
attrs = append(attrs, semconv.ServerPortKey.Int64(port))
}
if port, err := strconv.ParseInt(portStr, 10, 64); err == nil {
serverPort = port
}

return attrs
return serverAddress, serverPort, dbName
}
62 changes: 60 additions & 2 deletions helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,60 +28,77 @@ func TestAttributesFromDSN(t *testing.T) {
dsn string
expected []attribute.KeyValue
}{
{
dsn: "mysql://root:otel_password@example.com/db",
expected: []attribute.KeyValue{
semconv.ServerAddress("example.com"),
semconv.DBNamespace("db"),
},
},
{
dsn: "mysql://root:otel_password@tcp(example.com)/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.ServerAddress("example.com"),
semconv.DBNamespace("db"),
},
},
{
dsn: "mysql://root:otel_password@tcp(example.com:3307)/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.ServerAddress("example.com"),
semconv.ServerPort(3307),
semconv.DBNamespace("db"),
},
},
{
dsn: "mysql://root:otel_password@tcp([2001:db8:1234:5678:9abc:def0:0001]:3307)/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.ServerAddress("2001:db8:1234:5678:9abc:def0:0001"),
semconv.ServerPort(3307),
semconv.DBNamespace("db"),
},
},
{
dsn: "mysql://root:otel_password@tcp(2001:db8:1234:5678:9abc:def0:0001)/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.ServerAddress("2001:db8:1234:5678:9abc:def0:0001"),
semconv.DBNamespace("db"),
},
},
{
dsn: "root:secret@tcp(mysql)/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.ServerAddress("mysql"),
semconv.DBNamespace("db"),
},
},
{
dsn: "root:secret@tcp(mysql:3307)/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.ServerAddress("mysql"),
semconv.ServerPort(3307),
semconv.DBNamespace("db"),
},
},
{
dsn: "root:secret@/db?parseTime=true",
expected: nil,
dsn: "root:secret@/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.DBNamespace("db"),
},
},
{
dsn: "example.com/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.ServerAddress("example.com"),
semconv.DBNamespace("db"),
},
},
{
dsn: "example.com:3307/db?parseTime=true",
expected: []attribute.KeyValue{
semconv.ServerAddress("example.com"),
semconv.ServerPort(3307),
semconv.DBNamespace("db"),
},
},
{
Expand All @@ -103,38 +120,79 @@ func TestAttributesFromDSN(t *testing.T) {
semconv.ServerAddress("example.com"),
},
},
{
dsn: "example.com/db",
expected: []attribute.KeyValue{
semconv.ServerAddress("example.com"),
semconv.DBNamespace("db"),
},
},
{
dsn: "postgres://root:secret@0.0.0.0:42/db?param1=value1&paramN=valueN",
expected: []attribute.KeyValue{
semconv.ServerAddress("0.0.0.0"),
semconv.ServerPort(42),
semconv.DBNamespace("db"),
},
},
{
dsn: "postgres://root:secret@2001:db8:1234:5678:9abc:def0:0001/db?param1=value1&paramN=valueN",
expected: []attribute.KeyValue{
semconv.ServerAddress("2001:db8:1234:5678:9abc:def0:0001"),
semconv.DBNamespace("db"),
},
},
{
dsn: "postgres://root:secret@[2001:db8:1234:5678:9abc:def0:0001]:42/db?param1=value1&paramN=valueN",
expected: []attribute.KeyValue{
semconv.ServerAddress("2001:db8:1234:5678:9abc:def0:0001"),
semconv.ServerPort(42),
semconv.DBNamespace("db"),
},
},
{
dsn: "root:secret@0.0.0.0:42/db?param1=value1&paramN=valueN",
expected: []attribute.KeyValue{
semconv.ServerAddress("0.0.0.0"),
semconv.ServerPort(42),
semconv.DBNamespace("db"),
},
},
{
// In this case, "tcp" will be considered as the server address.
dsn: "root:secret@tcp/db?param1=value1&paramN=valueN",
expected: []attribute.KeyValue{
semconv.ServerAddress("tcp"),
semconv.DBNamespace("db"),
},
},
{
// DSN lacking a db-name
dsn: "sqlserver://user:pass@dbhost:1433",
expected: []attribute.KeyValue{
semconv.ServerAddress("dbhost"),
semconv.ServerPort(1433),
},
},
{
// DSN lacking a db-name, with trailing '/'
dsn: "postgres://user:pass@dbhost/",
expected: []attribute.KeyValue{
semconv.ServerAddress("dbhost"),
},
},
{
dsn: "unknown://user:pass@dbhost/db",
expected: []attribute.KeyValue{
semconv.ServerAddress("dbhost"),
semconv.DBNamespace("db"),
},
},
{
// malformed DSN shouldn't fail
dsn: "root:pass@tcp(dbhost",
expected: []attribute.KeyValue{
semconv.ServerAddress("dbhost"),
},
},
}
Expand Down
Loading