github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/status/log_send.go (about) 1 // Copyright 2019 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package status 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "mime/multipart" 11 "os" 12 "regexp" 13 "strings" 14 "time" 15 16 humanize "github.com/dustin/go-humanize" 17 "github.com/keybase/client/go/libkb" 18 "github.com/keybase/client/go/protocol/keybase1" 19 ) 20 21 const ( 22 // After gzipping the logs we compress by this factor on avg. We use this 23 // to calculate the amount of raw log bytes we should read when sending. 24 AvgCompressionRatio = 5 25 LogSendDefaultBytesDesktop = 1024 * 1024 * 16 26 // NOTE: On mobile we may store less than the number of bytes we attempt to 27 // send. See go/libkb/env.go:Env.GetLogFileConfig 28 LogSendDefaultBytesMobileWifi = 1024 * 1024 * 10 29 LogSendDefaultBytesMobileNoWifi = 1024 * 1024 * 1 30 LogSendMaxBytes = 1024 * 1024 * 128 31 ) 32 33 // Logs is the struct to specify the path of log files 34 type Logs struct { 35 GUI string 36 Kbfs string 37 Service string 38 EK string 39 Perf string 40 KbfsPerf string 41 GitPerf string 42 Updater string 43 Start string 44 Install string 45 System string 46 Git string 47 Trace string 48 CPUProfile string 49 Watchdog string 50 Processes string 51 } 52 53 // LogSendContext for LogSend 54 type LogSendContext struct { 55 libkb.Contextified 56 57 InstallID libkb.InstallID 58 UID keybase1.UID 59 StatusJSON string 60 NetworkStatsJSON string 61 Feedback string 62 63 Logs Logs 64 65 kbfsLog string 66 svcLog string 67 ekLog string 68 perfLog string 69 desktopLog string 70 updaterLog string 71 startLog string 72 installLog string 73 systemLog string 74 gitLog string 75 traceBundle []byte 76 cpuProfileBundle []byte 77 watchdogLog string 78 processesLog string 79 } 80 81 var noncharacterRxx = regexp.MustCompile(`[^\w]`) 82 83 const redactedReplacer = "[REDACTED]" 84 const serialPaperKeyWordThreshold = 6 85 86 func redactPotentialPaperKeys(s string) string { 87 doubleDelimited := noncharacterRxx.ReplaceAllFunc([]byte(s), func(x []byte) []byte { 88 return []byte{'~', '~', '~', x[0], '~', '~', '~'} // regexp is single char so we can take first elem 89 }) 90 allWords := strings.Split(string(doubleDelimited), "~~~") 91 var checkWords []string 92 var checkWordLocations []int // keep track of each checkWord's index in allWords 93 for idx, word := range allWords { 94 if !(len(word) == 1 && noncharacterRxx.MatchString(word)) { 95 checkWords = append(checkWords, word) 96 checkWordLocations = append(checkWordLocations, idx) 97 } 98 } 99 didRedact := false 100 start := -1 101 for idx, word := range checkWords { 102 if !libkb.ValidSecWord(word) { 103 start = -1 104 continue 105 } 106 switch { 107 case start == -1: 108 start = idx 109 case idx-start+1 == serialPaperKeyWordThreshold: 110 for jdx := start; jdx <= idx; jdx++ { 111 allWords[checkWordLocations[jdx]] = redactedReplacer 112 } 113 didRedact = true 114 case idx-start+1 > serialPaperKeyWordThreshold: 115 allWords[checkWordLocations[idx]] = redactedReplacer 116 } 117 } 118 if didRedact { 119 return "[redacted feedback follows] " + strings.Join(allWords, "") 120 } 121 return s 122 } 123 124 func NewLogSendContext(g *libkb.GlobalContext, fstatus *keybase1.FullStatus, statusJSON, networkStatsJSON, feedback string) *LogSendContext { 125 logs := logFilesFromStatus(g, fstatus) 126 127 var uid keybase1.UID 128 if fstatus != nil && fstatus.CurStatus.User != nil { 129 uid = fstatus.CurStatus.User.Uid 130 } else { 131 uid = g.Env.GetUID() 132 } 133 if uid.IsNil() { 134 g.Log.Info("Not sending up a UID for logged in user; none found") 135 } 136 137 return &LogSendContext{ 138 Contextified: libkb.NewContextified(g), 139 UID: uid, 140 InstallID: g.Env.GetInstallID(), 141 StatusJSON: statusJSON, 142 NetworkStatsJSON: networkStatsJSON, 143 Feedback: feedback, 144 Logs: logs, 145 } 146 } 147 148 func (l *LogSendContext) post(mctx libkb.MetaContext) (keybase1.LogSendID, error) { 149 mctx.Debug("sending status + logs to keybase") 150 151 var body bytes.Buffer 152 mpart := multipart.NewWriter(&body) 153 154 if l.Feedback != "" { 155 feedback := redactPotentialPaperKeys(l.Feedback) 156 err := mpart.WriteField("feedback", feedback) 157 if err != nil { 158 return "", err 159 } 160 } 161 162 if len(l.InstallID) > 0 { 163 err := mpart.WriteField("install_id", string(l.InstallID)) 164 if err != nil { 165 return "", err 166 } 167 } 168 169 if !l.UID.IsNil() { 170 err := mpart.WriteField("uid", l.UID.String()) 171 if err != nil { 172 return "", err 173 } 174 } 175 176 if err := addGzippedFile(mpart, "status_gz", "status.gz", l.StatusJSON); err != nil { 177 return "", err 178 } 179 if err := addGzippedFile(mpart, "network_stats_gz", "network_stats.gz", l.NetworkStatsJSON); err != nil { 180 return "", err 181 } 182 if err := addGzippedFile(mpart, "kbfs_log_gz", "kbfs_log.gz", l.kbfsLog); err != nil { 183 return "", err 184 } 185 if err := addGzippedFile(mpart, "keybase_log_gz", "keybase_log.gz", l.svcLog); err != nil { 186 return "", err 187 } 188 if err := addGzippedFile(mpart, "ek_log_gz", "ek_log.gz", l.ekLog); err != nil { 189 return "", err 190 } 191 if err := addGzippedFile(mpart, "perf_log_gz", "perf_log.gz", l.perfLog); err != nil { 192 return "", err 193 } 194 if err := addGzippedFile(mpart, "updater_log_gz", "updater_log.gz", l.updaterLog); err != nil { 195 return "", err 196 } 197 if err := addGzippedFile(mpart, "gui_log_gz", "gui_log.gz", l.desktopLog); err != nil { 198 return "", err 199 } 200 if err := addGzippedFile(mpart, "start_log_gz", "start_log.gz", l.startLog); err != nil { 201 return "", err 202 } 203 if err := addGzippedFile(mpart, "install_log_gz", "install_log.gz", l.installLog); err != nil { 204 return "", err 205 } 206 if err := addGzippedFile(mpart, "system_log_gz", "system_log.gz", l.systemLog); err != nil { 207 return "", err 208 } 209 if err := addGzippedFile(mpart, "git_log_gz", "git_log.gz", l.gitLog); err != nil { 210 return "", err 211 } 212 if err := addGzippedFile(mpart, "watchdog_log_gz", "watchdog_log.gz", l.watchdogLog); err != nil { 213 return "", err 214 } 215 if err := addGzippedFile(mpart, "processes_log_gz", "processes_log.gz", l.processesLog); err != nil { 216 return "", err 217 } 218 219 if len(l.traceBundle) > 0 { 220 mctx.Debug("trace bundle size: %d", len(l.traceBundle)) 221 if err := addFile(mpart, "trace_tar_gz", "trace.tar.gz", l.traceBundle); err != nil { 222 return "", err 223 } 224 } 225 226 if len(l.cpuProfileBundle) > 0 { 227 mctx.Debug("CPU profile bundle size: %d", len(l.cpuProfileBundle)) 228 if err := addFile(mpart, "cpu_profile_tar_gz", "cpu_profile.tar.gz", l.cpuProfileBundle); err != nil { 229 return "", err 230 } 231 } 232 233 if err := mpart.Close(); err != nil { 234 return "", err 235 } 236 237 mctx.Debug("body size: %s", humanize.Bytes(uint64(body.Len()))) 238 239 arg := libkb.APIArg{ 240 Endpoint: "logdump/send", 241 SessionType: libkb.APISessionTypeOPTIONAL, 242 RetryCount: 2, 243 RetryMultiplier: 1.3, 244 InitialTimeout: 5 * time.Minute, 245 } 246 247 resp, err := mctx.G().API.PostRaw(mctx, arg, mpart.FormDataContentType(), &body) 248 if err != nil { 249 mctx.Debug("post error: %s", err) 250 return "", err 251 } 252 253 id, err := resp.Body.AtKey("logdump_id").GetString() 254 if err != nil { 255 return "", err 256 } 257 258 return keybase1.LogSendID(id), nil 259 } 260 261 // LogSend sends the tails of log files to kb, and also the last few trace 262 // output files. 263 func (l *LogSendContext) LogSend(sendLogs bool, numBytes int, mergeExtendedStatus, addNetworkStats bool) (id keybase1.LogSendID, err error) { 264 if numBytes < 1 { 265 numBytes = LogSendDefaultBytesDesktop 266 } else if numBytes > LogSendMaxBytes { 267 numBytes = LogSendMaxBytes 268 } 269 mctx := libkb.NewMetaContextBackground(l.G()).WithLogTag("LOGSEND") 270 defer mctx.Trace(fmt.Sprintf("LogSend sendLogs: %v numBytes: %s", 271 sendLogs, humanize.Bytes(uint64(numBytes))), &err)() 272 273 logs := l.Logs 274 // So far, install logs are Windows only 275 if logs.Install != "" { 276 defer os.Remove(logs.Install) 277 } 278 // So far, watchdog logs are Windows only 279 if logs.Watchdog != "" { 280 defer os.Remove(logs.Watchdog) 281 } 282 283 if sendLogs { 284 // Increase some log files by the average compression ratio size 285 // so we have more comprehensive coverage there. 286 l.svcLog = tail(l.G().Log, "service", logs.Service, numBytes*AvgCompressionRatio) 287 l.ekLog = tail(l.G().Log, "ek", logs.EK, numBytes) 288 289 { 290 // Scope these logs so they can be GC'd after this block. 291 servicePerfLog := tail(l.G().Log, "perf", logs.Perf, numBytes) 292 kbfsPerfLog := tail(l.G().Log, "kbfsPerf", logs.KbfsPerf, numBytes) 293 gitPerfLog := tail(l.G().Log, "gitPerf", logs.GitPerf, numBytes) 294 l.perfLog = zipLogs( 295 numBytes, servicePerfLog, kbfsPerfLog, gitPerfLog) 296 } 297 298 l.kbfsLog = tail(l.G().Log, "kbfs", logs.Kbfs, numBytes*AvgCompressionRatio) 299 l.desktopLog = tail(l.G().Log, "gui", logs.GUI, numBytes) 300 l.updaterLog = tail(l.G().Log, "updater", logs.Updater, numBytes) 301 // We don't use the systemd journal to store regular logs, since on 302 // some systems (e.g. Ubuntu 16.04) it's not persisted across boots. 303 // However we do use it for startup logs, since that's the only place 304 // to get them in systemd mode. 305 if l.G().Env.WantsSystemd() { 306 l.startLog = tailSystemdJournal(l.G().Log, []string{ 307 "keybase.service", 308 "kbfs.service", 309 "keybase.gui.service", 310 "keybase-redirector.service", 311 }, numBytes) 312 } else { 313 l.startLog = tail(l.G().Log, "start", logs.Start, numBytes) 314 } 315 l.installLog = tail(l.G().Log, "install", logs.Install, numBytes) 316 l.systemLog = tail(l.G().Log, "system", logs.System, numBytes) 317 l.gitLog = tail(l.G().Log, "git", logs.Git, numBytes) 318 l.watchdogLog = tail(l.G().Log, "watchdog", logs.Watchdog, numBytes) 319 if logs.Trace != "" { 320 l.traceBundle = getTraceBundle(l.G().Log, logs.Trace) 321 } 322 if logs.CPUProfile != "" { 323 l.cpuProfileBundle = getCPUProfileBundle(l.G().Log, logs.CPUProfile) 324 } 325 // Only add extended status if we're sending logs 326 if mergeExtendedStatus { 327 l.StatusJSON = l.mergeExtendedStatus(l.StatusJSON) 328 } 329 if addNetworkStats { 330 localStats, err := mctx.G().LocalNetworkInstrumenterStorage.Stats(mctx.Ctx()) 331 if err != nil { 332 return "", err 333 } 334 remoteStats, err := mctx.G().RemoteNetworkInstrumenterStorage.Stats(mctx.Ctx()) 335 if err != nil { 336 return "", err 337 } 338 networkStatsJSON, err := json.Marshal(libkb.NetworkStatsJSON{ 339 Local: localStats, 340 Remote: remoteStats, 341 }) 342 if err != nil { 343 return "", err 344 } 345 l.NetworkStatsJSON = string(networkStatsJSON) 346 } 347 l.processesLog = keybaseProcessList() 348 } 349 350 return l.post(mctx) 351 } 352 353 // mergeExtendedStatus adds the extended status to the given status json blob. 354 // If any errors occur the original status is returned unmodified. 355 func (l *LogSendContext) mergeExtendedStatus(status string) string { 356 extStatus, err := GetExtendedStatus(libkb.NewMetaContextTODO(l.G())) 357 if err != nil { 358 return status 359 } 360 return MergeStatusJSON(extStatus, "extstatus", status) 361 } 362 363 // Clear removes any log data that we don't want to stick around until the 364 // next time LogSend is called, in case sendLogs is false the next time. 365 func (l *LogSendContext) Clear() { 366 l.svcLog = "" 367 l.ekLog = "" 368 l.perfLog = "" 369 l.kbfsLog = "" 370 l.desktopLog = "" 371 l.updaterLog = "" 372 l.startLog = "" 373 l.installLog = "" 374 l.systemLog = "" 375 l.gitLog = "" 376 l.watchdogLog = "" 377 l.traceBundle = []byte{} 378 l.cpuProfileBundle = []byte{} 379 l.processesLog = "" 380 l.StatusJSON = "" 381 l.NetworkStatsJSON = "" 382 }