Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ 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)).

## [0.41.0] - 2025-12-16

### ⚠️ Notice ⚠️
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
134 changes: 96 additions & 38 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,61 +24,119 @@ import (
)

// AttributesFromDSN returns attributes extracted from a DSN string.
// It makes the best effort to retrieve values for [semconv.ServerAddressKey] and [semconv.ServerPortKey].
// It always sets [semconv.DBSystemNameKey], falling back to [semconv.DBSystemNameOtherSQL] when
// the scheme is missing or unrecognized. 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:]
scheme, serverAddress, serverPort, dbName := parseDSN(dsn)

var attrs []attribute.KeyValue

attrs = append(attrs, dbSystemFromScheme(scheme))

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
}

// dbSystemByScheme maps lowercase DSN schemes to their [semconv.DBSystemNameKey] attribute.
var dbSystemByScheme = map[string]attribute.KeyValue{
Comment thread
cyrille-leclerc marked this conversation as resolved.
Outdated
"mysql": semconv.DBSystemNameMySQL,
"postgres": semconv.DBSystemNamePostgreSQL,
"postgresql": semconv.DBSystemNamePostgreSQL,
"sqlserver": semconv.DBSystemNameMicrosoftSQLServer,
"mssql": semconv.DBSystemNameMicrosoftSQLServer,
"oracle": semconv.DBSystemNameOracleDB,
"oracle+cx_oracle": semconv.DBSystemNameOracleDB,
"sqlite": semconv.DBSystemNameSqlite,
"sqlite3": semconv.DBSystemNameSqlite,
"mariadb": semconv.DBSystemNameMariaDB,
"cockroachdb": semconv.DBSystemNameCockroachdb,
"cockroach": semconv.DBSystemNameCockroachdb,
"cassandra": semconv.DBSystemNameCassandra,
"redis": semconv.DBSystemNameRedis,
"rediss": semconv.DBSystemNameRedis,
"mongodb": semconv.DBSystemNameMongoDB,
"mongodb+srv": semconv.DBSystemNameMongoDB,
"clickhouse": semconv.DBSystemNameClickhouse,
"trino": semconv.DBSystemNameTrino,
"hive": semconv.DBSystemNameHive,
"spanner": semconv.DBSystemNameGCPSpanner,
"elasticsearch": semconv.DBSystemNameElasticsearch,
"couchbase": semconv.DBSystemNameCouchbase,
"influxdb": semconv.DBSystemNameInfluxdb,
"dynamodb": semconv.DBSystemNameAWSDynamoDB,
"redshift": semconv.DBSystemNameAWSRedshift,
"teradata": semconv.DBSystemNameTeradata,
"firebird": semconv.DBSystemNameFirebirdsql,
"firebirdsql": semconv.DBSystemNameFirebirdsql,
"hbase": semconv.DBSystemNameHBase,
}

// dbSystemFromScheme maps a DSN scheme to the corresponding [semconv.DBSystemNameKey] attribute.
// It returns [semconv.DBSystemNameOtherSQL] if the scheme is not recognized or missing.
func dbSystemFromScheme(scheme string) attribute.KeyValue {
if v, ok := dbSystemByScheme[strings.ToLower(scheme)]; ok {
return v
}

return semconv.DBSystemNameOtherSQL
}

// 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

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

// [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 i := strings.Index(dsn, "@"); i != -1 {
dsn = dsn[i+1:]
}

// [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 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]
}

// [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]
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
}

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
}
Loading
Loading