// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package upload import ( "fmt" "io" "log" "os" "path" "path/filepath" "runtime/debug" "strings" "time" "golang.org/x/telemetry/internal/configstore" "golang.org/x/telemetry/internal/telemetry" ) // RunConfig configures non-default behavior of a call to Run. // // All fields are optional, for testing or observability. type RunConfig struct { TelemetryDir string // if set, overrides the telemetry data directory UploadURL string // if set, overrides the telemetry upload endpoint LogWriter io.Writer // if set, used for detailed logging of the upload process Env []string // if set, appended to the config download environment StartTime time.Time // if set, overrides the upload start time } // Run generates and uploads reports, as allowed by the mode file. func Run(config RunConfig) error { defer func() { if err := recover(); err != nil { log.Printf("upload recover: %v", err) } }() uploader, err := newUploader(config) if err != nil { return err } defer uploader.Close() return uploader.Run() } // uploader encapsulates a single upload operation, carrying parameters and // shared state. type uploader struct { // config is used to select counters to upload. config *telemetry.UploadConfig // configVersion string // version of the config dir telemetry.Dir // the telemetry dir to process uploadServerURL string startTime time.Time cache parsedCache logFile *os.File logger *log.Logger } // newUploader creates a new uploader to use for running the upload for the // given config. // // Uploaders should only be used for one call to [uploader.Run]. func newUploader(rcfg RunConfig) (*uploader, error) { // Determine the upload directory. var dir telemetry.Dir if rcfg.TelemetryDir != "" { dir = telemetry.NewDir(rcfg.TelemetryDir) } else { dir = telemetry.Default } // Determine the upload URL. uploadURL := rcfg.UploadURL if uploadURL == "" { uploadURL = "https://telemetry.go.dev/upload" } // Determine the upload logger. // // This depends on the provided rcfg.LogWriter and the presence of // dir.DebugDir, as follows: // 1. If LogWriter is present, log to it. // 2. If DebugDir is present, log to a file within it. // 3. If both LogWriter and DebugDir are present, log to a multi writer. // 4. If neither LogWriter nor DebugDir are present, log to a noop logger. var logWriters []io.Writer logFile, err := debugLogFile(dir.DebugDir()) if err != nil { logFile = nil } if logFile != nil { logWriters = append(logWriters, logFile) } if rcfg.LogWriter != nil { logWriters = append(logWriters, rcfg.LogWriter) } var logWriter io.Writer switch len(logWriters) { case 0: logWriter = io.Discard case 1: logWriter = logWriters[0] default: logWriter = io.MultiWriter(logWriters...) } logger := log.New(logWriter, "", log.Ltime|log.Lmicroseconds|log.Lshortfile) // Fetch the upload config, if it is not provided. var ( config *telemetry.UploadConfig configVersion string ) if mode, _ := dir.Mode(); mode == "on" { // golang/go#68946: only download the upload config if it will be used. // // TODO(rfindley): This is a narrow change aimed at minimally fixing the // associated bug. In the future, we should read the mode only once during // the upload process. config, configVersion, err = configstore.Download("latest", rcfg.Env) if err != nil { return nil, err } } else { config = &telemetry.UploadConfig{} configVersion = "v0.0.0-0" } // Set the start time, if it is not provided. startTime := time.Now().UTC() if !rcfg.StartTime.IsZero() { startTime = rcfg.StartTime } return &uploader{ config: config, configVersion: configVersion, dir: dir, uploadServerURL: uploadURL, startTime: startTime, logFile: logFile, logger: logger, }, nil } // Close cleans up any resources associated with the uploader. func (u *uploader) Close() error { if u.logFile == nil { return nil } return u.logFile.Close() } // Run generates and uploads reports func (u *uploader) Run() error { if telemetry.DisabledOnPlatform { return nil } todo := u.findWork() ready, err := u.reports(&todo) if err != nil { u.logger.Printf("Error building reports: %v", err) return fmt.Errorf("reports failed: %v", err) } u.logger.Printf("Uploading %d reports", len(ready)) for _, f := range ready { u.uploadReport(f) } return nil } // debugLogFile arranges to write a log file in the given debug directory, if // it exists. func debugLogFile(debugDir string) (*os.File, error) { fd, err := os.Stat(debugDir) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } if !fd.IsDir() { return nil, fmt.Errorf("debug path %q is not a directory", debugDir) } info, ok := debug.ReadBuildInfo() if !ok { return nil, fmt.Errorf("no build info") } year, month, day := time.Now().UTC().Date() goVers := info.GoVersion // E.g., goVers:"go1.22-20240109-RC01 cl/597041403 +dcbe772469 X:loopvar" words := strings.Fields(goVers) goVers = words[0] progPkgPath := info.Path if progPkgPath == "" { progPkgPath = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") } prog := path.Base(progPkgPath) progVers := info.Main.Version if progVers == "(devel)" { // avoid special characters in created file names progVers = "devel" } logBase := strings.ReplaceAll( fmt.Sprintf("%s-%s-%s-%4d%02d%02d-%d.log", prog, progVers, goVers, year, month, day, os.Getpid()), " ", "") fname := filepath.Join(debugDir, logBase) if _, err := os.Stat(fname); err == nil { // This process previously called upload.Run return nil, nil } f, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) if err != nil { if os.IsExist(err) { return nil, nil // this process previously called upload.Run } return nil, err } return f, nil }