Skip to content

Commit 04b152d

Browse files
prestonvasquezdmathieupellared
authored
otelmongo: support OTEL_SEMCONV_STABILITY_OPT_IN for v1.26.0 semconv (#6172)
Co-authored-by: Damien Mathieu <[email protected]> Co-authored-by: Robert Pająk <[email protected]>
1 parent b991afd commit 04b152d

File tree

6 files changed

+450
-57
lines changed

6 files changed

+450
-57
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ migration](https://github.com/open-telemetry/semantic-conventions/blob/main/docs
4444

4545
- Add custom attribute to the span after execution of the SDK rather than before in `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws`. (#6543)
4646
- Support for the `OTEL_HTTP_CLIENT_COMPATIBILITY_MODE=http/dup` environment variable in `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin` to emit attributes for both the v1.20.0 and v1.26.0 semantic conventions. (#6778)
47+
- Support `OTEL_SEMCONV_STABILITY_OPT_IN` to emit telemetry following both `go.opentelemetry.io/otel/semconv/v1.21.0` (default) and `go.opentelemetry.io/otel/semconv/v1.26.0` (opt-in) in `go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo` per the [Database semantic convention stability migration guide](https://github.com/open-telemetry/semantic-conventions/blob/cb11bb9bac24f4b0e95ad0f61ce01813d8ceada8/docs/non-normative/db-migration.md#database-semantic-convention-stability-migration-guide). (#6172)
4748
- The `code.function` attribute emitted by `go.opentelemetry.io/contrib/bridges/otelslog` now stores the package path-qualified function name instead of just the function name.
4849
The `code.namespace` attribute is no longer added. (#6870)
4950
- The `code.function` attribute emitted by `go.opentelemetry.io/contrib/bridges/otelzap` now stores the package path-qualified function name instead of just the function name.

instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/doc.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66
// This package is compatible with v0.2.0 of
77
// go.mongodb.org/mongo-driver/mongo.
88
//
9-
// `NewMonitor` will return an event.CommandMonitor which is used to trace
9+
// NewMonitor will return an event.CommandMonitor which is used to trace
1010
// requests.
1111
//
1212
// This code was originally based on the following:
13-
// - https://github.com/DataDog/dd-trace-go/tree/02f0449efa3cb382d499fadc873957385dcb2192/contrib/go.mongodb.org/mongo-driver/mongo
14-
// - https://github.com/DataDog/dd-trace-go/tree/v1.23.3/ddtrace/ext
13+
// - https://github.com/DataDog/dd-trace-go/tree/02f0449efa3cb382d499fadc873957385dcb2192/contrib/go.mongodb.org/mongo-driver/mongo
14+
// - https://github.com/DataDog/dd-trace-go/tree/v1.23.3/ddtrace/ext
15+
//
16+
// The "OTEL_SEMCONV_STABILITY_OPT_IN" environment variable can be used to opt
17+
// into semconv/v1.26.0:
18+
// - "database": emit v1.26.0 semantic conventions
19+
// - "": emit v1.21.0 (default) semantic conventions
20+
// - "database/dup": emit v1.21.0 (default) and v1.26.0
21+
//
22+
// By default, otelmongo only emits v1.21.0.
1523
package otelmongo // import "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package semconv // import "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/internal/semconv"
5+
6+
import (
7+
"net"
8+
"os"
9+
"strconv"
10+
"strings"
11+
12+
"go.mongodb.org/mongo-driver/bson"
13+
"go.mongodb.org/mongo-driver/event"
14+
15+
"go.opentelemetry.io/otel/attribute"
16+
17+
semconv1210 "go.opentelemetry.io/otel/semconv/v1.21.0"
18+
semconv1260 "go.opentelemetry.io/otel/semconv/v1.26.0"
19+
)
20+
21+
// Constants for environment variable keys and versions.
22+
const (
23+
semconvOptIn = "OTEL_SEMCONV_STABILITY_OPT_IN"
24+
semconvOptInDup = "database/dup"
25+
semconvOptIn1260 = "database"
26+
)
27+
28+
// EventMonitor is responsible for monitoring events with a specified semantic
29+
// version.
30+
type EventMonitor struct {
31+
version string
32+
}
33+
34+
// NewEventMonitor creates an EventMonitor with the version set based on the
35+
// OTEL_SEMCONV_STABILITY_OPT_IN environment variable.
36+
func NewEventMonitor() EventMonitor {
37+
return EventMonitor{
38+
version: strings.ToLower(os.Getenv(semconvOptIn)),
39+
}
40+
}
41+
42+
// AttributeOptions represents options for tracing attributes.
43+
type AttributeOptions struct {
44+
collectionName string
45+
commandAttributeDisabled bool
46+
}
47+
48+
// AttributeOption is a function type that modifies AttributeOptions.
49+
type AttributeOption func(*AttributeOptions)
50+
51+
// WithCollectionName is a functional option to set the collection name in
52+
// AttributeOptions.
53+
func WithCollectionName(collName string) AttributeOption {
54+
return func(opts *AttributeOptions) {
55+
opts.collectionName = collName
56+
}
57+
}
58+
59+
// WithCommandAttributeDisabled is a functional option to enable or disable
60+
// command attributes.
61+
func WithCommandAttributeDisabled(disabled bool) AttributeOption {
62+
return func(opts *AttributeOptions) {
63+
opts.commandAttributeDisabled = disabled
64+
}
65+
}
66+
67+
// CommandStartedTraceAttrs generates trace attributes for a CommandStartedEvent
68+
// based on the EventMonitor version.
69+
func (m EventMonitor) CommandStartedTraceAttrs(
70+
evt *event.CommandStartedEvent,
71+
opts ...AttributeOption,
72+
) []attribute.KeyValue {
73+
switch m.version {
74+
case semconvOptIn1260:
75+
return commandStartedTraceAttrsV1260(evt, opts...)
76+
case semconvOptInDup:
77+
return append(commandStartedTraceAttrsV1260(evt, opts...), commandStartedTraceAttrsV1210(evt, opts...)...)
78+
default:
79+
return commandStartedTraceAttrsV1210(evt, opts...)
80+
}
81+
}
82+
83+
// peerInfo extracts the hostname and port from a CommandStartedEvent.
84+
func peerInfo(evt *event.CommandStartedEvent) (hostname string, port int) {
85+
hostname = evt.ConnectionID
86+
port = 27017 // Default MongoDB port
87+
88+
host, portStr, err := net.SplitHostPort(hostname)
89+
if err != nil {
90+
// If there's an error (likely because there's no port), assume default port
91+
// and use ConnectionID as hostname
92+
return hostname, port
93+
}
94+
95+
if parsedPort, err := strconv.Atoi(portStr); err == nil {
96+
port = parsedPort
97+
}
98+
99+
return host, port
100+
}
101+
102+
// sanitizeCommand converts a BSON command to a sanitized JSON string.
103+
// TODO: Sanitize values where possible.
104+
// TODO: Limit maximum size.
105+
func sanitizeCommand(command bson.Raw) string {
106+
b, _ := bson.MarshalExtJSON(command, false, false)
107+
108+
return string(b)
109+
}
110+
111+
// commandStartedTraceAttrsV1260 generates trace attributes for semantic version
112+
// 1.26.0.
113+
func commandStartedTraceAttrsV1260(evt *event.CommandStartedEvent, setters ...AttributeOption) []attribute.KeyValue {
114+
opts := &AttributeOptions{}
115+
for _, set := range setters {
116+
set(opts)
117+
}
118+
119+
attrs := []attribute.KeyValue{semconv1260.DBSystemMongoDB}
120+
121+
attrs = append(attrs, semconv1260.DBOperationName(evt.CommandName))
122+
attrs = append(attrs, semconv1260.DBNamespace(evt.DatabaseName))
123+
attrs = append(attrs, semconv1260.NetworkTransportTCP)
124+
125+
hostname, port := peerInfo(evt)
126+
attrs = append(attrs, semconv1260.NetworkPeerPort(port))
127+
attrs = append(attrs, semconv1260.NetworkPeerAddress(net.JoinHostPort(hostname, strconv.Itoa(port))))
128+
129+
if !opts.commandAttributeDisabled {
130+
attrs = append(attrs, semconv1260.DBQueryText(sanitizeCommand(evt.Command)))
131+
}
132+
133+
if opts.collectionName != "" {
134+
attrs = append(attrs, semconv1260.DBCollectionName(opts.collectionName))
135+
}
136+
137+
return attrs
138+
}
139+
140+
// commandStartedTraceAttrsV1210 generates trace attributes for semantic version
141+
// 1.21.0.
142+
func commandStartedTraceAttrsV1210(evt *event.CommandStartedEvent, setters ...AttributeOption) []attribute.KeyValue {
143+
opts := &AttributeOptions{}
144+
for _, set := range setters {
145+
set(opts)
146+
}
147+
148+
attrs := []attribute.KeyValue{semconv1210.DBSystemMongoDB}
149+
150+
attrs = append(attrs, semconv1210.DBOperation(evt.CommandName))
151+
attrs = append(attrs, semconv1210.DBName(evt.DatabaseName))
152+
attrs = append(attrs, semconv1210.NetTransportTCP)
153+
154+
hostname, port := peerInfo(evt)
155+
attrs = append(attrs, semconv1210.NetPeerPort(port))
156+
attrs = append(attrs, semconv1210.NetPeerName(hostname))
157+
158+
if !opts.commandAttributeDisabled {
159+
attrs = append(attrs, semconv1210.DBStatement(sanitizeCommand(evt.Command)))
160+
}
161+
162+
if opts.collectionName != "" {
163+
attrs = append(attrs, semconv1210.DBMongoDBCollection(opts.collectionName))
164+
}
165+
166+
return attrs
167+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package semconv
5+
6+
import (
7+
"net"
8+
"strconv"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"go.mongodb.org/mongo-driver/bson"
13+
"go.mongodb.org/mongo-driver/event"
14+
15+
"go.opentelemetry.io/otel/attribute"
16+
semconv1210 "go.opentelemetry.io/otel/semconv/v1.21.0"
17+
semconv1260 "go.opentelemetry.io/otel/semconv/v1.26.0"
18+
)
19+
20+
func TestNewEventMonitor(t *testing.T) {
21+
tests := []struct {
22+
name string
23+
version string
24+
want string
25+
}{
26+
{
27+
name: "Default Version",
28+
version: "",
29+
want: "",
30+
},
31+
{
32+
name: "Version 1260",
33+
version: semconvOptIn1260,
34+
want: "database",
35+
},
36+
{
37+
name: "Duplicate Version",
38+
version: semconvOptInDup,
39+
want: "database/dup",
40+
},
41+
}
42+
43+
for _, test := range tests {
44+
t.Run(test.name, func(t *testing.T) {
45+
t.Setenv(semconvOptIn, test.version)
46+
47+
monitor := NewEventMonitor()
48+
assert.Equal(t, test.want, monitor.version, "Expected version does not match")
49+
})
50+
}
51+
}
52+
53+
func TestPeerInfo(t *testing.T) {
54+
// Test cases for peerInfo
55+
tests := []struct {
56+
name string
57+
connectionID string
58+
wantHostname string
59+
wantPort int
60+
}{
61+
{"No Port", "localhost", "localhost", 27017},
62+
{"With Port", "localhost:12345", "localhost", 12345},
63+
}
64+
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
evt := &event.CommandStartedEvent{ConnectionID: tt.connectionID}
68+
hostname, port := peerInfo(evt)
69+
assert.Equal(t, tt.wantHostname, hostname, "Hostname does not match")
70+
assert.Equal(t, tt.wantPort, port, "Port does not match")
71+
})
72+
}
73+
}
74+
75+
func TestCommandStartedTraceAttrs(t *testing.T) {
76+
const (
77+
opName = "opName"
78+
dbNamespace = "dbNamespace"
79+
port = 1
80+
host = "host"
81+
address = "host:1"
82+
stmt = `{"insert":"users"}`
83+
coll = "coll"
84+
)
85+
86+
v1210 := []attribute.KeyValue{
87+
semconv1210.DBSystemMongoDB,
88+
{Key: "db.operation", Value: attribute.StringValue(opName)},
89+
{Key: "db.name", Value: attribute.StringValue(dbNamespace)},
90+
{Key: "db.statement", Value: attribute.StringValue(stmt)},
91+
{Key: "net.peer.port", Value: attribute.IntValue(port)},
92+
{Key: "net.peer.name", Value: attribute.StringValue(host)},
93+
{Key: "net.transport", Value: attribute.StringValue("ip_tcp")},
94+
{Key: "db.mongodb.collection", Value: attribute.StringValue("coll")},
95+
}
96+
97+
v1260 := []attribute.KeyValue{
98+
semconv1260.DBSystemMongoDB,
99+
{Key: "db.operation.name", Value: attribute.StringValue(opName)},
100+
{Key: "db.namespace", Value: attribute.StringValue(dbNamespace)},
101+
{Key: "db.query.text", Value: attribute.StringValue(stmt)},
102+
{Key: "network.peer.port", Value: attribute.IntValue(port)},
103+
{Key: "network.peer.address", Value: attribute.StringValue(address)},
104+
{Key: "network.transport", Value: attribute.StringValue("tcp")},
105+
{Key: "db.collection.name", Value: attribute.StringValue("coll")},
106+
}
107+
108+
tests := []struct {
109+
name string
110+
initAttrs []attribute.KeyValue
111+
version string
112+
want []attribute.KeyValue
113+
}{
114+
{
115+
name: "no version",
116+
initAttrs: []attribute.KeyValue{},
117+
version: "",
118+
want: v1210,
119+
},
120+
{
121+
name: "unsupported version",
122+
initAttrs: []attribute.KeyValue{},
123+
version: "database/foo",
124+
want: v1210,
125+
},
126+
{
127+
name: "database",
128+
initAttrs: []attribute.KeyValue{},
129+
version: "database",
130+
want: v1260,
131+
},
132+
{
133+
name: "database/dup",
134+
initAttrs: []attribute.KeyValue{},
135+
version: "database/dup",
136+
want: append(v1210, v1260...),
137+
},
138+
}
139+
140+
for _, test := range tests {
141+
t.Run(test.name, func(t *testing.T) {
142+
t.Setenv(semconvOptIn, test.version)
143+
144+
stmtBytes, err := bson.Marshal(bson.D{{Key: "insert", Value: "users"}})
145+
assert.NoError(t, err)
146+
147+
monitor := NewEventMonitor()
148+
attrs := monitor.CommandStartedTraceAttrs(&event.CommandStartedEvent{
149+
DatabaseName: dbNamespace,
150+
CommandName: opName,
151+
Command: bson.Raw(stmtBytes),
152+
ConnectionID: net.JoinHostPort(host, strconv.FormatInt(int64(port), 10)),
153+
}, WithCollectionName(coll))
154+
155+
assert.ElementsMatch(t, test.want, attrs)
156+
})
157+
}
158+
}

0 commit comments

Comments
 (0)