1// Copyright 2023 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package upload 6 7import ( 8 "crypto/rand" 9 "encoding/binary" 10 "encoding/json" 11 "fmt" 12 "math" 13 "os" 14 "path/filepath" 15 "strings" 16 "time" 17 18 "golang.org/x/telemetry/internal/config" 19 "golang.org/x/telemetry/internal/counter" 20 "golang.org/x/telemetry/internal/telemetry" 21) 22 23// reports generates reports from inactive count files 24func (u *uploader) reports(todo *work) ([]string, error) { 25 if mode, _ := u.dir.Mode(); mode == "off" { 26 return nil, nil // no reports 27 } 28 thisInstant := u.startTime 29 today := thisInstant.Format(time.DateOnly) 30 lastWeek := latestReport(todo.uploaded) 31 if lastWeek >= today { //should never happen 32 lastWeek = "" 33 } 34 u.logger.Printf("Last week: %s, today: %s", lastWeek, today) 35 countFiles := make(map[string][]string) // expiry date string->filenames 36 earliest := make(map[string]time.Time) // earliest begin time for any counter 37 for _, f := range todo.countfiles { 38 begin, end, err := u.counterDateSpan(f) 39 if err != nil { 40 // This shouldn't happen: we should have already skipped count files that 41 // don't contain valid start or end times. 42 u.logger.Printf("BUG: failed to parse expiry for collected count file: %v", err) 43 continue 44 } 45 46 if end.Before(thisInstant) { 47 expiry := end.Format(dateFormat) 48 countFiles[expiry] = append(countFiles[expiry], f) 49 if earliest[expiry].IsZero() || earliest[expiry].After(begin) { 50 earliest[expiry] = begin 51 } 52 } 53 } 54 for expiry, files := range countFiles { 55 if notNeeded(expiry, *todo) { 56 u.logger.Printf("Files for %s not needed, deleting %v", expiry, files) 57 // The report already exists. 58 // There's another check in createReport. 59 u.deleteFiles(files) 60 continue 61 } 62 fname, err := u.createReport(earliest[expiry], expiry, files, lastWeek) 63 if err != nil { 64 u.logger.Printf("Failed to create report for %s: %v", expiry, err) 65 continue 66 } 67 if fname != "" { 68 u.logger.Printf("Ready to upload: %s", filepath.Base(fname)) 69 todo.readyfiles = append(todo.readyfiles, fname) 70 } 71 } 72 return todo.readyfiles, nil 73} 74 75// latestReport returns the YYYY-MM-DD of the last report uploaded 76// or the empty string if there are no reports. 77func latestReport(uploaded map[string]bool) string { 78 var latest string 79 for name := range uploaded { 80 if strings.HasSuffix(name, ".json") { 81 if name > latest { 82 latest = name 83 } 84 } 85 } 86 if latest == "" { 87 return "" 88 } 89 // strip off the .json 90 return latest[:len(latest)-len(".json")] 91} 92 93// notNeeded returns true if the report for date has already been created 94func notNeeded(date string, todo work) bool { 95 if todo.uploaded != nil && todo.uploaded[date+".json"] { 96 return true 97 } 98 // maybe the report is already in todo.readyfiles 99 for _, f := range todo.readyfiles { 100 if strings.Contains(f, date) { 101 return true 102 } 103 } 104 return false 105} 106 107func (u *uploader) deleteFiles(files []string) { 108 for _, f := range files { 109 if err := os.Remove(f); err != nil { 110 // this could be a race condition. 111 // conversely, on Windows, err may be nil and 112 // the file not deleted if anyone has it open. 113 u.logger.Printf("%v failed to remove %s", err, f) 114 } 115 } 116} 117 118// createReport creates local and upload report files by 119// combining all the count files for the expiryDate, and 120// returns the upload report file's path. 121// It may delete the count files once local and upload report 122// files are successfully created. 123func (u *uploader) createReport(start time.Time, expiryDate string, countFiles []string, lastWeek string) (string, error) { 124 uploadOK := true 125 mode, asof := u.dir.Mode() 126 if mode != "on" { 127 u.logger.Printf("No upload config or mode %q is not 'on'", mode) 128 uploadOK = false // no config, nothing to upload 129 } 130 if u.tooOld(expiryDate, u.startTime) { 131 u.logger.Printf("Expiry date %s is too old", expiryDate) 132 uploadOK = false 133 } 134 // If the mode is recorded with an asof date, don't upload if the report 135 // includes any data on or before the asof date. 136 if !asof.IsZero() && !asof.Before(start) { 137 u.logger.Printf("As-of date %s is not before start %s", asof, start) 138 uploadOK = false 139 } 140 // TODO(rfindley): check that all the x.Meta are consistent for GOOS, GOARCH, etc. 141 report := &telemetry.Report{ 142 Config: u.configVersion, 143 X: computeRandom(), // json encodes all the bits 144 Week: expiryDate, 145 LastWeek: lastWeek, 146 } 147 if report.X > u.config.SampleRate && u.config.SampleRate > 0 { 148 u.logger.Printf("X: %f > SampleRate:%f, not uploadable", report.X, u.config.SampleRate) 149 uploadOK = false 150 } 151 var succeeded bool 152 for _, f := range countFiles { 153 fok := false 154 x, err := u.parseCountFile(f) 155 if err != nil { 156 u.logger.Printf("Unparseable count file %s: %v", filepath.Base(f), err) 157 continue 158 } 159 prog := findProgReport(x.Meta, report) 160 for k, v := range x.Count { 161 if counter.IsStackCounter(k) { 162 // stack 163 prog.Stacks[k] += int64(v) 164 } else { 165 // counter 166 prog.Counters[k] += int64(v) 167 } 168 succeeded = true 169 fok = true 170 } 171 if !fok { 172 u.logger.Printf("no counters found in %s", f) 173 } 174 } 175 if !succeeded { 176 return "", fmt.Errorf("none of the %d count files for %s contained counters", len(countFiles), expiryDate) 177 } 178 // 1. generate the local report 179 localContents, err := json.MarshalIndent(report, "", " ") 180 if err != nil { 181 return "", fmt.Errorf("failed to marshal report for %s: %v", expiryDate, err) 182 } 183 // check that the report can be read back 184 // TODO(pjw): remove for production? 185 var report2 telemetry.Report 186 if err := json.Unmarshal(localContents, &report2); err != nil { 187 return "", fmt.Errorf("failed to unmarshal local report for %s: %v", expiryDate, err) 188 } 189 190 var uploadContents []byte 191 if uploadOK { 192 // 2. create the uploadable version 193 cfg := config.NewConfig(u.config) 194 upload := &telemetry.Report{ 195 Week: report.Week, 196 LastWeek: report.LastWeek, 197 X: report.X, 198 Config: report.Config, 199 } 200 for _, p := range report.Programs { 201 // does the uploadConfig want this program? 202 // if so, copy over the Stacks and Counters 203 // that the uploadConfig mentions. 204 if !cfg.HasGoVersion(p.GoVersion) || !cfg.HasProgram(p.Program) || !cfg.HasVersion(p.Program, p.Version) { 205 continue 206 } 207 x := &telemetry.ProgramReport{ 208 Program: p.Program, 209 Version: p.Version, 210 GOOS: p.GOOS, 211 GOARCH: p.GOARCH, 212 GoVersion: p.GoVersion, 213 Counters: make(map[string]int64), 214 Stacks: make(map[string]int64), 215 } 216 upload.Programs = append(upload.Programs, x) 217 for k, v := range p.Counters { 218 if cfg.HasCounter(p.Program, k) && report.X <= cfg.Rate(p.Program, k) { 219 x.Counters[k] = v 220 } 221 } 222 // and the same for Stacks 223 // this can be made more efficient, when it matters 224 for k, v := range p.Stacks { 225 before, _, _ := strings.Cut(k, "\n") 226 if cfg.HasStack(p.Program, before) && report.X <= cfg.Rate(p.Program, before) { 227 x.Stacks[k] = v 228 } 229 } 230 } 231 232 uploadContents, err = json.MarshalIndent(upload, "", " ") 233 if err != nil { 234 return "", fmt.Errorf("failed to marshal upload report for %s: %v", expiryDate, err) 235 } 236 } 237 localFileName := filepath.Join(u.dir.LocalDir(), "local."+expiryDate+".json") 238 uploadFileName := filepath.Join(u.dir.LocalDir(), expiryDate+".json") 239 240 /* Prepare to write files */ 241 // if either file exists, someone has been here ahead of us 242 // (there is still a race, but this check shortens the open window) 243 if _, err := os.Stat(localFileName); err == nil { 244 u.deleteFiles(countFiles) 245 return "", fmt.Errorf("local report %s already exists", localFileName) 246 } 247 if _, err := os.Stat(uploadFileName); err == nil { 248 u.deleteFiles(countFiles) 249 return "", fmt.Errorf("report %s already exists", uploadFileName) 250 } 251 // write the uploadable file 252 var errUpload, errLocal error 253 if uploadOK { 254 _, errUpload = exclusiveWrite(uploadFileName, uploadContents) 255 } 256 // write the local file 257 _, errLocal = exclusiveWrite(localFileName, localContents) 258 /* Wrote the files */ 259 260 // even though these errors won't occur, what should happen 261 // if errUpload == nil and it is ok to upload, and errLocal != nil? 262 if errLocal != nil { 263 return "", fmt.Errorf("failed to write local file %s (%v)", localFileName, errLocal) 264 } 265 if errUpload != nil { 266 return "", fmt.Errorf("failed to write upload file %s (%v)", uploadFileName, errUpload) 267 } 268 u.logger.Printf("Created %s, deleting %d count files", filepath.Base(uploadFileName), len(countFiles)) 269 u.deleteFiles(countFiles) 270 if uploadOK { 271 return uploadFileName, nil 272 } 273 return "", nil 274} 275 276// exclusiveWrite attempts to create filename exclusively, and if successful, 277// writes content to the resulting file handle. 278// 279// It returns a boolean indicating whether the exclusive handle was acquired, 280// and an error indicating whether the operation succeeded. 281// If the file already exists, exclusiveWrite returns (false, nil). 282func exclusiveWrite(filename string, content []byte) (_ bool, rerr error) { 283 f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) 284 if err != nil { 285 if os.IsExist(err) { 286 return false, nil 287 } 288 return false, err 289 } 290 defer func() { 291 if err := f.Close(); err != nil && rerr == nil { 292 rerr = err 293 } 294 }() 295 if _, err := f.Write(content); err != nil { 296 return false, err 297 } 298 return true, nil 299} 300 301// return an existing ProgremReport, or create anew 302func findProgReport(meta map[string]string, report *telemetry.Report) *telemetry.ProgramReport { 303 for _, prog := range report.Programs { 304 if prog.Program == meta["Program"] && prog.Version == meta["Version"] && 305 prog.GoVersion == meta["GoVersion"] && prog.GOOS == meta["GOOS"] && 306 prog.GOARCH == meta["GOARCH"] { 307 return prog 308 } 309 } 310 prog := telemetry.ProgramReport{ 311 Program: meta["Program"], 312 Version: meta["Version"], 313 GoVersion: meta["GoVersion"], 314 GOOS: meta["GOOS"], 315 GOARCH: meta["GOARCH"], 316 Counters: make(map[string]int64), 317 Stacks: make(map[string]int64), 318 } 319 report.Programs = append(report.Programs, &prog) 320 return &prog 321} 322 323// computeRandom returns a cryptographic random float64 in the range [0, 1], 324// with 52 bits of precision. 325func computeRandom() float64 { 326 for { 327 b := make([]byte, 8) 328 _, err := rand.Read(b) 329 if err != nil { 330 panic(fmt.Sprintf("rand.Read failed: %v", err)) 331 } 332 // and turn it into a float64 333 x := math.Float64frombits(binary.LittleEndian.Uint64(b)) 334 if math.IsNaN(x) || math.IsInf(x, 0) { 335 continue 336 } 337 x = math.Abs(x) 338 if x < 0x1p-1000 { // avoid underflow patterns 339 continue 340 } 341 frac, _ := math.Frexp(x) // 52 bits of randomness 342 return frac*2 - 1 343 } 344} 345