Repository: open-telemetry/opentelemetry-go-contrib (go)

Issue:
Instrumentation should assume `OTEL_SEMCONV_STABILITY_OPT_IN` has a list of values
### Description

Current implementation often assumes `OTEL_SEMCONV_STABILITY_OPT_IN` has one value. For instance, in HTTP shared `semconv` library.

https://github.com/open-telemetry/opentelemetry-go-contrib/blob/d74c304f15dbe5d1e5f8bdfb51222277c7c9f0e4/internal/shared/semconv/env.go.tmpl#L175-L179

This only works when users want to change the behavior for one category of semconv, like setting it for `http` only. If users want to set up multiple categories of semconv, like `http,database`, the current implementation will break.

https://opentelemetry.io/docs/specs/semconv/http/

> SHOULD introduce an environment variable OTEL_SEMCONV_STABILITY_OPT_IN in the existing major version which is **a comma-separated list of values**.

### Affected packages

- All HTTP related packages that use `internal/shared/semconv/env.go.tmpl` @open-telemetry/go-maintainers 
- otelmongo @prestonvasquez

### Expected behavior

Implementation should assume that they would receive a list of values from `OTEL_SEMCONV_STABILITY_OPT_IN`, like `http,database`.


Pull request description:
feat: Support mixed categories for semconv opt-in
Resolves #7246 

Introducing support for category-specific values in the OTEL_SEMCONV_STABILITY_OPT_IN environment variable for otelhttp and otelmongo. For example, multiple categories should be supported without error:

```
export OTEL_SEMCONV_STABILITY_OPT_IN=http,database,messaging/dup
```



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e4b1f60b..13ac7e45b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -68,6 +68,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 - Removed a duplicate instance of the `Changed` subheader under `1.18.0/0.43.0/0.12.0` in `./CHANGELOG.md`. (#7163)
 - Check for TLS related options to be set before creating TLS config `go.opentelemetry.io/contrib/otelconf`. (#6984)
 - Fixed handling of the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable in `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin`. (#7215)
+- Support mixed categories for `OTEL_SEMCONV_STABILITY_OPT_IN` opt-in in the following packages. (#7246)
+  - `go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful`.
+  - `go.opentelemetry.io/contrib/instrumentation/gin-gonic/gin/otelgin`.
+  - `go.opentelemetry.io/contrib/instrumentation/gorilla/mux/otelmux`.
+  - `go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo`.
+  - `go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace`.
+  - `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`.
 
 ### Removed
 
diff --git a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/env.go b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/env.go
index 7d45b2dd0..798543baf 100644
--- a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/env.go
+++ b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/env.go
@@ -171,9 +171,20 @@ func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
 	}
 }
 
+// hasOptIn returns true if the comma-separated version string contains the
+// exact optIn value.
+func hasOptIn(version, optIn string) bool {
+	for _, v := range strings.Split(version, ",") {
+		if strings.TrimSpace(v) == optIn {
+			return true
+		}
+	}
+	return false
+}
+
 func NewHTTPServer(meter metric.Meter) HTTPServer {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	server := HTTPServer{
 		duplicate: duplicate,
 	}
@@ -199,7 +210,7 @@ type HTTPClient struct {
 
 func NewHTTPClient(meter metric.Meter) HTTPClient {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	client := HTTPClient{
 		duplicate: duplicate,
 	}
diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconv/env.go b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconv/env.go
index dbb3716e6..294c70186 100644
--- a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconv/env.go
+++ b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconv/env.go
@@ -171,9 +171,20 @@ func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
 	}
 }
 
+// hasOptIn returns true if the comma-separated version string contains the
+// exact optIn value.
+func hasOptIn(version, optIn string) bool {
+	for _, v := range strings.Split(version, ",") {
+		if strings.TrimSpace(v) == optIn {
+			return true
+		}
+	}
+	return false
+}
+
 func NewHTTPServer(meter metric.Meter) HTTPServer {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	server := HTTPServer{
 		duplicate: duplicate,
 	}
@@ -199,7 +210,7 @@ type HTTPClient struct {
 
 func NewHTTPClient(meter metric.Meter) HTTPClient {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	client := HTTPClient{
 		duplicate: duplicate,
 	}
diff --git a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv/env.go b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv/env.go
index a6a26969e..f17915644 100644
--- a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv/env.go
+++ b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv/env.go
@@ -171,9 +171,20 @@ func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
 	}
 }
 
+// hasOptIn returns true if the comma-separated version string contains the
+// exact optIn value.
+func hasOptIn(version, optIn string) bool {
+	for _, v := range strings.Split(version, ",") {
+		if strings.TrimSpace(v) == optIn {
+			return true
+		}
+	}
+	return false
+}
+
 func NewHTTPServer(meter metric.Meter) HTTPServer {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	server := HTTPServer{
 		duplicate: duplicate,
 	}
@@ -199,7 +210,7 @@ type HTTPClient struct {
 
 func NewHTTPClient(meter metric.Meter) HTTPClient {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	client := HTTPClient{
 		duplicate: duplicate,
 	}
diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv/event_monitor.go b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv/event_monitor.go
index 4d29634a7..37fc79b38 100644
--- a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv/event_monitor.go
+++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv/event_monitor.go
@@ -65,20 +65,38 @@ func WithCommandAttributeDisabled(disabled bool) AttributeOption {
 	}
 }
 
+// hasOptIn returns true if the comma-separated version string contains the
+// exact optIn value.
+func hasOptIn(version, optIn string) bool {
+	for _, v := range strings.Split(version, ",") {
+		if strings.TrimSpace(v) == optIn {
+			return true
+		}
+	}
+	return false
+}
+
 // CommandStartedTraceAttrs generates trace attributes for a CommandStartedEvent
 // based on the EventMonitor version.
 func (m EventMonitor) CommandStartedTraceAttrs(
 	evt *event.CommandStartedEvent,
 	opts ...AttributeOption,
 ) []attribute.KeyValue {
-	switch m.version {
-	case semconvOptIn1260:
+	// Dup implies both v1.26.0 and v1.21.0
+	if hasOptIn(m.version, semconvOptInDup) {
+		return append(
+			commandStartedTraceAttrsV1260(evt, opts...),
+			commandStartedTraceAttrsV1210(evt, opts...)...,
+		)
+	}
+
+	// Check for the 1.26.0 opt-in
+	if hasOptIn(m.version, semconvOptIn1260) {
 		return commandStartedTraceAttrsV1260(evt, opts...)
-	case semconvOptInDup:
-		return append(commandStartedTraceAttrsV1260(evt, opts...), commandStartedTraceAttrsV1210(evt, opts...)...)
-	default:
-		return commandStartedTraceAttrsV1210(evt, opts...)
 	}
+
+	// Fallback to v1.21.0
+	return commandStartedTraceAttrsV1210(evt, opts...)
 }
 
 // peerInfo extracts the hostname and port from a CommandStartedEvent.
diff --git a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconv/env.go b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconv/env.go
index 287c3a0b5..631466ae6 100644
--- a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconv/env.go
+++ b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconv/env.go
@@ -171,9 +171,20 @@ func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
 	}
 }
 
+// hasOptIn returns true if the comma-separated version string contains the
+// exact optIn value.
+func hasOptIn(version, optIn string) bool {
+	for _, v := range strings.Split(version, ",") {
+		if strings.TrimSpace(v) == optIn {
+			return true
+		}
+	}
+	return false
+}
+
 func NewHTTPServer(meter metric.Meter) HTTPServer {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	server := HTTPServer{
 		duplicate: duplicate,
 	}
@@ -199,7 +210,7 @@ type HTTPClient struct {
 
 func NewHTTPClient(meter metric.Meter) HTTPClient {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	client := HTTPClient{
 		duplicate: duplicate,
 	}
diff --git a/instrumentation/net/http/otelhttp/internal/semconv/env.go b/instrumentation/net/http/otelhttp/internal/semconv/env.go
index 938626519..7cb9693d9 100644
--- a/instrumentation/net/http/otelhttp/internal/semconv/env.go
+++ b/instrumentation/net/http/otelhttp/internal/semconv/env.go
@@ -171,9 +171,20 @@ func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
 	}
 }
 
+// hasOptIn returns true if the comma-separated version string contains the
+// exact optIn value.
+func hasOptIn(version, optIn string) bool {
+	for _, v := range strings.Split(version, ",") {
+		if strings.TrimSpace(v) == optIn {
+			return true
+		}
+	}
+	return false
+}
+
 func NewHTTPServer(meter metric.Meter) HTTPServer {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	server := HTTPServer{
 		duplicate: duplicate,
 	}
@@ -199,7 +210,7 @@ type HTTPClient struct {
 
 func NewHTTPClient(meter metric.Meter) HTTPClient {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	client := HTTPClient{
 		duplicate: duplicate,
 	}
diff --git a/internal/shared/semconv/env.go.tmpl b/internal/shared/semconv/env.go.tmpl
index e93fb7845..8f358aeca 100644
--- a/internal/shared/semconv/env.go.tmpl
+++ b/internal/shared/semconv/env.go.tmpl
@@ -171,9 +171,20 @@ func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
 	}
 }
 
+// hasOptIn returns true if the comma-separated version string contains the
+// exact optIn value.
+func hasOptIn(version, optIn string) bool {
+	for _, v := range strings.Split(version, ",") {
+		if strings.TrimSpace(v) == optIn {
+			return true
+		}
+	}
+	return false
+}
+
 func NewHTTPServer(meter metric.Meter) HTTPServer {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	server := HTTPServer{
 		duplicate: duplicate,
 	}
@@ -199,7 +210,7 @@ type HTTPClient struct {
 
 func NewHTTPClient(meter metric.Meter) HTTPClient {
 	env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
-	duplicate := env == "http/dup"
+	duplicate := hasOptIn(env, "http/dup")
 	client := HTTPClient{
 		duplicate: duplicate,
 	}


Test patch:
diff --git a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/env_test.go b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/env_test.go
index 63fa7576a..ca746f018 100644
--- a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/env_test.go
+++ b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/env_test.go
@@ -76,6 +76,16 @@ func TestServerNetworkTransportAttr(t *testing.T) {
 			optinVal: "http/dup",
 			network:  "tcp",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("net.transport", "ip_tcp"),
+				attribute.String("network.transport", "tcp"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+			network:  "tcp",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("net.transport", "ip_tcp"),
 				attribute.String("network.transport", "tcp"),
@@ -150,6 +160,15 @@ func TestHTTPClientTraceAttributes(t *testing.T) {
 			name:     "with optin set to duplicate",
 			optinVal: "http/dup",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
@@ -187,6 +206,16 @@ func TestClientTraceAttributes(t *testing.T) {
 			optinVal: "http/dup",
 			host:     "example.com",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup",
+			host:     "example.com",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconv/env_test.go b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconv/env_test.go
index 63fa7576a..ca746f018 100644
--- a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconv/env_test.go
+++ b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconv/env_test.go
@@ -76,6 +76,16 @@ func TestServerNetworkTransportAttr(t *testing.T) {
 			optinVal: "http/dup",
 			network:  "tcp",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("net.transport", "ip_tcp"),
+				attribute.String("network.transport", "tcp"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+			network:  "tcp",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("net.transport", "ip_tcp"),
 				attribute.String("network.transport", "tcp"),
@@ -150,6 +160,15 @@ func TestHTTPClientTraceAttributes(t *testing.T) {
 			name:     "with optin set to duplicate",
 			optinVal: "http/dup",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
@@ -187,6 +206,16 @@ func TestClientTraceAttributes(t *testing.T) {
 			optinVal: "http/dup",
 			host:     "example.com",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup",
+			host:     "example.com",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
diff --git a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv/env_test.go b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv/env_test.go
index 63fa7576a..ca746f018 100644
--- a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv/env_test.go
+++ b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv/env_test.go
@@ -76,6 +76,16 @@ func TestServerNetworkTransportAttr(t *testing.T) {
 			optinVal: "http/dup",
 			network:  "tcp",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("net.transport", "ip_tcp"),
+				attribute.String("network.transport", "tcp"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+			network:  "tcp",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("net.transport", "ip_tcp"),
 				attribute.String("network.transport", "tcp"),
@@ -150,6 +160,15 @@ func TestHTTPClientTraceAttributes(t *testing.T) {
 			name:     "with optin set to duplicate",
 			optinVal: "http/dup",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
@@ -187,6 +206,16 @@ func TestClientTraceAttributes(t *testing.T) {
 			optinVal: "http/dup",
 			host:     "example.com",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup",
+			host:     "example.com",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv/event_monitor_test.go b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv/event_monitor_test.go
index 1344f3ac0..4cad353f8 100644
--- a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv/event_monitor_test.go
+++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv/event_monitor_test.go
@@ -135,6 +135,12 @@ func TestCommandStartedTraceAttrs(t *testing.T) {
 			version:   "database/dup",
 			want:      append(v1210, v1260...),
 		},
+		{
+			name:      "mixed categories",
+			initAttrs: []attribute.KeyValue{},
+			version:   "database/dup,http",
+			want:      append(v1210, v1260...),
+		},
 	}
 
 	for _, test := range tests {
diff --git a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconv/env_test.go b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconv/env_test.go
index 63fa7576a..ca746f018 100644
--- a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconv/env_test.go
+++ b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconv/env_test.go
@@ -76,6 +76,16 @@ func TestServerNetworkTransportAttr(t *testing.T) {
 			optinVal: "http/dup",
 			network:  "tcp",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("net.transport", "ip_tcp"),
+				attribute.String("network.transport", "tcp"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+			network:  "tcp",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("net.transport", "ip_tcp"),
 				attribute.String("network.transport", "tcp"),
@@ -150,6 +160,15 @@ func TestHTTPClientTraceAttributes(t *testing.T) {
 			name:     "with optin set to duplicate",
 			optinVal: "http/dup",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
@@ -187,6 +206,16 @@ func TestClientTraceAttributes(t *testing.T) {
 			optinVal: "http/dup",
 			host:     "example.com",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup",
+			host:     "example.com",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
diff --git a/instrumentation/net/http/otelhttp/internal/semconv/env_test.go b/instrumentation/net/http/otelhttp/internal/semconv/env_test.go
index 63fa7576a..ca746f018 100644
--- a/instrumentation/net/http/otelhttp/internal/semconv/env_test.go
+++ b/instrumentation/net/http/otelhttp/internal/semconv/env_test.go
@@ -76,6 +76,16 @@ func TestServerNetworkTransportAttr(t *testing.T) {
 			optinVal: "http/dup",
 			network:  "tcp",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("net.transport", "ip_tcp"),
+				attribute.String("network.transport", "tcp"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+			network:  "tcp",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("net.transport", "ip_tcp"),
 				attribute.String("network.transport", "tcp"),
@@ -150,6 +160,15 @@ func TestHTTPClientTraceAttributes(t *testing.T) {
 			name:     "with optin set to duplicate",
 			optinVal: "http/dup",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
@@ -187,6 +206,16 @@ func TestClientTraceAttributes(t *testing.T) {
 			optinVal: "http/dup",
 			host:     "example.com",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup",
+			host:     "example.com",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
diff --git a/internal/shared/semconv/env_test.go.tmpl b/internal/shared/semconv/env_test.go.tmpl
index 63fa7576a..ca746f018 100644
--- a/internal/shared/semconv/env_test.go.tmpl
+++ b/internal/shared/semconv/env_test.go.tmpl
@@ -76,6 +76,16 @@ func TestServerNetworkTransportAttr(t *testing.T) {
 			optinVal: "http/dup",
 			network:  "tcp",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("net.transport", "ip_tcp"),
+				attribute.String("network.transport", "tcp"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+			network:  "tcp",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("net.transport", "ip_tcp"),
 				attribute.String("network.transport", "tcp"),
@@ -150,6 +160,15 @@ func TestHTTPClientTraceAttributes(t *testing.T) {
 			name:     "with optin set to duplicate",
 			optinVal: "http/dup",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup,database",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),
@@ -187,6 +206,16 @@ func TestClientTraceAttributes(t *testing.T) {
 			optinVal: "http/dup",
 			host:     "example.com",
 
+			wantAttributes: []attribute.KeyValue{
+				attribute.String("server.address", "example.com"),
+				attribute.String("net.host.name", "example.com"),
+			},
+		},
+		{
+			name:     "with mixed categories",
+			optinVal: "http/dup",
+			host:     "example.com",
+
 			wantAttributes: []attribute.KeyValue{
 				attribute.String("server.address", "example.com"),
 				attribute.String("net.host.name", "example.com"),