run.go (6102B)
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 "fmt" 9 "io" 10 "log" 11 "os" 12 "path" 13 "path/filepath" 14 "runtime/debug" 15 "strings" 16 "time" 17 18 "golang.org/x/telemetry/internal/configstore" 19 "golang.org/x/telemetry/internal/telemetry" 20 ) 21 22 // RunConfig configures non-default behavior of a call to Run. 23 // 24 // All fields are optional, for testing or observability. 25 type RunConfig struct { 26 TelemetryDir string // if set, overrides the telemetry data directory 27 UploadURL string // if set, overrides the telemetry upload endpoint 28 LogWriter io.Writer // if set, used for detailed logging of the upload process 29 Env []string // if set, appended to the config download environment 30 StartTime time.Time // if set, overrides the upload start time 31 } 32 33 // Run generates and uploads reports, as allowed by the mode file. 34 func Run(config RunConfig) error { 35 defer func() { 36 if err := recover(); err != nil { 37 log.Printf("upload recover: %v", err) 38 } 39 }() 40 uploader, err := newUploader(config) 41 if err != nil { 42 return err 43 } 44 defer uploader.Close() 45 return uploader.Run() 46 } 47 48 // uploader encapsulates a single upload operation, carrying parameters and 49 // shared state. 50 type uploader struct { 51 // config is used to select counters to upload. 52 config *telemetry.UploadConfig // 53 configVersion string // version of the config 54 dir telemetry.Dir // the telemetry dir to process 55 56 uploadServerURL string 57 startTime time.Time 58 59 cache parsedCache 60 61 logFile *os.File 62 logger *log.Logger 63 } 64 65 // newUploader creates a new uploader to use for running the upload for the 66 // given config. 67 // 68 // Uploaders should only be used for one call to [uploader.Run]. 69 func newUploader(rcfg RunConfig) (*uploader, error) { 70 // Determine the upload directory. 71 var dir telemetry.Dir 72 if rcfg.TelemetryDir != "" { 73 dir = telemetry.NewDir(rcfg.TelemetryDir) 74 } else { 75 dir = telemetry.Default 76 } 77 78 // Determine the upload URL. 79 uploadURL := rcfg.UploadURL 80 if uploadURL == "" { 81 uploadURL = "https://telemetry.go.dev/upload" 82 } 83 84 // Determine the upload logger. 85 // 86 // This depends on the provided rcfg.LogWriter and the presence of 87 // dir.DebugDir, as follows: 88 // 1. If LogWriter is present, log to it. 89 // 2. If DebugDir is present, log to a file within it. 90 // 3. If both LogWriter and DebugDir are present, log to a multi writer. 91 // 4. If neither LogWriter nor DebugDir are present, log to a noop logger. 92 var logWriters []io.Writer 93 logFile, err := debugLogFile(dir.DebugDir()) 94 if err != nil { 95 logFile = nil 96 } 97 if logFile != nil { 98 logWriters = append(logWriters, logFile) 99 } 100 if rcfg.LogWriter != nil { 101 logWriters = append(logWriters, rcfg.LogWriter) 102 } 103 var logWriter io.Writer 104 switch len(logWriters) { 105 case 0: 106 logWriter = io.Discard 107 case 1: 108 logWriter = logWriters[0] 109 default: 110 logWriter = io.MultiWriter(logWriters...) 111 } 112 logger := log.New(logWriter, "", log.Ltime|log.Lmicroseconds|log.Lshortfile) 113 114 // Fetch the upload config, if it is not provided. 115 var ( 116 config *telemetry.UploadConfig 117 configVersion string 118 ) 119 120 if mode, _ := dir.Mode(); mode == "on" { 121 // golang/go#68946: only download the upload config if it will be used. 122 // 123 // TODO(rfindley): This is a narrow change aimed at minimally fixing the 124 // associated bug. In the future, we should read the mode only once during 125 // the upload process. 126 config, configVersion, err = configstore.Download("latest", rcfg.Env) 127 if err != nil { 128 return nil, err 129 } 130 } else { 131 config = &telemetry.UploadConfig{} 132 configVersion = "v0.0.0-0" 133 } 134 135 // Set the start time, if it is not provided. 136 startTime := time.Now().UTC() 137 if !rcfg.StartTime.IsZero() { 138 startTime = rcfg.StartTime 139 } 140 141 return &uploader{ 142 config: config, 143 configVersion: configVersion, 144 dir: dir, 145 uploadServerURL: uploadURL, 146 startTime: startTime, 147 148 logFile: logFile, 149 logger: logger, 150 }, nil 151 } 152 153 // Close cleans up any resources associated with the uploader. 154 func (u *uploader) Close() error { 155 if u.logFile == nil { 156 return nil 157 } 158 return u.logFile.Close() 159 } 160 161 // Run generates and uploads reports 162 func (u *uploader) Run() error { 163 if telemetry.DisabledOnPlatform { 164 return nil 165 } 166 todo := u.findWork() 167 ready, err := u.reports(&todo) 168 if err != nil { 169 u.logger.Printf("Error building reports: %v", err) 170 return fmt.Errorf("reports failed: %v", err) 171 } 172 u.logger.Printf("Uploading %d reports", len(ready)) 173 for _, f := range ready { 174 u.uploadReport(f) 175 } 176 return nil 177 } 178 179 // debugLogFile arranges to write a log file in the given debug directory, if 180 // it exists. 181 func debugLogFile(debugDir string) (*os.File, error) { 182 fd, err := os.Stat(debugDir) 183 if os.IsNotExist(err) { 184 return nil, nil 185 } 186 if err != nil { 187 return nil, err 188 } 189 if !fd.IsDir() { 190 return nil, fmt.Errorf("debug path %q is not a directory", debugDir) 191 } 192 info, ok := debug.ReadBuildInfo() 193 if !ok { 194 return nil, fmt.Errorf("no build info") 195 } 196 year, month, day := time.Now().UTC().Date() 197 goVers := info.GoVersion 198 // E.g., goVers:"go1.22-20240109-RC01 cl/597041403 +dcbe772469 X:loopvar" 199 words := strings.Fields(goVers) 200 goVers = words[0] 201 progPkgPath := info.Path 202 if progPkgPath == "" { 203 progPkgPath = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") 204 } 205 prog := path.Base(progPkgPath) 206 progVers := info.Main.Version 207 if progVers == "(devel)" { // avoid special characters in created file names 208 progVers = "devel" 209 } 210 logBase := strings.ReplaceAll( 211 fmt.Sprintf("%s-%s-%s-%4d%02d%02d-%d.log", prog, progVers, goVers, year, month, day, os.Getpid()), 212 " ", "") 213 fname := filepath.Join(debugDir, logBase) 214 if _, err := os.Stat(fname); err == nil { 215 // This process previously called upload.Run 216 return nil, nil 217 } 218 f, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) 219 if err != nil { 220 if os.IsExist(err) { 221 return nil, nil // this process previously called upload.Run 222 } 223 return nil, err 224 } 225 return f, nil 226 }