diff --git a/CHANGELOG.md b/CHANGELOG.md index 654e7c555e1..c4895cb23be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Added support for providing `endpoint`, `pollingIntervalMs` and `initialSamplingRate` using environment variable `OTEL_TRACES_SAMPLER_ARG` in `go.opentelemetry.io/contrib/samples/jaegerremote`. (#6310) - Added support exporting logs via OTLP over gRPC in `go.opentelemetry.io/contrib/config`. (#6340) +### Changed + +- Support 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/main/docs/non-normative/db-migration.md#database-semantic-convention-stability-migration-guide) (#6172) + ### Fixed - Fixed the value for configuring the OTLP exporter to use `grpc` instead of `grpc/protobuf` in `go.opentelemetry.io/contrib/config`. (#6338) diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/doc.go b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/doc.go index f8d53a53918..e046f68116a 100644 --- a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/doc.go +++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/doc.go @@ -6,10 +6,18 @@ // This package is compatible with v0.2.0 of // go.mongodb.org/mongo-driver/mongo. // -// `NewMonitor` will return an event.CommandMonitor which is used to trace +// NewMonitor will return an event.CommandMonitor which is used to trace // requests. // // This code was originally based on the following: -// - https://github.com/DataDog/dd-trace-go/tree/02f0449efa3cb382d499fadc873957385dcb2192/contrib/go.mongodb.org/mongo-driver/mongo -// - https://github.com/DataDog/dd-trace-go/tree/v1.23.3/ddtrace/ext +// - https://github.com/DataDog/dd-trace-go/tree/02f0449efa3cb382d499fadc873957385dcb2192/contrib/go.mongodb.org/mongo-driver/mongo +// - https://github.com/DataDog/dd-trace-go/tree/v1.23.3/ddtrace/ext +// +// The "OTEL_SEMCONV_STABILITY_OPT_IN" environment variable can be used to opt +// into semconv/v1.26.0: +// - "mongo": emit v1.26.0 semantic conventions +// - "": emit v1.21.0 (default) semantic conventions +// - "mongo/dup": emit v1.21.0 (default) and v1.26.0 +// +// By default, otelmongo only emits v1.21.0. package otelmongo // import "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo" diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/go.mod b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/go.mod index 5a13afb3e45..7eb3180d047 100644 --- a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/go.mod +++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/go.mod @@ -3,17 +3,20 @@ module go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/m go 1.22 require ( + github.com/stretchr/testify v1.9.0 go.mongodb.org/mongo-driver v1.17.1 go.opentelemetry.io/otel v1.32.0 go.opentelemetry.io/otel/trace v1.32.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/montanaflynn/stats v0.7.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect @@ -22,4 +25,5 @@ require ( golang.org/x/crypto v0.29.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/text v0.20.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/go.sum b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/go.sum index ea179f05eee..e245419d153 100644 --- a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/go.sum +++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/go.sum @@ -63,5 +63,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/mongo.go b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/mongo.go index 746e2e2c516..82acfe253be 100644 --- a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/mongo.go +++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/mongo.go @@ -7,13 +7,14 @@ import ( "context" "errors" "fmt" + "net" "strconv" "strings" "sync" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" - semconv "go.opentelemetry.io/otel/semconv/v1.21.0" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" "go.opentelemetry.io/otel/trace" "go.mongodb.org/mongo-driver/bson" @@ -32,25 +33,28 @@ type monitor struct { } func (m *monitor) Started(ctx context.Context, evt *event.CommandStartedEvent) { - var spanName string - hostname, port := peerInfo(evt) - attrs := []attribute.KeyValue{ - semconv.DBSystemMongoDB, - semconv.DBOperation(evt.CommandName), - semconv.DBName(evt.DatabaseName), - semconv.NetPeerName(hostname), - semconv.NetPeerPort(port), - semconv.NetTransportTCP, - } + attrs := []attribute.KeyValue{semconv.DBSystemMongoDB} + + attrs = appendOpNameAttrs(attrs, evt.CommandName) + attrs = appendDBNamespace(attrs, evt.DatabaseName) + attrs = appendNetworkPort(attrs, port) + attrs = appendNetworkHost(attrs, hostname) + attrs = appendNetworkAddress(attrs, net.JoinHostPort(hostname, strconv.Itoa(port))) + attrs = appendNetworkTransport(attrs) + if !m.cfg.CommandAttributeDisabled { - attrs = append(attrs, semconv.DBStatement(sanitizeCommand(evt.Command))) + attrs = appendDBStatement(attrs, sanitizeCommand(evt.Command)) } + + var spanName string if collection, err := extractCollection(evt); err == nil && collection != "" { spanName = collection + "." - attrs = append(attrs, semconv.DBMongoDBCollection(collection)) + + attrs = appendCollection(attrs, collection) } + spanName += evt.CommandName opts := []trace.SpanStartOption{ trace.WithSpanKind(trace.SpanKindClient), diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/semconv.go b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/semconv.go new file mode 100644 index 00000000000..d8683c6c9ac --- /dev/null +++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/semconv.go @@ -0,0 +1,88 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelmongo // import "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo" + +import ( + "os" + + "go.opentelemetry.io/otel/attribute" + semconv1210 "go.opentelemetry.io/otel/semconv/v1.21.0" + semconv1260 "go.opentelemetry.io/otel/semconv/v1.26.0" +) + +const ( + semconvOptIn = "OTEL_SEMCONV_STABILITY_OPT_IN" + semconvOptIn1260 = "mongo" + semconvOptInDup = "mongo/dup" +) + +func appendAttrs[T string | int]( + attrs []attribute.KeyValue, + semconvMap1210 func(T) attribute.KeyValue, + semconvMap1260 func(T) attribute.KeyValue, + val T, +) []attribute.KeyValue { + switch os.Getenv(semconvOptIn) { + case semconvOptIn1260: + if semconvMap1260 != nil { + attrs = append(attrs, semconvMap1260(val)) + } + case semconvOptInDup: + if semconvMap1210 != nil { + attrs = append(attrs, semconvMap1210(val)) + } + + if semconvMap1260 != nil { + attrs = append(attrs, semconvMap1260(val)) + } + default: + if semconvMap1210 != nil { + attrs = append(attrs, semconvMap1210(val)) + } + } + + return attrs +} + +func appendOpNameAttrs(attrs []attribute.KeyValue, op string) []attribute.KeyValue { + return appendAttrs(attrs, semconv1210.DBOperation, semconv1260.DBOperationName, op) +} + +func appendDBNamespace(attrs []attribute.KeyValue, ns string) []attribute.KeyValue { + return appendAttrs(attrs, semconv1210.DBName, semconv1260.DBNamespace, ns) +} + +func appendDBStatement(attrs []attribute.KeyValue, stmt string) []attribute.KeyValue { + return appendAttrs(attrs, semconv1210.DBStatement, semconv1260.DBQueryText, stmt) +} + +func appendNetworkPort(attrs []attribute.KeyValue, p int) []attribute.KeyValue { + return appendAttrs(attrs, semconv1210.NetPeerPort, semconv1260.NetworkPeerPort, p) +} + +func appendNetworkHost(attrs []attribute.KeyValue, h string) []attribute.KeyValue { + return appendAttrs(attrs, semconv1210.NetPeerName, nil, h) +} + +func appendNetworkAddress(attrs []attribute.KeyValue, addr string) []attribute.KeyValue { + return appendAttrs(attrs, nil, semconv1260.NetworkPeerAddress, addr) +} + +func appendNetworkTransport(attrs []attribute.KeyValue) []attribute.KeyValue { + switch os.Getenv(semconvOptIn) { + case semconvOptIn1260: + attrs = append(attrs, semconv1260.NetworkTransportTCP) + case semconvOptInDup: + attrs = append(attrs, semconv1260.NetworkTransportTCP) + fallthrough + default: + attrs = append(attrs, semconv1210.NetTransportTCP) + } + + return attrs +} + +func appendCollection(attrs []attribute.KeyValue, coll string) []attribute.KeyValue { + return appendAttrs(attrs, semconv1210.DBMongoDBCollection, semconv1260.DBCollectionName, coll) +} diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/semconv_test.go b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/semconv_test.go new file mode 100644 index 00000000000..93efdf43a48 --- /dev/null +++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/semconv_test.go @@ -0,0 +1,111 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelmongo + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/attribute" +) + +func Test_appendOpNameAttrs(t *testing.T) { + const ( + opName = "opName" + dbNamespace = "dbNamespace" + port = 1 + host = "host" + address = "host:1" + stmt = `{insert: "users"}` + coll = "coll" + ) + + v1170 := []attribute.KeyValue{ + {Key: "db.operation", Value: attribute.StringValue(opName)}, + {Key: "db.name", Value: attribute.StringValue(dbNamespace)}, + {Key: "db.statement", Value: attribute.StringValue(stmt)}, + {Key: "net.peer.port", Value: attribute.IntValue(port)}, + {Key: "net.peer.name", Value: attribute.StringValue(host)}, + {Key: "net.transport", Value: attribute.StringValue("ip_tcp")}, + {Key: "db.mongodb.collection", Value: attribute.StringValue("coll")}, + } + + v1260 := []attribute.KeyValue{ + {Key: "db.operation.name", Value: attribute.StringValue(opName)}, + {Key: "db.namespace", Value: attribute.StringValue(dbNamespace)}, + {Key: "db.query.text", Value: attribute.StringValue(stmt)}, + {Key: "network.peer.port", Value: attribute.IntValue(port)}, + {Key: "network.peer.address", Value: attribute.StringValue(address)}, + {Key: "network.transport", Value: attribute.StringValue("tcp")}, + {Key: "db.collection.name", Value: attribute.StringValue("coll")}, + } + + tests := []struct { + name string + initAttrs []attribute.KeyValue + version string + want []attribute.KeyValue + }{ + { + name: "no version", + initAttrs: []attribute.KeyValue{}, + version: "", + want: v1170, + }, + { + name: "unsupported version", + initAttrs: []attribute.KeyValue{}, + version: "mongo/foo", + want: v1170, + }, + { + name: "mongo", + initAttrs: []attribute.KeyValue{}, + version: "mongo", + want: v1260, + }, + { + name: "mongo/dup", + initAttrs: []attribute.KeyValue{}, + version: "mongo/dup", + want: append(v1170, v1260...), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Setenv(semconvOptIn, test.version) + + attrs := appendOpNameAttrs(test.initAttrs, opName) + attrs = appendDBNamespace(attrs, dbNamespace) + attrs = appendDBStatement(attrs, stmt) + attrs = appendNetworkPort(attrs, port) + attrs = appendNetworkHost(attrs, host) + attrs = appendNetworkAddress(attrs, address) + attrs = appendNetworkTransport(attrs) + attrs = appendCollection(attrs, coll) + + assert.ElementsMatch(t, test.want, attrs) + }) + } +} + +func Benchmark_appendAttrs(b *testing.B) { + ini := []attribute.KeyValue{} + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + ini = appendOpNameAttrs(ini, "opName") + ini = appendDBNamespace(ini, "dbNamespace") + ini = appendDBStatement(ini, `{insert: "users"}`) + ini = appendNetworkPort(ini, 1) + ini = appendNetworkHost(ini, "host") + ini = appendNetworkAddress(ini, "host:1") + ini = appendNetworkTransport(ini) + ini = appendCollection(ini, "coll") + } +} diff --git a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/test/mongo_test.go b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/test/mongo_test.go index 4b0f62b932b..d0f4d3e974a 100644 --- a/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/test/mongo_test.go +++ b/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo/test/mongo_test.go @@ -248,3 +248,90 @@ func TestDBCollectionAttribute(t *testing.T) { }) } } + +func assertSemconv1170(mt *mtest.T, attrs []attribute.KeyValue) { + mt.Helper() + + assert.Contains(mt, attrs, attribute.String("db.system", "mongodb")) + assert.Contains(mt, attrs, attribute.String("net.peer.name", "")) + assert.Contains(mt, attrs, attribute.Int64("net.peer.port", int64(27017))) + assert.Contains(mt, attrs, attribute.String("net.transport", "ip_tcp")) + assert.Contains(mt, attrs, attribute.String("db.name", "test-database")) +} + +func assertSemconv1260(mt *mtest.T, attrs []attribute.KeyValue) { + mt.Helper() + + assert.Contains(mt, attrs, attribute.String("db.system", "mongodb")) + assert.Contains(mt, attrs, attribute.String("network.peer.address", ":27017")) + assert.Contains(mt, attrs, attribute.Int64("network.peer.port", int64(27017))) + assert.Contains(mt, attrs, attribute.String("network.transport", "tcp")) + assert.Contains(mt, attrs, attribute.String("db.namespace", "test-database")) +} + +func TestSemanticConventionOptIn(t *testing.T) { + tt := []struct { + name string + semconvOptIn string + assert func(*mtest.T, []attribute.KeyValue) + }{ + { + name: "default", + semconvOptIn: "", + assert: assertSemconv1170, + }, + { + name: "mongo", + semconvOptIn: "mongo", + assert: assertSemconv1260, + }, + } + for _, tc := range tt { + tc := tc + + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + mt.Run(tc.name, func(mt *mtest.T) { + mt.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", tc.semconvOptIn) + + sr := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + ctx, span := provider.Tracer("test").Start(ctx, "mongodb-test") + + addr := "mongodb://localhost:27017/?connect=direct" + opts := options.Client() + opts.Monitor = otelmongo.NewMonitor( + otelmongo.WithTracerProvider(provider), + otelmongo.WithCommandAttributeDisabled(true), + ) + opts.ApplyURI(addr) + + mt.ResetClient(opts) + mt.AddMockResponses(bson.D{{Key: "ok", Value: 1}}) + + db := mt.Client.Database("test-database") + + _, _ = db.Collection("test-collection").InsertOne(ctx, bson.D{{Key: "test-item", Value: "test-value"}}) + + span.End() + + spans := sr.Ended() + if !assert.Len(mt, spans, 2, "expected 2 spans, received %d", len(spans)) { + mt.FailNow() + } + assert.Len(mt, spans, 2) + assert.Equal(mt, spans[0].SpanContext().TraceID(), spans[1].SpanContext().TraceID()) + assert.Equal(mt, spans[0].Parent().SpanID(), spans[1].SpanContext().SpanID()) + assert.Equal(mt, span.SpanContext().SpanID(), spans[1].SpanContext().SpanID()) + + s := spans[0] + assert.Equal(mt, trace.SpanKindClient, s.SpanKind()) + + tc.assert(mt, s.Attributes()) + }) + } +}