Files
percona-toolkit/src/go/mongolib/stats/stats.go
2025-02-18 15:01:34 +01:00

450 lines
13 KiB
Go

package stats
import (
"crypto/md5"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/montanaflynn/stats"
"go.mongodb.org/mongo-driver/bson"
"github.com/percona/percona-toolkit/src/go/mongolib/proto"
)
const (
planSummaryCollScan = "COLLSCAN"
planSummaryIXScan = "IXSCAN"
)
type StatsError struct {
error
}
func (e *StatsError) Error() string {
if e == nil {
return "<nil>"
}
return fmt.Sprintf("stats error: %s", e.error)
}
func (e *StatsError) Parent() error {
return e.error
}
type StatsFingerprintError StatsError
// New creates new instance of stats with given Fingerprinter
func New(fingerprinter Fingerprinter) *Stats {
s := &Stats{
fingerprinter: fingerprinter,
}
s.Reset()
return s
}
// Stats is a collection of MongoDB statistics
type Stats struct {
// dependencies
fingerprinter Fingerprinter
// internal
queryInfoAndCounters map[GroupKey]*QueryInfoAndCounters
sync.RWMutex
}
// Reset clears the collection of statistics
func (s *Stats) Reset() {
s.Lock()
defer s.Unlock()
s.queryInfoAndCounters = make(map[GroupKey]*QueryInfoAndCounters)
}
// Add adds proto.SystemProfile to the collection of statistics
func (s *Stats) Add(doc proto.SystemProfile) error {
fp, err := s.fingerprinter.Fingerprint(doc)
if err != nil {
return &StatsFingerprintError{err}
}
var qiac *QueryInfoAndCounters
var ok bool
key := GroupKey{
Operation: fp.Operation,
Fingerprint: fp.Fingerprint,
Namespace: fp.Namespace,
}
if qiac, ok = s.getQueryInfoAndCounters(key); !ok {
query := proto.NewExampleQuery(doc)
queryBson, err := bson.MarshalExtJSON(query, true, true)
if err != nil {
return err
}
qiac = &QueryInfoAndCounters{
ID: fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s", key)))),
Operation: fp.Operation,
Fingerprint: fp.Fingerprint,
Namespace: fp.Namespace,
TableScan: false,
Query: string(queryBson),
PlanSummary: doc.PlanSummary,
QueryHash: doc.QueryHash,
AppName: doc.AppName,
Client: doc.Client,
User: strings.Split(doc.User, "@")[0],
Comments: doc.Comments,
}
s.setQueryInfoAndCounters(key, qiac)
}
qiac.Count++
s.Lock()
if qiac.PlanSummary == planSummaryCollScan {
qiac.CollScanCount++
}
if strings.HasPrefix(qiac.PlanSummary, planSummaryIXScan) {
qiac.PlanSummary = planSummaryIXScan
}
qiac.NReturned = append(qiac.NReturned, float64(doc.Nreturned))
qiac.QueryTime = append(qiac.QueryTime, float64(doc.Millis))
qiac.ResponseLength = append(qiac.ResponseLength, float64(doc.ResponseLength))
if qiac.FirstSeen.IsZero() || qiac.FirstSeen.After(doc.Ts) {
qiac.FirstSeen = doc.Ts
}
if qiac.LastSeen.IsZero() || qiac.LastSeen.Before(doc.Ts) {
qiac.LastSeen = doc.Ts
}
if doc.Storage.Data.BytesRead > 0 {
qiac.StorageBytesRead = append(qiac.StorageBytesRead, float64(doc.Storage.Data.BytesRead))
}
if doc.Storage.Data.TimeReadingMicros > 0 {
qiac.StorageTimeReadingMicros = append(qiac.StorageTimeReadingMicros, float64(doc.Storage.Data.TimeReadingMicros))
}
qiac.LocksGlobalAcquireCountReadShared += doc.Locks.Global.AcquireCount.ReadShared
qiac.LocksGlobalAcquireCountWriteShared += doc.Locks.Global.AcquireCount.WriteShared
qiac.LocksDatabaseAcquireCountReadShared += doc.Locks.Database.AcquireCount.ReadShared
qiac.LocksDatabaseAcquireWaitCountReadShared += doc.Locks.Database.AcquireWaitCount.ReadShared
if doc.Locks.Database.TimeAcquiringMicros.ReadShared > 0 {
qiac.LocksDatabaseTimeAcquiringMicrosReadShared = append(qiac.LocksDatabaseTimeAcquiringMicrosReadShared, float64(doc.Locks.Database.TimeAcquiringMicros.ReadShared))
}
qiac.LocksCollectionAcquireCountReadShared += doc.Locks.Collection.AcquireCount.ReadShared
if doc.DocsExamined > 0 {
qiac.DocsExamined = append(qiac.DocsExamined, float64(doc.DocsExamined))
}
if doc.KeysExamined > 0 {
qiac.KeysExamined = append(qiac.KeysExamined, float64(doc.KeysExamined))
}
s.Unlock()
return nil
}
// Queries returns all collected statistics
func (s *Stats) Queries() Queries {
s.Lock()
defer s.Unlock()
keys := GroupKeys{}
for key := range s.queryInfoAndCounters {
keys = append(keys, key)
}
sort.Sort(keys)
queries := []QueryInfoAndCounters{}
for _, key := range keys {
queries = append(queries, *s.queryInfoAndCounters[key])
}
return queries
}
func (s *Stats) getQueryInfoAndCounters(key GroupKey) (*QueryInfoAndCounters, bool) {
s.RLock()
defer s.RUnlock()
v, ok := s.queryInfoAndCounters[key]
return v, ok
}
func (s *Stats) setQueryInfoAndCounters(key GroupKey, value *QueryInfoAndCounters) {
s.Lock()
defer s.Unlock()
s.queryInfoAndCounters[key] = value
}
// Queries is a slice of MongoDB statistics
type Queries []QueryInfoAndCounters
// CalcQueriesStats calculates QueryStats for given uptime
func (q Queries) CalcQueriesStats(uptime int64) []QueryStats {
qs := []QueryStats{}
tc := calcTotalCounters(q)
for _, query := range q {
queryStats := countersToStats(query, uptime, tc)
qs = append(qs, queryStats)
}
return qs
}
// CalcTotalQueriesStats calculates total QueryStats for given uptime
func (q Queries) CalcTotalQueriesStats(uptime int64) QueryStats {
tc := calcTotalCounters(q)
totalQueryInfoAndCounters := aggregateCounters(q)
totalStats := countersToStats(totalQueryInfoAndCounters, uptime, tc)
return totalStats
}
type QueryInfoAndCounters struct {
ID string
Namespace string
Operation string
Query string
Fingerprint string
FirstSeen time.Time
LastSeen time.Time
TableScan bool
Count int
BlockedTime Times
LockTime Times
NReturned []float64
QueryTime []float64 // in milliseconds
ResponseLength []float64
PlanSummary string
CollScanCount int
DocsExamined []float64
KeysExamined []float64
QueryHash string
AppName string
Client string
User string
Comments string
LocksGlobalAcquireCountReadShared int
LocksGlobalAcquireCountWriteShared int
LocksDatabaseAcquireCountReadShared int
LocksDatabaseAcquireWaitCountReadShared int
LocksDatabaseTimeAcquiringMicrosReadShared []float64 // in microseconds
LocksCollectionAcquireCountReadShared int
StorageBytesRead []float64
StorageTimeReadingMicros []float64 // in microseconds
}
// times is an array of time.Time that implements the Sorter interface
type Times []time.Time
func (a Times) Len() int { return len(a) }
func (a Times) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a Times) Less(i, j int) bool { return a[i].Before(a[j]) }
type GroupKeys []GroupKey
func (a GroupKeys) Len() int { return len(a) }
func (a GroupKeys) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a GroupKeys) Less(i, j int) bool { return a[i].String() < a[j].String() }
type GroupKey struct {
Operation string
Namespace string
Fingerprint string
}
func (g GroupKey) String() string {
return g.Operation + g.Namespace + g.Fingerprint
}
type totalCounters struct {
Count int
Scanned float64
Returned float64
QueryTime float64
Bytes float64
DocsExamined float64
KeysExamined float64
}
type QueryStats struct {
ID string
Namespace string
Operation string
Query string
Fingerprint string
FirstSeen time.Time
LastSeen time.Time
Count int
QPS float64
Rank int
Ratio float64
QueryTime Statistics
ResponseLengthCount int
ResponseLength Statistics
Returned Statistics
Scanned Statistics
PlanSummary string
CollScanCount int
DocsExaminedCount int
DocsExamined Statistics
KeysExaminedCount int
KeysExamined Statistics
QueryHash string
AppName string
Client string
User string
Comments string
LocksGlobalAcquireCountReadShared int
LocksGlobalAcquireCountWriteShared int
LocksDatabaseAcquireCountReadShared int
LocksDatabaseAcquireWaitCountReadShared int
LocksDatabaseTimeAcquiringMicrosReadSharedCount int
LocksDatabaseTimeAcquiringMicrosReadShared Statistics // in microseconds
LocksCollectionAcquireCountReadShared int
StorageBytesReadCount int
StorageBytesRead Statistics
StorageTimeReadingMicrosCount int
StorageTimeReadingMicros Statistics // in microseconds
}
type Statistics struct {
Pct float64
Total float64
Min float64
Max float64
Avg float64
Pct95 float64
Pct99 float64
StdDev float64
Median float64
}
func countersToStats(query QueryInfoAndCounters, uptime int64, tc totalCounters) QueryStats {
queryStats := QueryStats{
Count: query.Count,
ID: query.ID,
Operation: query.Operation,
Query: query.Query,
Fingerprint: query.Fingerprint,
Returned: calcStats(query.NReturned),
QueryTime: calcStats(query.QueryTime),
FirstSeen: query.FirstSeen,
LastSeen: query.LastSeen,
Namespace: query.Namespace,
QPS: float64(query.Count) / float64(uptime),
PlanSummary: query.PlanSummary,
CollScanCount: query.CollScanCount,
ResponseLengthCount: len(query.ResponseLength),
ResponseLength: calcStats(query.ResponseLength),
DocsExaminedCount: len(query.DocsExamined),
DocsExamined: calcStats(query.DocsExamined),
KeysExaminedCount: len(query.KeysExamined),
KeysExamined: calcStats(query.KeysExamined),
QueryHash: query.QueryHash,
AppName: query.AppName,
Client: query.Client,
User: query.User,
Comments: query.Comments,
LocksGlobalAcquireCountReadShared: query.LocksGlobalAcquireCountReadShared,
LocksGlobalAcquireCountWriteShared: query.LocksGlobalAcquireCountWriteShared,
LocksDatabaseAcquireCountReadShared: query.LocksDatabaseAcquireCountReadShared,
LocksDatabaseAcquireWaitCountReadShared: query.LocksDatabaseAcquireWaitCountReadShared,
LocksDatabaseTimeAcquiringMicrosReadSharedCount: len(query.LocksDatabaseTimeAcquiringMicrosReadShared),
LocksDatabaseTimeAcquiringMicrosReadShared: calcStats(query.LocksDatabaseTimeAcquiringMicrosReadShared),
LocksCollectionAcquireCountReadShared: query.LocksCollectionAcquireCountReadShared,
StorageBytesReadCount: len(query.StorageBytesRead),
StorageBytesRead: calcStats(query.StorageBytesRead),
StorageTimeReadingMicrosCount: len(query.StorageTimeReadingMicros),
StorageTimeReadingMicros: calcStats(query.StorageTimeReadingMicros),
}
if tc.Scanned > 0 {
queryStats.Scanned.Pct = queryStats.Scanned.Total * 100 / tc.Scanned
}
if tc.Returned > 0 {
queryStats.Returned.Pct = queryStats.Returned.Total * 100 / tc.Returned
}
if tc.QueryTime > 0 {
queryStats.QueryTime.Pct = queryStats.QueryTime.Total * 100 / tc.QueryTime
}
if tc.Bytes > 0 {
queryStats.ResponseLength.Pct = queryStats.ResponseLength.Total * 100 / tc.Bytes
}
if queryStats.Returned.Total > 0 {
queryStats.Ratio = queryStats.Scanned.Total / queryStats.Returned.Total
}
if tc.DocsExamined > 0 {
queryStats.DocsExamined.Pct = queryStats.DocsExamined.Total / tc.DocsExamined
}
if tc.KeysExamined > 0 {
queryStats.KeysExamined.Pct = queryStats.KeysExamined.Total / tc.KeysExamined
}
return queryStats
}
func aggregateCounters(queries []QueryInfoAndCounters) QueryInfoAndCounters {
qt := QueryInfoAndCounters{}
for _, query := range queries {
qt.Count += query.Count
qt.NReturned = append(qt.NReturned, query.NReturned...)
qt.QueryTime = append(qt.QueryTime, query.QueryTime...)
qt.ResponseLength = append(qt.ResponseLength, query.ResponseLength...)
qt.DocsExamined = append(qt.DocsExamined, query.DocsExamined...)
qt.KeysExamined = append(qt.KeysExamined, query.KeysExamined...)
}
return qt
}
func calcTotalCounters(queries []QueryInfoAndCounters) totalCounters {
tc := totalCounters{}
for _, query := range queries {
tc.Count += query.Count
returned, _ := stats.Sum(query.NReturned)
tc.Returned += returned
queryTime, _ := stats.Sum(query.QueryTime)
tc.QueryTime += queryTime
bytes, _ := stats.Sum(query.ResponseLength)
tc.Bytes += bytes
docsExamined, _ := stats.Sum(query.DocsExamined)
tc.DocsExamined += docsExamined
keysExamined, _ := stats.Sum(query.KeysExamined)
tc.KeysExamined += keysExamined
}
return tc
}
func calcStats(samples []float64) Statistics {
var s Statistics
s.Total, _ = stats.Sum(samples)
s.Min, _ = stats.Min(samples)
s.Max, _ = stats.Max(samples)
s.Avg, _ = stats.Mean(samples)
s.Pct95, _ = stats.PercentileNearestRank(samples, 95)
s.Pct99, _ = stats.PercentileNearestRank(samples, 99)
s.StdDev, _ = stats.StandardDeviation(samples)
s.Median, _ = stats.Median(samples)
return s
}