github.com/newrelic/go-agent@v3.26.0+incompatible/_integrations/nrmongo/nrmongo.go (about) 1 // Copyright 2020 New Relic Corporation. All rights reserved. 2 // SPDX-License-Identifier: Apache-2.0 3 4 // Package nrmongo instruments https://github.com/mongodb/mongo-go-driver 5 // 6 // Use this package to instrument your MongoDB calls without having to manually 7 // create DatastoreSegments. To do so, first set the monitor in the connect 8 // options using `SetMonitor` 9 // (https://godoc.org/go.mongodb.org/mongo-driver/mongo/options#ClientOptions.SetMonitor): 10 // 11 // nrMon := nrmongo.NewCommandMonitor(nil) 12 // client, err := mongo.Connect(ctx, options.Client().SetMonitor(nrMon)) 13 // 14 // Note that it is important that this `nrmongo` monitor is the last monitor 15 // set, otherwise it will be overwritten. If needing to use more than one 16 // `event.CommandMonitor`, pass the original monitor to the 17 // `nrmongo.NewCommandMonitor` function: 18 // 19 // origMon := &event.CommandMonitor{ 20 // Started: origStarted, 21 // Succeeded: origSucceeded, 22 // Failed: origFailed, 23 // } 24 // nrMon := nrmongo.NewCommandMonitor(origMon) 25 // client, err := mongo.Connect(ctx, options.Client().SetMonitor(nrMon)) 26 // 27 // Then add the current transaction to the context used in any MongoDB call: 28 // 29 // ctx = newrelic.NewContext(context.Background(), txn) 30 // resp, err := collection.InsertOne(ctx, bson.M{"name": "pi", "value": 3.14159}) 31 package nrmongo 32 33 import ( 34 "context" 35 "regexp" 36 "sync" 37 38 newrelic "github.com/newrelic/go-agent" 39 "github.com/newrelic/go-agent/internal" 40 "go.mongodb.org/mongo-driver/event" 41 ) 42 43 func init() { internal.TrackUsage("integration", "datastore", "mongo") } 44 45 type mongoMonitor struct { 46 segmentMap map[int64]*newrelic.DatastoreSegment 47 origCommMon *event.CommandMonitor 48 sync.Mutex 49 } 50 51 // The Mongo connection ID is constructed as: `fmt.Sprintf("%s[-%d]", addr, nextConnectionID())`, 52 // where addr is of the form `host:port` (or `a.sock` for unix sockets) 53 // See https://github.com/mongodb/mongo-go-driver/blob/b39cd78ce7021252efee2fb44aa6e492d67680ef/x/mongo/driver/topology/connection.go#L68 54 // and https://github.com/mongodb/mongo-go-driver/blob/b39cd78ce7021252efee2fb44aa6e492d67680ef/x/mongo/driver/address/addr.go 55 var connIDPattern = regexp.MustCompile(`([^:\[]+)(?::(\d+))?\[-\d+]`) 56 57 // NewCommandMonitor returns a new `*event.CommandMonitor` 58 // (https://godoc.org/go.mongodb.org/mongo-driver/event#CommandMonitor). If 59 // provided, the original `*event.CommandMonitor` will be called as well. The 60 // returned `*event.CommandMonitor` creates `newrelic.DatastoreSegment`s 61 // (https://godoc.org/github.com/newrelic/go-agent#DatastoreSegment) for each 62 // database call. 63 // 64 // // Use `SetMonitor` to register the CommandMonitor. 65 // client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017").SetMonitor(nrmongo.NewCommandMonitor(nil))) 66 // if err != nil { 67 // log.Fatal(err) 68 // } 69 // 70 // // Add transaction to the context. This step is required. 71 // ctx = newrelic.NewContext(ctx, txn) 72 // 73 // collection := client.Database("testing").Collection("numbers") 74 // resp, err := collection.InsertOne(ctx, bson.M{"name": "pi", "value": 3.14159}) 75 // if err != nil { 76 // log.Fatal(err) 77 // } 78 func NewCommandMonitor(original *event.CommandMonitor) *event.CommandMonitor { 79 m := mongoMonitor{ 80 segmentMap: make(map[int64]*newrelic.DatastoreSegment), 81 origCommMon: original, 82 } 83 return &event.CommandMonitor{ 84 Started: m.started, 85 Succeeded: m.succeeded, 86 Failed: m.failed, 87 } 88 } 89 90 func (m *mongoMonitor) started(ctx context.Context, e *event.CommandStartedEvent) { 91 if m.origCommMon != nil && m.origCommMon.Started != nil { 92 m.origCommMon.Started(ctx, e) 93 } 94 txn := newrelic.FromContext(ctx) 95 if txn == nil { 96 return 97 } 98 host, port := calcHostAndPort(e.ConnectionID) 99 sgmt := newrelic.DatastoreSegment{ 100 StartTime: newrelic.StartSegmentNow(txn), 101 Product: newrelic.DatastoreMongoDB, 102 Collection: collName(e), 103 Operation: e.CommandName, 104 Host: host, 105 PortPathOrID: port, 106 DatabaseName: e.DatabaseName, 107 } 108 m.addSgmt(e, &sgmt) 109 } 110 111 func collName(e *event.CommandStartedEvent) string { 112 coll := e.Command.Lookup(e.CommandName) 113 collName, _ := coll.StringValueOK() 114 return collName 115 } 116 117 func (m *mongoMonitor) addSgmt(e *event.CommandStartedEvent, sgmt *newrelic.DatastoreSegment) { 118 m.Lock() 119 defer m.Unlock() 120 m.segmentMap[e.RequestID] = sgmt 121 } 122 123 func (m *mongoMonitor) succeeded(ctx context.Context, e *event.CommandSucceededEvent) { 124 m.endSgmtIfExists(e.RequestID) 125 if m.origCommMon != nil && m.origCommMon.Succeeded != nil { 126 m.origCommMon.Succeeded(ctx, e) 127 } 128 } 129 130 func (m *mongoMonitor) failed(ctx context.Context, e *event.CommandFailedEvent) { 131 m.endSgmtIfExists(e.RequestID) 132 if m.origCommMon != nil && m.origCommMon.Failed != nil { 133 m.origCommMon.Failed(ctx, e) 134 } 135 } 136 137 func (m *mongoMonitor) endSgmtIfExists(id int64) { 138 m.getAndRemoveSgmt(id).End() 139 } 140 141 func (m *mongoMonitor) getAndRemoveSgmt(id int64) *newrelic.DatastoreSegment { 142 m.Lock() 143 defer m.Unlock() 144 sgmt := m.segmentMap[id] 145 if sgmt != nil { 146 delete(m.segmentMap, id) 147 } 148 return sgmt 149 } 150 151 func calcHostAndPort(connID string) (host string, port string) { 152 // FindStringSubmatch either returns nil or an array of the size # of submatches + 1 (in this case 3) 153 addressParts := connIDPattern.FindStringSubmatch(connID) 154 if len(addressParts) == 3 { 155 host = addressParts[1] 156 port = addressParts[2] 157 } 158 return 159 }