github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/network_instrumenter.go (about) 1 package libkb 2 3 import ( 4 "fmt" 5 "net/http" 6 "strings" 7 "sync" 8 "time" 9 10 humanize "github.com/dustin/go-humanize" 11 keybase1 "github.com/keybase/client/go/protocol/keybase1" 12 "github.com/keybase/go-framed-msgpack-rpc/rpc" 13 context "golang.org/x/net/context" 14 "golang.org/x/sync/errgroup" 15 ) 16 17 type NetworkStatsJSON struct { 18 Local []keybase1.InstrumentationStat `json:"local"` 19 Remote []keybase1.InstrumentationStat `json:"remote"` 20 } 21 22 var internalHosts = map[string]struct{}{ 23 DevelServerURI: {}, 24 StagingServerURI: {}, 25 ProductionServerURI: {}, 26 ProductionSiteURI: {}, 27 } 28 29 func InstrumentationTagFromRequest(req *http.Request) string { 30 if req.URL == nil { 31 return "" 32 } 33 host := req.URL.Host 34 path := req.URL.Path 35 if _, ok := internalHosts[fmt.Sprintf("%s://%s", req.URL.Scheme, host)]; ok { 36 host = "" 37 path = strings.TrimPrefix(req.URL.Path, APIURIPathPrefix) 38 path = strings.TrimPrefix(path, "/") 39 } 40 return fmt.Sprintf("%s %s%s", req.Method, host, path) 41 } 42 43 func AddRPCRecord(tag string, stat keybase1.InstrumentationStat, record rpc.InstrumentationRecord) keybase1.InstrumentationStat { 44 if stat.NumCalls == 0 { 45 stat.Ctime = keybase1.ToTime(time.Now()) 46 } 47 if stat.Tag == "" { 48 stat.Tag = tag 49 } 50 stat.Mtime = keybase1.ToTime(time.Now()) 51 stat.NumCalls++ 52 dur := keybase1.ToDurationMsec(record.Dur) 53 stat.TotalDur += dur 54 if dur > stat.MaxDur { 55 stat.MaxDur = dur 56 } 57 if dur < stat.MinDur || stat.NumCalls == 1 { 58 stat.MinDur = dur 59 } 60 61 stat.TotalSize += record.Size 62 if record.Size > stat.MaxSize { 63 stat.MaxSize = record.Size 64 } 65 if record.Size < stat.MinSize || stat.NumCalls == 1 { 66 stat.MinSize = record.Size 67 } 68 69 stat.AvgDur = stat.TotalDur / keybase1.DurationMsec(stat.NumCalls) 70 stat.AvgSize = stat.TotalSize / int64(stat.NumCalls) 71 return stat 72 } 73 74 type DiskInstrumentationStorage struct { 75 Contextified 76 sync.Mutex 77 src keybase1.NetworkSource 78 storage map[string]keybase1.InstrumentationStat 79 80 eg errgroup.Group 81 stopCh chan struct{} 82 started bool 83 } 84 85 var _ rpc.NetworkInstrumenterStorage = (*DiskInstrumentationStorage)(nil) 86 87 func NewDiskInstrumentationStorage(g *GlobalContext, src keybase1.NetworkSource) *DiskInstrumentationStorage { 88 return &DiskInstrumentationStorage{ 89 Contextified: NewContextified(g), 90 src: src, 91 storage: make(map[string]keybase1.InstrumentationStat), 92 } 93 } 94 95 func (s *DiskInstrumentationStorage) Start(ctx context.Context) { 96 defer s.G().CTrace(ctx, "DiskInstrumentationStorage: Start", nil)() 97 s.Lock() 98 defer s.Unlock() 99 if s.started { 100 return 101 } 102 s.stopCh = make(chan struct{}) 103 s.started = true 104 s.eg.Go(func() error { return s.flushLoop(s.stopCh) }) 105 } 106 107 func (s *DiskInstrumentationStorage) Stop(ctx context.Context) chan struct{} { 108 defer s.G().CTrace(ctx, "DiskInstrumentationStorage: Stop", nil)() 109 s.Lock() 110 defer s.Unlock() 111 ch := make(chan struct{}) 112 if s.started { 113 close(s.stopCh) 114 s.started = false 115 go func() { 116 if err := s.eg.Wait(); err != nil { 117 s.G().Log.Debug("DiskInstrumentationStorage: flush: unable to wait for shutdown: %v", err) 118 } 119 close(ch) 120 }() 121 } else { 122 close(ch) 123 } 124 return ch 125 } 126 127 func (s *DiskInstrumentationStorage) flushLoop(stopCh chan struct{}) error { 128 ctx := context.Background() 129 for { 130 select { 131 case <-stopCh: 132 return s.Flush(ctx) 133 case <-time.After(5 * time.Minute): 134 if err := s.Flush(ctx); err != nil { 135 s.G().Log.CDebugf(ctx, "DiskInstrumentationStorage: flushLoop: unable to flush: %v", err) 136 } 137 } 138 } 139 } 140 141 func (s *DiskInstrumentationStorage) Flush(ctx context.Context) (err error) { 142 s.Lock() 143 storage := s.storage 144 s.storage = make(map[string]keybase1.InstrumentationStat) 145 s.Unlock() 146 return s.flush(ctx, storage) 147 } 148 149 func (s *DiskInstrumentationStorage) flush(ctx context.Context, storage map[string]keybase1.InstrumentationStat) (err error) { 150 defer s.G().CTrace(ctx, "DiskInstrumentationStorage: flush", &err)() 151 for tag, record := range storage { 152 var existing keybase1.InstrumentationStat 153 found, err := s.G().LocalDb.GetIntoMsgpack(&existing, s.dbKey(tag)) 154 if err != nil { 155 return err 156 } 157 // Keep only window of the past month 158 if found && time.Since(existing.Ctime.Time()) <= time.Hour*24*30 { 159 record = existing.AppendStat(record) 160 } 161 if err := s.G().LocalDb.PutObjMsgpack(s.dbKey(tag), nil, record); err != nil { 162 return err 163 } 164 } 165 return nil 166 } 167 168 func (s *DiskInstrumentationStorage) keyPrefix() string { 169 return fmt.Sprintf("src:%d", s.src) 170 } 171 172 func (s *DiskInstrumentationStorage) dbKey(tag string) DbKey { 173 return DbKey{ 174 Typ: DBNetworkInstrumentation, 175 Key: fmt.Sprintf("%s|%s", s.keyPrefix(), tag), 176 } 177 } 178 179 func (s *DiskInstrumentationStorage) getAllKeysLocked() (keys []DbKey, err error) { 180 prefix := DbKey{ 181 Typ: DBNetworkInstrumentation, 182 }.ToBytes() 183 dbKeys, err := s.G().LocalDb.KeysWithPrefixes(prefix) 184 if err != nil { 185 return nil, fmt.Errorf("could not get KeysWithPrefixes: %v", err) 186 } 187 keys = make([]DbKey, 0, len(dbKeys)) 188 for dbKey := range dbKeys { 189 if dbKey.Typ == DBNetworkInstrumentation { 190 keys = append(keys, dbKey) 191 } 192 } 193 return keys, nil 194 } 195 196 func (s *DiskInstrumentationStorage) GetAll(ctx context.Context) (res []keybase1.InstrumentationStat, err error) { 197 defer s.G().CTrace(ctx, "DiskInstrumentationStorage: GetAll", &err)() 198 s.Lock() 199 defer s.Unlock() 200 201 if err := s.flush(ctx, s.storage); err != nil { 202 return nil, err 203 } 204 205 dbKeys, err := s.getAllKeysLocked() 206 if err != nil { 207 return nil, err 208 } 209 keyPrefix := s.keyPrefix() 210 for _, dbKey := range dbKeys { 211 // ensure key matches expected format 212 keyParts := strings.Split(dbKey.Key, "|") 213 if len(keyParts) < 2 || keyParts[0] != keyPrefix { 214 continue 215 } 216 var record keybase1.InstrumentationStat 217 ok, err := s.G().LocalDb.GetIntoMsgpack(&record, dbKey) 218 if err != nil { 219 return nil, err 220 } else if !ok { 221 continue 222 } 223 res = append(res, record) 224 } 225 return res, nil 226 } 227 228 func (s *DiskInstrumentationStorage) Stats(ctx context.Context) (res []keybase1.InstrumentationStat, err error) { 229 defer s.G().CTrace(ctx, "DiskInstrumentationStorage: Stats", &err)() 230 return s.GetAll(ctx) 231 } 232 233 var tagLogBlacklist = map[string]struct{}{ 234 "Call gregor.1.incoming.ping": {}, 235 } 236 237 func (s *DiskInstrumentationStorage) logRecord(ctx context.Context, tag string, record rpc.InstrumentationRecord) { 238 if s.src == keybase1.NetworkSource_LOCAL { 239 return 240 } 241 if _, ok := tagLogBlacklist[tag]; !ok { 242 s.G().PerfLog.CDebugf(ctx, "%s %v %s", tag, record.Dur, humanize.Bytes(uint64(record.Size))) 243 s.G().RuntimeStats.PushPerfEvent(keybase1.PerfEvent{ 244 EventType: keybase1.PerfEventType_NETWORK, 245 Message: tag, 246 Ctime: keybase1.ToTime(record.Ctime), 247 }) 248 } 249 } 250 251 func (s *DiskInstrumentationStorage) Put(ctx context.Context, tag string, record rpc.InstrumentationRecord) error { 252 s.Lock() 253 defer s.Unlock() 254 s.storage[tag] = AddRPCRecord(tag, s.storage[tag], record) 255 s.logRecord(ctx, tag, record) 256 return nil 257 } 258 259 func NetworkInstrumenterStorageFromSrc(g *GlobalContext, src keybase1.NetworkSource) rpc.NetworkInstrumenterStorage { 260 switch src { 261 case keybase1.NetworkSource_LOCAL: 262 return g.LocalNetworkInstrumenterStorage 263 case keybase1.NetworkSource_REMOTE: 264 return g.RemoteNetworkInstrumenterStorage 265 default: 266 return rpc.NewDummyInstrumentationStorage() 267 } 268 }