src

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

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 }