1// Copyright (C) MongoDB, Inc. 2014-present.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may
4// not use this file except in compliance with the License. You may obtain
5// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
7package status
8
9import (
10	"fmt"
11	"regexp"
12	"sort"
13	"time"
14
15	"github.com/mongodb/mongo-tools/common/text"
16	"github.com/mongodb/mongo-tools/common/util"
17)
18
19type ReaderConfig struct {
20	HumanReadable bool
21	TimeFormat    string
22}
23
24type LockUsage struct {
25	Namespace string
26	Reads     int64
27	Writes    int64
28}
29
30type lockUsages []LockUsage
31
32func (slice lockUsages) Len() int {
33	return len(slice)
34}
35
36func (slice lockUsages) Less(i, j int) bool {
37	return slice[i].Reads+slice[i].Writes < slice[j].Reads+slice[j].Writes
38}
39
40func (slice lockUsages) Swap(i, j int) {
41	slice[i], slice[j] = slice[j], slice[i]
42}
43
44func formatBits(should bool, amt int64) string {
45	if should {
46		return text.FormatBits(amt)
47	}
48	return fmt.Sprintf("%v", amt)
49}
50
51func formatMegabyteAmount(should bool, amt int64) string {
52	if should {
53		return text.FormatMegabyteAmount(amt)
54	}
55	return fmt.Sprintf("%v", amt*1024*1024)
56}
57
58func numberToInt64(num interface{}) (int64, bool) {
59	switch n := num.(type) {
60	case int64:
61		return n, true
62	case int32:
63		return int64(n), true
64	case int:
65		return int64(n), true
66	}
67	return 0, false
68}
69
70func percentageInt64(value, outOf int64) float64 {
71	if value == 0 || outOf == 0 {
72		return 0
73	}
74	return 100 * (float64(value) / float64(outOf))
75}
76
77func averageInt64(value, outOf int64) int64 {
78	if value == 0 || outOf == 0 {
79		return 0
80	}
81	return value / outOf
82}
83
84func parseLocks(stat *ServerStatus) map[string]LockUsage {
85	returnVal := map[string]LockUsage{}
86	for namespace, lockInfo := range stat.Locks {
87		returnVal[namespace] = LockUsage{
88			namespace,
89			lockInfo.TimeLockedMicros.Read + lockInfo.TimeLockedMicros.ReadLower,
90			lockInfo.TimeLockedMicros.Write + lockInfo.TimeLockedMicros.WriteLower,
91		}
92	}
93	return returnVal
94}
95
96func computeLockDiffs(prevLocks, curLocks map[string]LockUsage) []LockUsage {
97	lockUsages := lockUsages(make([]LockUsage, 0, len(curLocks)))
98	for namespace, curUsage := range curLocks {
99		prevUsage, hasKey := prevLocks[namespace]
100		if !hasKey {
101			// This namespace didn't appear in the previous batch of lock info,
102			// so we can't compute a diff for it - skip it.
103			continue
104		}
105		// Calculate diff of lock usage for this namespace and add to the list
106		lockUsages = append(lockUsages,
107			LockUsage{
108				namespace,
109				curUsage.Reads - prevUsage.Reads,
110				curUsage.Writes - prevUsage.Writes,
111			})
112	}
113	// Sort the array in order of least to most locked
114	sort.Sort(lockUsages)
115	return lockUsages
116}
117
118func diff(newVal, oldVal int64, sampleSecs float64) int64 {
119	return int64(float64(newVal-oldVal) / sampleSecs)
120}
121
122func diffOp(newStat, oldStat *ServerStatus, f func(*OpcountStats) int64, both bool) string {
123	sampleSecs := float64(newStat.SampleTime.Sub(oldStat.SampleTime).Seconds())
124	var opcount int64
125	var opcountRepl int64
126	if newStat.Opcounters != nil && oldStat.Opcounters != nil {
127		opcount = diff(f(newStat.Opcounters), f(oldStat.Opcounters), sampleSecs)
128	}
129	if newStat.OpcountersRepl != nil && oldStat.OpcountersRepl != nil {
130		opcountRepl = diff(f(newStat.OpcountersRepl), f(oldStat.OpcountersRepl), sampleSecs)
131	}
132	switch {
133	case both || opcount > 0 && opcountRepl > 0:
134		return fmt.Sprintf("%v|%v", opcount, opcountRepl)
135	case opcount > 0:
136		return fmt.Sprintf("%v", opcount)
137	case opcountRepl > 0:
138		return fmt.Sprintf("*%v", opcountRepl)
139	default:
140		return "*0"
141	}
142}
143
144func getStorageEngine(stat *ServerStatus) string {
145	val := "mmapv1"
146	if stat.StorageEngine != nil && stat.StorageEngine["name"] != "" {
147		val = stat.StorageEngine["name"]
148	}
149	return val
150}
151
152// mongosProcessRE matches mongos not followed by any slashes before next whitespace
153var mongosProcessRE = regexp.MustCompile(`^.*\bmongos\b[^\\\/]*(\s.*)?$`)
154
155func IsMongos(stat *ServerStatus) bool {
156	return stat.ShardCursorType != nil || mongosProcessRE.MatchString(stat.Process)
157}
158
159func HasLocks(stat *ServerStatus) bool {
160	return ReadLockedDB(nil, stat, stat) != ""
161}
162
163func IsReplSet(stat *ServerStatus) (res bool) {
164	if stat.Repl != nil {
165		isReplSet, ok := stat.Repl.IsReplicaSet.(bool)
166		res = (ok && isReplSet) || len(stat.Repl.SetName) > 0
167	}
168	return
169}
170
171func IsMMAP(stat *ServerStatus) bool {
172	return getStorageEngine(stat) == "mmapv1"
173}
174
175func IsWT(stat *ServerStatus) bool {
176	return getStorageEngine(stat) == "wiredTiger"
177}
178
179func ReadHost(_ *ReaderConfig, newStat, _ *ServerStatus) string {
180	return newStat.Host
181}
182
183func ReadStorageEngine(_ *ReaderConfig, newStat, _ *ServerStatus) string {
184	return getStorageEngine(newStat)
185}
186
187func ReadInsert(_ *ReaderConfig, newStat, oldStat *ServerStatus) string {
188	return diffOp(newStat, oldStat, func(o *OpcountStats) int64 {
189		return o.Insert
190	}, false)
191}
192
193func ReadQuery(_ *ReaderConfig, newStat, oldStat *ServerStatus) string {
194	return diffOp(newStat, oldStat, func(s *OpcountStats) int64 {
195		return s.Query
196	}, false)
197}
198
199func ReadUpdate(_ *ReaderConfig, newStat, oldStat *ServerStatus) string {
200	return diffOp(newStat, oldStat, func(s *OpcountStats) int64 {
201		return s.Update
202	}, false)
203}
204
205func ReadDelete(_ *ReaderConfig, newStat, oldStat *ServerStatus) string {
206	return diffOp(newStat, oldStat, func(s *OpcountStats) int64 {
207		return s.Delete
208	}, false)
209}
210
211func ReadGetMore(_ *ReaderConfig, newStat, oldStat *ServerStatus) string {
212	sampleSecs := float64(newStat.SampleTime.Sub(oldStat.SampleTime).Seconds())
213	return fmt.Sprintf("%d", diff(newStat.Opcounters.GetMore, oldStat.Opcounters.GetMore, sampleSecs))
214}
215
216func ReadCommand(_ *ReaderConfig, newStat, oldStat *ServerStatus) string {
217	return diffOp(newStat, oldStat, func(s *OpcountStats) int64 {
218		return s.Command
219	}, true)
220}
221
222func ReadDirty(c *ReaderConfig, newStat, _ *ServerStatus) (val string) {
223	if newStat.WiredTiger != nil {
224		bytes := float64(newStat.WiredTiger.Cache.TrackedDirtyBytes)
225		max := float64(newStat.WiredTiger.Cache.MaxBytesConfigured)
226		if max != 0 {
227			val = fmt.Sprintf("%.1f", 100*bytes/max)
228			if c.HumanReadable {
229				val = val + "%"
230			}
231		}
232	}
233	return
234}
235
236func ReadUsed(c *ReaderConfig, newStat, _ *ServerStatus) (val string) {
237	if newStat.WiredTiger != nil {
238		bytes := float64(newStat.WiredTiger.Cache.CurrentCachedBytes)
239		max := float64(newStat.WiredTiger.Cache.MaxBytesConfigured)
240		if max != 0 {
241			val = fmt.Sprintf("%.1f", 100*bytes/max)
242			if c.HumanReadable {
243				val = val + "%"
244			}
245		}
246	}
247	return
248}
249
250func ReadFlushes(_ *ReaderConfig, newStat, oldStat *ServerStatus) string {
251	var val int64
252	if newStat.WiredTiger != nil && oldStat.WiredTiger != nil {
253		val = newStat.WiredTiger.Transaction.TransCheckpoints - oldStat.WiredTiger.Transaction.TransCheckpoints
254	} else if newStat.BackgroundFlushing != nil && oldStat.BackgroundFlushing != nil {
255		val = newStat.BackgroundFlushing.Flushes - oldStat.BackgroundFlushing.Flushes
256	}
257	return fmt.Sprintf("%d", val)
258}
259
260func ReadMapped(c *ReaderConfig, newStat, _ *ServerStatus) (val string) {
261	if util.IsTruthy(newStat.Mem.Supported) && IsMongos(newStat) {
262		val = formatMegabyteAmount(c.HumanReadable, newStat.Mem.Mapped)
263	}
264	return
265}
266
267func ReadVSize(c *ReaderConfig, newStat, _ *ServerStatus) (val string) {
268	if util.IsTruthy(newStat.Mem.Supported) {
269		val = formatMegabyteAmount(c.HumanReadable, newStat.Mem.Virtual)
270	}
271	return
272}
273
274func ReadRes(c *ReaderConfig, newStat, _ *ServerStatus) (val string) {
275	if util.IsTruthy(newStat.Mem.Supported) {
276		val = formatMegabyteAmount(c.HumanReadable, newStat.Mem.Resident)
277	}
278	return
279}
280
281func ReadNonMapped(c *ReaderConfig, newStat, _ *ServerStatus) (val string) {
282	if util.IsTruthy(newStat.Mem.Supported) && !IsMongos(newStat) {
283		val = formatMegabyteAmount(c.HumanReadable, newStat.Mem.Virtual-newStat.Mem.Mapped)
284	}
285	return
286}
287
288func ReadFaults(_ *ReaderConfig, newStat, oldStat *ServerStatus) string {
289	if !IsMMAP(newStat) {
290		return "n/a"
291	}
292	var val int64 = -1
293	if oldStat.ExtraInfo != nil && newStat.ExtraInfo != nil &&
294		oldStat.ExtraInfo.PageFaults != nil && newStat.ExtraInfo.PageFaults != nil {
295		sampleSecs := float64(newStat.SampleTime.Sub(oldStat.SampleTime).Seconds())
296		val = diff(*(newStat.ExtraInfo.PageFaults), *(oldStat.ExtraInfo.PageFaults), sampleSecs)
297	}
298	return fmt.Sprintf("%d", val)
299}
300
301func ReadLRW(_ *ReaderConfig, newStat, oldStat *ServerStatus) (val string) {
302	if !IsMongos(newStat) && newStat.Locks != nil && oldStat.Locks != nil {
303		global, ok := oldStat.Locks["Global"]
304		if ok && global.AcquireCount != nil {
305			newColl, inNew := newStat.Locks["Collection"]
306			oldColl, inOld := oldStat.Locks["Collection"]
307			if inNew && inOld && newColl.AcquireWaitCount != nil && oldColl.AcquireWaitCount != nil {
308				rWait := newColl.AcquireWaitCount.Read - oldColl.AcquireWaitCount.Read
309				wWait := newColl.AcquireWaitCount.Write - oldColl.AcquireWaitCount.Write
310				rTotal := newColl.AcquireCount.Read - oldColl.AcquireCount.Read
311				wTotal := newColl.AcquireCount.Write - oldColl.AcquireCount.Write
312				r := percentageInt64(rWait, rTotal)
313				w := percentageInt64(wWait, wTotal)
314				val = fmt.Sprintf("%.1f%%|%.1f%%", r, w)
315			}
316		}
317	}
318	return
319}
320
321func ReadLRWT(_ *ReaderConfig, newStat, oldStat *ServerStatus) (val string) {
322	if !IsMongos(newStat) && newStat.Locks != nil && oldStat.Locks != nil {
323		global, ok := oldStat.Locks["Global"]
324		if ok && global.AcquireCount != nil {
325			newColl, inNew := newStat.Locks["Collection"]
326			oldColl, inOld := oldStat.Locks["Collection"]
327			if inNew && inOld && newColl.AcquireWaitCount != nil && oldColl.AcquireWaitCount != nil {
328				rWait := newColl.AcquireWaitCount.Read - oldColl.AcquireWaitCount.Read
329				wWait := newColl.AcquireWaitCount.Write - oldColl.AcquireWaitCount.Write
330				rAcquire := newColl.TimeAcquiringMicros.Read - oldColl.TimeAcquiringMicros.Read
331				wAcquire := newColl.TimeAcquiringMicros.Write - oldColl.TimeAcquiringMicros.Write
332				r := averageInt64(rAcquire, rWait)
333				w := averageInt64(wAcquire, wWait)
334				val = fmt.Sprintf("%v|%v", r, w)
335			}
336		}
337	}
338	return
339}
340
341func ReadLockedDB(_ *ReaderConfig, newStat, oldStat *ServerStatus) (val string) {
342	if !IsMongos(newStat) && newStat.Locks != nil && oldStat.Locks != nil {
343		global, ok := oldStat.Locks["Global"]
344		if !ok || global.AcquireCount == nil {
345			prevLocks := parseLocks(oldStat)
346			curLocks := parseLocks(newStat)
347			lockdiffs := computeLockDiffs(prevLocks, curLocks)
348			db := ""
349			var percentage string
350			if len(lockdiffs) == 0 {
351				if newStat.GlobalLock != nil {
352					percentage = fmt.Sprintf("%.1f", percentageInt64(newStat.GlobalLock.LockTime, newStat.GlobalLock.TotalTime))
353				}
354			} else {
355				// Get the entry with the highest lock
356				highestLocked := lockdiffs[len(lockdiffs)-1]
357				timeDiffMillis := newStat.UptimeMillis - oldStat.UptimeMillis
358				lockToReport := highestLocked.Writes
359
360				// if the highest locked namespace is not '.'
361				if highestLocked.Namespace != "." {
362					for _, namespaceLockInfo := range lockdiffs {
363						if namespaceLockInfo.Namespace == "." {
364							lockToReport += namespaceLockInfo.Writes
365						}
366					}
367				}
368
369				// lock data is in microseconds and uptime is in milliseconds - so
370				// divide by 1000 so that the units match
371				lockToReport /= 1000
372
373				db = highestLocked.Namespace
374				percentage = fmt.Sprintf("%.1f", percentageInt64(lockToReport, timeDiffMillis))
375			}
376			if percentage != "" {
377				val = fmt.Sprintf("%s:%s%%", db, percentage)
378			}
379		}
380	}
381	return
382}
383
384func ReadQRW(_ *ReaderConfig, newStat, _ *ServerStatus) string {
385	var qr int64
386	var qw int64
387	gl := newStat.GlobalLock
388	if gl != nil && gl.CurrentQueue != nil {
389		// If we have wiredtiger stats, use those instead
390		if newStat.WiredTiger != nil {
391			qr = gl.CurrentQueue.Readers + gl.ActiveClients.Readers - newStat.WiredTiger.Concurrent.Read.Out
392			qw = gl.CurrentQueue.Writers + gl.ActiveClients.Writers - newStat.WiredTiger.Concurrent.Write.Out
393			if qr < 0 {
394				qr = 0
395			}
396			if qw < 0 {
397				qw = 0
398			}
399		} else {
400			qr = gl.CurrentQueue.Readers
401			qw = gl.CurrentQueue.Writers
402		}
403	}
404	return fmt.Sprintf("%v|%v", qr, qw)
405}
406
407func ReadARW(_ *ReaderConfig, newStat, _ *ServerStatus) string {
408	var ar int64
409	var aw int64
410	if gl := newStat.GlobalLock; gl != nil {
411		if newStat.WiredTiger != nil {
412			ar = newStat.WiredTiger.Concurrent.Read.Out
413			aw = newStat.WiredTiger.Concurrent.Write.Out
414		} else if newStat.GlobalLock.ActiveClients != nil {
415			ar = gl.ActiveClients.Readers
416			aw = gl.ActiveClients.Writers
417		}
418	}
419	return fmt.Sprintf("%v|%v", ar, aw)
420}
421
422func ReadNetIn(c *ReaderConfig, newStat, oldStat *ServerStatus) string {
423	sampleSecs := float64(newStat.SampleTime.Sub(oldStat.SampleTime).Seconds())
424	val := diff(newStat.Network.BytesIn, oldStat.Network.BytesIn, sampleSecs)
425	return formatBits(c.HumanReadable, val)
426}
427
428func ReadNetOut(c *ReaderConfig, newStat, oldStat *ServerStatus) string {
429	sampleSecs := float64(newStat.SampleTime.Sub(oldStat.SampleTime).Seconds())
430	val := diff(newStat.Network.BytesOut, oldStat.Network.BytesOut, sampleSecs)
431	return formatBits(c.HumanReadable, val)
432}
433
434func ReadConn(_ *ReaderConfig, newStat, _ *ServerStatus) string {
435	return fmt.Sprintf("%d", newStat.Connections.Current)
436}
437
438func ReadSet(_ *ReaderConfig, newStat, _ *ServerStatus) (name string) {
439	if newStat.Repl != nil {
440		name = newStat.Repl.SetName
441	}
442	return
443}
444
445func ReadRepl(_ *ReaderConfig, newStat, _ *ServerStatus) string {
446	switch {
447	case newStat.Repl == nil && IsMongos(newStat):
448		return "RTR"
449	case newStat.Repl == nil:
450		return ""
451	case util.IsTruthy(newStat.Repl.IsMaster):
452		return "PRI"
453	case util.IsTruthy(newStat.Repl.Secondary):
454		return "SEC"
455	case util.IsTruthy(newStat.Repl.IsReplicaSet):
456		return "REC"
457	case util.IsTruthy(newStat.Repl.ArbiterOnly):
458		return "ARB"
459	case util.SliceContains(newStat.Repl.Passives, newStat.Repl.Me):
460		return "PSV"
461	default:
462		if !IsReplSet(newStat) {
463			return "UNK"
464		}
465		return "SLV"
466	}
467}
468
469func ReadTime(c *ReaderConfig, newStat, _ *ServerStatus) string {
470	if c.TimeFormat != "" {
471		return newStat.SampleTime.Format(c.TimeFormat)
472	}
473	if c.HumanReadable {
474		return newStat.SampleTime.Format(time.StampMilli)
475	}
476	return newStat.SampleTime.Format(time.RFC3339)
477}
478
479func ReadStatField(field string, stat *ServerStatus) string {
480	val, ok := stat.Flattened[field]
481	if ok {
482		return fmt.Sprintf("%v", val)
483	}
484	return "INVALID"
485}
486
487func ReadStatDiff(field string, newStat, oldStat *ServerStatus) string {
488	new, validNew := newStat.Flattened[field]
489	old, validOld := oldStat.Flattened[field]
490	if validNew && validOld {
491		new, validNew := numberToInt64(new)
492		old, validOld := numberToInt64(old)
493		if validNew && validOld {
494			return fmt.Sprintf("%v", new-old)
495		}
496	}
497	return "INVALID"
498}
499
500func ReadStatRate(field string, newStat, oldStat *ServerStatus) string {
501	sampleSecs := float64(newStat.SampleTime.Sub(oldStat.SampleTime).Seconds())
502	new, validNew := newStat.Flattened[field]
503	old, validOld := oldStat.Flattened[field]
504	if validNew && validOld {
505		new, validNew := numberToInt64(new)
506		old, validOld := numberToInt64(old)
507		if validNew && validOld {
508			return fmt.Sprintf("%v", diff(new, old, sampleSecs))
509		}
510	}
511	return "INVALID"
512}
513
514var literalRE = regexp.MustCompile(`^(.*?)(\.(\w+)\(\))?$`)
515
516func InterpretField(field string, newStat, oldStat *ServerStatus) string {
517	match := literalRE.FindStringSubmatch(field)
518	if len(match) == 4 {
519		switch match[3] {
520		case "diff":
521			return ReadStatDiff(match[1], newStat, oldStat)
522		case "rate":
523			return ReadStatRate(match[1], newStat, oldStat)
524		}
525	}
526	return ReadStatField(field, newStat)
527}
528