From dc78b1c6cc65fe72d6f2c490e5fae8813867112a Mon Sep 17 00:00:00 2001 From: sj Date: Wed, 24 Jun 2026 11:24:39 +0200 Subject: [PATCH 1/2] PT-2354: pt-mongodb-summary graceful ARBITER handling Running pt-mongodb-summary directly against a replica-set arbiter crashed the report: hostInfo is denied on an arbiter, and main treated any hostInfo failure as fatal (os.Exit). Connecting to a data-bearing member also left the arbiter as a blank row in the Instances list. Detect arbiters via isMaster (arbiterOnly, with me-in-arbiters fallback) and: - build a best-effort host summary from non-privileged fields instead of exiting, with a note explaining arbiter limitations; - render the arbiter as ARBITER in the Instances list, with ReplSet filled; - guard the mongos, runningOps and security report sections against nil so the formatter no longer panics when those collectors are unauthorized. --- src/go/mongolib/proto/master_doc.go | 24 ++++++- src/go/mongolib/util/util.go | 22 +++++- src/go/pt-mongodb-summary/main.go | 70 +++++++++++++------ .../pt-mongodb-summary/templates/hostinfo.go | 8 +++ 4 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/go/mongolib/proto/master_doc.go b/src/go/mongolib/proto/master_doc.go index 77d530d5c..5d889e944 100644 --- a/src/go/mongolib/proto/master_doc.go +++ b/src/go/mongolib/proto/master_doc.go @@ -14,7 +14,25 @@ package proto type MasterDoc struct { - SetName interface{} `bson:"setName"` - Hosts interface{} `bson:"hosts"` - Msg string `bson:"msg"` + SetName interface{} `bson:"setName"` + Hosts interface{} `bson:"hosts"` + Msg string `bson:"msg"` + ArbiterOnly bool `bson:"arbiterOnly"` + Me string `bson:"me"` + Arbiters []string `bson:"arbiters"` +} + +func (md MasterDoc) IsArbiter() bool { + if md.ArbiterOnly { + return true + } + if md.Me == "" { + return false + } + for _, a := range md.Arbiters { + if a == md.Me { + return true + } + } + return false } diff --git a/src/go/mongolib/util/util.go b/src/go/mongolib/util/util.go index 5e0af6982..d4e58a8ad 100644 --- a/src/go/mongolib/util/util.go +++ b/src/go/mongolib/util/util.go @@ -106,6 +106,17 @@ func GetReplicasetMembers(ctx context.Context, clientOptions *options.ClientOpti } m.StateStr = strings.ToUpper(cmdOpts.Parsed.Sharding.ClusterRole) + if md, mdErr := GetMasterDoc(ctx, client); mdErr == nil && md.IsArbiter() { + if m.StateStr != "" { + m.StateStr += "/ARBITER" + } else { + m.StateStr = "ARBITER" + } + if setName, ok := md.SetName.(string); ok { + m.Set = setName + } + } + if serverStatus, err := GetServerStatus(ctx, client); err == nil { m.ID = serverStatus.Pid m.StorageEngine = serverStatus.StorageEngine @@ -537,9 +548,16 @@ func fillProcInfo(pid int32, procInfo *proto.ProcInfo) error { return nil } -func getNodeType(ctx context.Context, client *mongo.Client) (string, error) { +func GetMasterDoc(ctx context.Context, client *mongo.Client) (proto.MasterDoc, error) { md := proto.MasterDoc{} - if err := client.Database("admin").RunCommand(ctx, primitive.M{"isMaster": 1}).Decode(&md); err != nil { + err := client.Database("admin").RunCommand(ctx, primitive.M{"isMaster": 1}).Decode(&md) + + return md, err +} + +func getNodeType(ctx context.Context, client *mongo.Client) (string, error) { + md, err := GetMasterDoc(ctx, client) + if err != nil { return "", err } diff --git a/src/go/pt-mongodb-summary/main.go b/src/go/pt-mongodb-summary/main.go index 020356521..40edd1b9d 100644 --- a/src/go/pt-mongodb-summary/main.go +++ b/src/go/pt-mongodb-summary/main.go @@ -118,6 +118,7 @@ type hostInfo struct { ReplicasetName string Version string NodeType string + IsArbiter bool } type procInfo struct { @@ -365,12 +366,14 @@ func formatResults(ci *collectedInfo, format string) ([]byte, error) { default: buf = new(bytes.Buffer) - t := template.Must(template.New("mongos").Parse(templates.MongosInfo)) - if err := t.Execute(buf, ci.MongosInfo); err != nil { - return nil, errors.Wrap(err, "cannot parse mongos section of the output template") + if ci.MongosInfo != nil { + t := template.Must(template.New("mongos").Parse(templates.MongosInfo)) + if err := t.Execute(buf, ci.MongosInfo); err != nil { + return nil, errors.Wrap(err, "cannot parse mongos section of the output template") + } } - t = template.Must(template.New("replicas").Parse(templates.Replicas)) + t := template.Must(template.New("replicas").Parse(templates.Replicas)) if err := t.Execute(buf, ci.ReplicaMembers); err != nil { return nil, errors.Wrap(err, "cannot parse replicas section of the output template") } @@ -385,14 +388,18 @@ func formatResults(ci *collectedInfo, format string) ([]byte, error) { return nil, errors.Wrap(err, "cannot parse the command line args section of the output template") } - t = template.Must(template.New("runningOps").Parse(templates.RunningOps)) - if err := t.Execute(buf, ci.RunningOps); err != nil { - return nil, errors.Wrap(err, "cannot parse runningOps section of the output template") + if ci.RunningOps != nil { + t = template.Must(template.New("runningOps").Parse(templates.RunningOps)) + if err := t.Execute(buf, ci.RunningOps); err != nil { + return nil, errors.Wrap(err, "cannot parse runningOps section of the output template") + } } - t = template.Must(template.New("ssl").Parse(templates.Security)) - if err := t.Execute(buf, ci.SecuritySettings); err != nil { - return nil, errors.Wrap(err, "cannot parse ssl section of the output template") + if ci.SecuritySettings != nil { + t = template.Must(template.New("ssl").Parse(templates.Security)) + if err := t.Execute(buf, ci.SecuritySettings); err != nil { + return nil, errors.Wrap(err, "cannot parse ssl section of the output template") + } } if ci.OplogInfo != nil && len(ci.OplogInfo) > 0 { @@ -416,24 +423,43 @@ func formatResults(ci *collectedInfo, format string) ([]byte, error) { return buf.Bytes(), nil } +func newArbiterHostInfo(md proto.MasterDoc) *hostInfo { + setName, _ := md.SetName.(string) + + return &hostInfo{ + NodeType: "ARBITER", + Hostname: md.Me, + ReplicasetName: setName, + IsArbiter: true, + } +} + func getHostInfo(ctx context.Context, client *mongo.Client) (*hostInfo, error) { + var i *hostInfo + hi := proto.HostInfo{} if err := client.Database("admin").RunCommand(ctx, primitive.M{"hostInfo": 1}).Decode(&hi); err != nil { log.Debugf("run('hostInfo') error: %s", err) - return nil, errors.Wrap(err, "GetHostInfo.hostInfo") - } - - nodeType, _ := getNodeType(ctx, client) - procCount, _ := countMongodProcesses() + md, mdErr := util.GetMasterDoc(ctx, client) + if mdErr != nil || !md.IsArbiter() { + return nil, errors.Wrap(err, "GetHostInfo.hostInfo") + } - i := &hostInfo{ - Hostname: hi.System.Hostname, - HostOsType: hi.Os.Type, - HostSystemCPUArch: hi.System.CpuArch, - ProcProcessCount: procCount, - NodeType: nodeType, - CmdlineArgs: nil, + i = newArbiterHostInfo(md) + i.ProcProcessCount, _ = countMongodProcesses() + } else { + nodeType, _ := getNodeType(ctx, client) + procCount, _ := countMongodProcesses() + + i = &hostInfo{ + Hostname: hi.System.Hostname, + HostOsType: hi.Os.Type, + HostSystemCPUArch: hi.System.CpuArch, + ProcProcessCount: procCount, + NodeType: nodeType, + CmdlineArgs: nil, + } } var cmdOpts proto.CommandLineOptions diff --git a/src/go/pt-mongodb-summary/templates/hostinfo.go b/src/go/pt-mongodb-summary/templates/hostinfo.go index 3e31f3a6b..6a6771354 100644 --- a/src/go/pt-mongodb-summary/templates/hostinfo.go +++ b/src/go/pt-mongodb-summary/templates/hostinfo.go @@ -26,7 +26,9 @@ const HostInfo = `# This host Hostname | {{.Hostname}} Version | {{.Version}} Built On | {{.HostOsType}} {{.HostSystemCPUArch}} +{{- if not .ProcCreateTime.IsZero }} Started | {{.ProcCreateTime}} +{{- end }} {{- if .DBPath }} Datadir | {{.DBPath}} {{- end }} @@ -35,5 +37,11 @@ const HostInfo = `# This host {{- if .ReplicasetName }} ReplSet | {{.ReplicasetName}} Repl Status | +{{- end }} +{{- if .IsArbiter }} + +# Arbiter ############################################################################################### +This node is a replica-set ARBITER: it holds no data, so host, security, storage and +running operations details are unavailable from this member. {{- end -}} ` From f1c28e73d04280a067d2440f250cc8da4ca254e0 Mon Sep 17 00:00:00 2001 From: sj Date: Wed, 24 Jun 2026 11:41:50 +0200 Subject: [PATCH 2/2] PT-2354: keep arbiter ReplSet name when serverStatus has none newArbiterHostInfo seeds ReplicasetName from isMaster's setName, but the serverStatus block unconditionally overwrote it with ss.Repl.SetName, which can be empty on an arbiter. Only override when non-empty so the seeded value survives. --- src/go/pt-mongodb-summary/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/go/pt-mongodb-summary/main.go b/src/go/pt-mongodb-summary/main.go index 40edd1b9d..97d9c47c9 100644 --- a/src/go/pt-mongodb-summary/main.go +++ b/src/go/pt-mongodb-summary/main.go @@ -480,7 +480,7 @@ func getHostInfo(ctx context.Context, client *mongo.Client) (*hostInfo, error) { if err == nil { i.ProcessName = ss.Process i.Version = ss.Version - if ss.Repl != nil { + if ss.Repl != nil && ss.Repl.SetName != "" { i.ReplicasetName = ss.Repl.SetName }