Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ 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))
and the database system (e.g. `mysql` or `postgresql`) and sets it as the `db.system.name` attribute
Comment thread
cyrille-leclerc marked this conversation as resolved.
Outdated

## [0.42.0] - 2026-03-30

### ⚠️ Notice ⚠️
Expand All @@ -25,6 +31,7 @@ This is the last release that will support the `OTEL_SEMCONV_STABILITY_OPT_IN` e
### Removed

- Drop support for [Go 1.24]. (#611)
>>>>>>> main
Comment thread
cyrille-leclerc marked this conversation as resolved.
Outdated
Comment thread
cyrille-leclerc marked this conversation as resolved.
Outdated

## [0.41.0] - 2025-12-16

Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,14 @@ test-coverage:
(cd "$${dir}" && \
$(GO) list ./... \
| grep -v third_party \
| grep -v internal \
| xargs $(GO) test -coverpkg=./... -covermode=$(COVERAGE_MODE) -coverprofile="$(COVERAGE_PROFILE)" && \
$(GO) tool cover -html=coverage.out -o coverage.html); \
$(GO) list ./... \
| grep -v third_party \
| grep internal \
| xargs $(GO) test -coverpkg=$$($(GO) list ./... | grep internal | tr '\n' ',') -covermode=$(COVERAGE_MODE) -coverprofile="$(COVERAGE_PROFILE).internal" && \
grep -v "^mode:" "$(COVERAGE_PROFILE).internal" >> "$(COVERAGE_PROFILE)" && \
$(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \
Comment thread
cyrille-leclerc marked this conversation as resolved.
Outdated
[ -f "$${dir}/coverage.out" ] && cat "$${dir}/coverage.out" >> coverage.txt; \
done; \
sed -i.bak -e '2,$$ { /^mode: /d; }' coverage.txt
Expand Down
92 changes: 54 additions & 38 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,61 +24,77 @@ 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 {
// [scheme://][user[:password]@][protocol([addr])]/dbname[?param1=value1&paramN=valueN]
// Find the schema part.
schemaIndex := strings.Index(dsn, "://")
if schemaIndex != -1 {
// Remove the schema part from the DSN.
dsn = dsn[schemaIndex+3:]
_, serverAddress, serverPort, dbName := parseDSN(dsn)

var attrs []attribute.KeyValue

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

// [user[:password]@][protocol([addr])]/dbname[?param1=value1&paramN=valueN]
// Find credentials part.
Comment thread
cyrille-leclerc marked this conversation as resolved.
atIndex := strings.Index(dsn, "@")
if atIndex != -1 {
// Remove the credential part from the DSN.
dsn = dsn[atIndex+1:]
if serverPort != -1 {
attrs = append(attrs, semconv.ServerPortKey.Int64(serverPort))
}

// [protocol([addr])]/dbname[?param1=value1&paramN=valueN]
// Find the '/' that separates the address part from the database part.
pathIndex := strings.Index(dsn, "/")
if pathIndex != -1 {
// Remove the path part from the DSN.
dsn = dsn[:pathIndex]
if dbName != "" {
attrs = append(attrs, semconv.DBNamespace(dbName))
}

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

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

// Extract scheme
Comment thread
cyrille-leclerc marked this conversation as resolved.
Outdated
if i := strings.Index(dsn, "://"); i != -1 {
scheme = dsn[:i]
dsn = dsn[i+3:]
}

// Skip credentials
if i := strings.Index(dsn, "@"); i != -1 {
dsn = dsn[i+1:]
}

// Extract db name
if i := strings.Index(dsn, "/"); i != -1 {
path := dsn[i+1:]
if j := strings.Index(path, "?"); j != -1 {
path = path[:j]
}

dbName = path
dsn = dsn[:i]
}

// Skip protocol
if i := strings.Index(dsn, "("); i != -1 {
dsn = dsn[i+1 : len(dsn)-1]
Comment thread
cyrille-leclerc marked this conversation as resolved.
Outdated
}

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

// Extract host and port
host, portStr, err := net.SplitHostPort(dsn)
if err != nil {
host = dsn
serverAddress = dsn
return scheme, 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 scheme, serverAddress, serverPort, dbName
}
59 changes: 55 additions & 4 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,46 +120,80 @@ 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"),
},
},
}

for _, tc := range testCases {
t.Run(tc.dsn, func(t *testing.T) {
got := AttributesFromDSN(tc.dsn)
assert.Equal(t, tc.expected, got)
gotAttrs := AttributesFromDSN(tc.dsn)
assert.Equal(t, tc.expected, gotAttrs)
})
}
}
Loading