Source file src/cmd/vendor/golang.org/x/telemetry/internal/upload/run.go

     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  }
   227  

View as plain text