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  }