| // 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. |
| config, configVersion, err := configstore.Download("latest", rcfg.Env) |
| if err != nil { |
| return nil, err |
| } |
| |
| // 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 |
| } |