diff --git a/cluster.go b/cluster.go index d43245649..81e4f7ff5 100644 --- a/cluster.go +++ b/cluster.go @@ -30,6 +30,7 @@ import ( "errors" "fmt" "net" + "runtime" "strconv" "strings" "sync" @@ -61,9 +62,10 @@ type mongoCluster struct { cachedIndex map[string]bool sync chan bool dial dialer + appName string } -func newCluster(userSeeds []string, direct, failFast bool, dial dialer, setName string) *mongoCluster { +func newCluster(userSeeds []string, direct, failFast bool, dial dialer, setName string, appName string) *mongoCluster { cluster := &mongoCluster{ userSeeds: userSeeds, references: 1, @@ -71,6 +73,7 @@ func newCluster(userSeeds []string, direct, failFast bool, dial dialer, setName failFast: failFast, dial: dial, setName: setName, + appName: appName, } cluster.serverSynced.L = cluster.RWMutex.RLocker() cluster.sync = make(chan bool, 1) @@ -144,7 +147,17 @@ func (cluster *mongoCluster) isMaster(socket *mongoSocket, result *isMasterResul // Monotonic let's it talk to a slave and still hold the socket. session := newSession(Monotonic, cluster, 10*time.Second) session.setSocket(socket) - err := session.Run("ismaster", result) + + // provide some meta infos on the client, + // see https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#connection-handshake + // for details + metaInfo := bson.M{"driver": bson.M{"name": "mgo", "version": "globalsign"}, + "os": bson.M{"type": runtime.GOOS, "architecture": runtime.GOARCH}} + + if cluster.appName != "" { + metaInfo["application"] = bson.M{"name": cluster.appName} + } + err := session.Run(bson.D{{"isMaster", 1}, {"client", metaInfo}}, result) session.Close() return err } diff --git a/session.go b/session.go index d18277be4..cfe271ff9 100644 --- a/session.go +++ b/session.go @@ -235,6 +235,10 @@ const ( // Defines the per-server socket pool limit. Defaults to 4096. // See Session.SetPoolLimit for details. // +// appName= +// +// The identifier of the client application which ran the operation. This +// param can't exceed 128 bytes // // Relevant documentation: // @@ -279,6 +283,7 @@ func ParseURL(url string) (*DialInfo, error) { source := "" setName := "" poolLimit := 0 + appName := "" readPreferenceMode := Primary var readPreferenceTagSets []bson.D for _, opt := range uinfo.options { @@ -296,6 +301,11 @@ func ParseURL(url string) (*DialInfo, error) { if err != nil { return nil, errors.New("bad value for maxPoolSize: " + opt.value) } + case "appName": + if len(opt.value) > 128 { + return nil, errors.New("appName too long, must be < 128 bytes: " + opt.value) + } + appName = opt.value case "readPreference": switch opt.value { case "nearest": @@ -350,6 +360,7 @@ func ParseURL(url string) (*DialInfo, error) { Service: service, Source: source, PoolLimit: poolLimit, + AppName: appName, ReadPreference: &ReadPreference{ Mode: readPreferenceMode, TagSets: readPreferenceTagSets, @@ -409,6 +420,9 @@ type DialInfo struct { // See Session.SetPoolLimit for details. PoolLimit int + // The identifier of the client application which ran the operation. + AppName string + // ReadPreference defines the manner in which servers are chosen. See // Session.SetMode and Session.SelectServers. ReadPreference *ReadPreference @@ -472,7 +486,7 @@ func DialWithInfo(info *DialInfo) (*Session, error) { } addrs[i] = addr } - cluster := newCluster(addrs, info.Direct, info.FailFast, dialer{info.Dial, info.DialServer}, info.ReplicaSetName) + cluster := newCluster(addrs, info.Direct, info.FailFast, dialer{info.Dial, info.DialServer}, info.ReplicaSetName, info.AppName) session := newSession(Eventual, cluster, info.Timeout) session.defaultdb = info.Database if session.defaultdb == "" { diff --git a/session_test.go b/session_test.go index e29221cb4..216474889 100644 --- a/session_test.go +++ b/session_test.go @@ -200,6 +200,50 @@ func (s *S) TestURLInvalidReadPreferenceTags(c *C) { } } +func (s *S) TestURLWithAppName(c *C) { + if !s.versionAtLeast(3, 4) { + c.Skip("appName depends on MongoDB 3.4+") + } + appName := "myAppName" + session, err := mgo.Dial("localhost:40001?appName=" + appName) + c.Assert(err, IsNil) + defer session.Close() + + db := session.DB("mydb") + + err = db.Run(bson.D{{"profile", 2}}, nil) + c.Assert(err, IsNil) + + coll := db.C("mycoll") + err = coll.Insert(M{"a": 1, "b": 2}) + c.Assert(err, IsNil) + + result := struct{ A, B int }{} + err = coll.Find(M{"a": 1}).One(&result) + c.Assert(err, IsNil) + + profileResult := struct { + AppName string `bson:"appName"` + }{} + + err = db.C("system.profile").Find(nil).Sort("-ts").One(&profileResult) + c.Assert(err, IsNil) + c.Assert(appName, Equals, profileResult.AppName) + // reset profiling to 0 as it add unecessary overhead to all other test + err = db.Run(bson.D{{"profile", 0}}, nil) + c.Assert(err, IsNil) +} + +func (s *S) TestURLWithAppNameTooLong(c *C) { + if !s.versionAtLeast(3, 4) { + c.Skip("appName depends on MongoDB 3.4+") + } + appName := "myAppNameWayTooLongmyAppNameWayTooLongmyAppNameWayTooLongmyAppNameWayTooLong" + appName += appName + _, err := mgo.Dial("localhost:40001?appName=" + appName) + c.Assert(err, ErrorMatches, "appName too long, must be < 128 bytes: "+appName) +} + func (s *S) TestInsertFindOne(c *C) { session, err := mgo.Dial("localhost:40001") c.Assert(err, IsNil)