src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

reports.go (10671B)


      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 
      5 package upload
      6 
      7 import (
      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
     24 func (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(telemetry.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.
     77 func 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
     94 func 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 
    107 func (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.
    123 func (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).
    282 func 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
    302 func 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.
    325 func 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 }